-
Notifications
You must be signed in to change notification settings - Fork 105
Expand file tree
/
Copy pathweb.js
More file actions
241 lines (222 loc) · 8.57 KB
/
Copy pathweb.js
File metadata and controls
241 lines (222 loc) · 8.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/**
* @file Tiny utility for inserting transformation logic into Web Streams
* pipelines. Returns `TransformStream` instances; zero runtime dependencies;
* runs anywhere `TransformStream` is a global (modern browsers, Node.js,
* Deno, Bun, Cloudflare Workers). See the root `through2` entry for the
* Node.js streams variant.
*/
/** @typedef {(chunk: any, controller: TransformStreamDefaultController<any>) => void} ClassicWebTransformFn */
/** @typedef {(chunk: any) => any | Promise<any>} AsyncWebTransformFn */
/** @typedef {(source: AsyncIterable<any>) => AsyncGenerator<any, void, void>} AsyncGenWebTransformFn */
/** @typedef {ClassicWebTransformFn | AsyncWebTransformFn | AsyncGenWebTransformFn} WebTransformFn */
/** @typedef {(controller: TransformStreamDefaultController<any>) => void} ClassicWebFlushFn */
/** @typedef {() => any | Promise<any>} AsyncWebFlushFn */
/** @typedef {ClassicWebFlushFn | AsyncWebFlushFn} WebFlushFn */
const AsyncFunction = async function () {}.constructor
const AsyncGeneratorFunction = async function * () {}.constructor
/**
* @param {Function} fn
* @returns {'async' | 'asyncgen' | 'classic'}
*/
function fnKind (fn) {
if (fn instanceof AsyncGeneratorFunction) return 'asyncgen'
if (fn instanceof AsyncFunction) return 'async'
return 'classic'
}
/**
* Build a TransformStream around a transformation function (classic /
* async / async generator; auto-dispatched).
*
* @example Classic controller form (multiple enqueue per chunk)
* transform((chunk, controller) => {
* controller.enqueue(chunk)
* controller.enqueue({ size: chunk.length })
* })
* @example Async (returned value enqueued)
* transform(async (chunk) => chunk.toString().toUpperCase())
* @example Async generator (cross-chunk state is just a local var)
* transform(async function * (source) {
* let buf = ''
* for await (const chunk of source) {
* buf += chunk.toString()
* const lines = buf.split('\n')
* buf = lines.pop()
* for (const line of lines) yield line
* }
* if (buf) yield buf
* })
* @example Pipe a fetch response
* await response.body.pipeThrough(transform(async (chunk) => chunk)).pipeTo(dest)
*
* @param {WebTransformFn} [transformFn]
* @param {WebFlushFn} [flushFn]
* @returns {{ readable: ReadableStream<any>, writable: WritableStream<any> }}
*/
export function transform (transformFn, flushFn) {
if (typeof transformFn !== 'function') {
/** @type {ClassicWebTransformFn} */
const passthrough = (chunk, controller) => controller.enqueue(chunk)
transformFn = passthrough
}
const tKind = fnKind(transformFn)
if (tKind === 'asyncgen') {
return asyncGenTransform(/** @type {AsyncGenWebTransformFn} */ (transformFn), flushFn)
}
/** @type {Transformer<any, any>} */
const inner = {}
if (tKind === 'async') {
const fn = /** @type {AsyncWebTransformFn} */ (transformFn)
inner.transform = async (chunk, controller) => {
const out = await fn(chunk)
if (out !== undefined) controller.enqueue(out)
}
} else {
const fn = /** @type {ClassicWebTransformFn} */ (transformFn)
inner.transform = (chunk, controller) => fn(chunk, controller)
}
if (flushFn) {
if (fnKind(flushFn) === 'async') {
const fn = /** @type {AsyncWebFlushFn} */ (flushFn)
inner.flush = async (controller) => {
const out = await fn()
if (out !== undefined) controller.enqueue(out)
}
} else {
const fn = /** @type {ClassicWebFlushFn} */ (flushFn)
inner.flush = (controller) => fn(controller)
}
}
return new TransformStream(inner)
}
/**
* Drive an async-generator transform as a `{ readable, writable }` pair.
*
* The pair shape (rather than a `TransformStream`) is required because we
* need cancellation hooks: the `Transformer.cancel` callback is in the
* WHATWG Streams spec but not yet implemented in major browsers as of 2026,
* whereas `ReadableStream.cancel` and `WritableStream.abort` are universally
* available. The pair satisfies `pipeThrough`'s structural contract.
*
* Backpressure follows the standard pull-based model: `pull()` enqueues one
* yielded value per call and is invoked by the runtime exactly when the
* readable buffer has room; `write()` returns a pending promise when the
* internal queue is full, propagating pressure upstream.
*
* @param {AsyncGenWebTransformFn} gen
* @param {WebFlushFn} [flushFn]
* @returns {{ readable: ReadableStream<any>, writable: WritableStream<any> }}
*/
function asyncGenTransform (gen, flushFn) {
/** @type {any[]} */
const queue = []
/** @type {Array<(value?: any) => void>} */
const sourceWakeups = []
/** @type {Array<(value?: any) => void>} */
const writeWaiters = []
let sourceEnded = false
let flushed = false
const QUEUE_HWM = 16
const wakeOne = (/** @type {Array<(value?: any) => void>} */ arr) => {
const w = arr.shift()
if (w) w()
}
const wakeAll = (/** @type {Array<(value?: any) => void>} */ arr) => {
while (arr.length) /** @type {(value?: any) => void} */ (arr.shift())()
}
// The source iterable user code consumes via `for await (const x of source)`.
const source = (async function * () {
while (true) {
while (queue.length > 0) {
yield queue.shift()
wakeOne(writeWaiters)
}
if (sourceEnded) return
await new Promise((resolve) => { sourceWakeups.push(resolve) })
}
})()
// Build the user's iterator once; pull() drives it on demand.
const iter = gen(source)
// Controllers captured in start() so the two sides can cross-propagate
// termination (matching the WHATWG TransformStream behaviour: cancel on
// one side errors the other).
/** @type {ReadableStreamDefaultController<any> | null} */
let readableController = null
/** @type {WritableStreamDefaultController | null} */
let writableController = null
let terminating = false
// Shared cleanup: end the source, finalize the user's generator (running
// its `finally`), error the opposite side. Idempotent.
const finalize = async (/** @type {any} */ reason, /** @type {'readable'|'writable'} */ origin) => {
if (terminating) return
terminating = true
sourceEnded = true
wakeAll(sourceWakeups)
wakeAll(writeWaiters)
if (typeof iter.return === 'function') {
try { await iter.return() } catch { /* user finally threw; ignored */ }
}
if (origin === 'readable' && writableController) {
try { writableController.error(reason) } catch { /* already errored */ }
} else if (origin === 'writable' && readableController) {
try { readableController.error(reason) } catch { /* already errored */ }
}
}
return {
readable: new ReadableStream({
start (controller) { readableController = controller },
async pull (controller) {
try {
const { value, done } = await iter.next()
if (!done) {
controller.enqueue(value)
return
}
if (!flushed) {
flushed = true
if (flushFn) {
if (fnKind(flushFn) === 'async') {
const fn = /** @type {AsyncWebFlushFn} */ (flushFn)
const out = await fn()
if (out !== undefined) controller.enqueue(out)
} else {
// Classic flush expects a TransformStreamDefaultController-ish
// shape; synthesize one over the ReadableStream's controller.
/** @type {any} */
const proxy = {
enqueue: (/** @type {any} */ c) => controller.enqueue(c),
error: (/** @type {any} */ e) => controller.error(e),
terminate: () => controller.close()
}
const fn = /** @type {ClassicWebFlushFn} */ (flushFn)
fn(proxy)
}
}
}
controller.close()
} catch (err) {
controller.error(err)
}
},
cancel (reason) { return finalize(reason, 'readable') }
}),
writable: new WritableStream({
start (controller) { writableController = controller },
write (chunk) {
queue.push(chunk)
wakeOne(sourceWakeups)
if (queue.length >= QUEUE_HWM) {
return new Promise((resolve) => { writeWaiters.push(resolve) })
}
return undefined
},
close () {
// Clean end: let the source iterable return so the user's `for await`
// exits normally and pull() can run any flush logic.
sourceEnded = true
wakeAll(sourceWakeups)
},
abort (reason) { return finalize(reason, 'writable') }
})
}
}
export default transform