lopata 0.18.4 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/api/dispatch.d.ts +2 -2
- package/dist/types/api/handlers/workflows.d.ts +2 -2
- package/dist/types/bindings/container-cleanup.d.ts +43 -0
- package/dist/types/bindings/container-docker.d.ts +24 -0
- package/dist/types/bindings/container.d.ts +4 -2
- package/dist/types/bindings/do-executor-worker.d.ts +73 -89
- package/dist/types/bindings/do-executor.d.ts +22 -1
- package/dist/types/bindings/do-worker-env.d.ts +16 -7
- package/dist/types/bindings/durable-object.d.ts +66 -4
- package/dist/types/bindings/queue.d.ts +4 -1
- package/dist/types/bindings/rpc-stub.d.ts +23 -1
- package/dist/types/bindings/scheduled.d.ts +13 -3
- package/dist/types/bindings/service-binding.d.ts +13 -4
- package/dist/types/bindings/static-assets.d.ts +1 -1
- package/dist/types/bindings/websocket-pair.d.ts +12 -3
- package/dist/types/bindings/workflow.d.ts +29 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/env.d.ts +6 -0
- package/dist/types/error-page-render.d.ts +6 -0
- package/dist/types/execution-context.d.ts +8 -0
- package/dist/types/generation-manager.d.ts +21 -2
- package/dist/types/generation.d.ts +16 -21
- package/dist/types/import-graph.d.ts +28 -0
- package/dist/types/lopata-config.d.ts +3 -3
- package/dist/types/plugin.d.ts +4 -1
- package/dist/types/rpc-validate.d.ts +9 -0
- package/dist/types/setup-globals.d.ts +5 -1
- package/dist/types/tracing/context.d.ts +12 -0
- package/dist/types/tracing/span.d.ts +21 -3
- package/dist/types/tracing/store.d.ts +17 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/vite-plugin/index.d.ts +8 -1
- package/dist/types/worker-registry.d.ts +11 -4
- package/dist/types/worker-thread/do-protocol.d.ts +256 -0
- package/dist/types/worker-thread/entry.d.ts +2 -0
- package/dist/types/worker-thread/execution-context.d.ts +22 -0
- package/dist/types/worker-thread/executor.d.ts +123 -0
- package/dist/types/worker-thread/protocol.d.ts +553 -0
- package/dist/types/worker-thread/remote-trace-store.d.ts +27 -0
- package/dist/types/worker-thread/rpc-client.d.ts +4 -0
- package/dist/types/worker-thread/rpc-shared.d.ts +138 -0
- package/dist/types/worker-thread/serialize.d.ts +12 -0
- package/dist/types/worker-thread/stream-shared.d.ts +147 -0
- package/dist/types/worker-thread/thread-env.d.ts +42 -0
- package/dist/types/worker-thread/wire-handlers.d.ts +16 -0
- package/dist/types/worker-thread/ws-bridge-shared.d.ts +163 -0
- package/package.json +1 -1
- package/src/api/handlers/containers.ts +2 -1
- package/src/api/handlers/workflows.ts +39 -34
- package/src/bindings/container-cleanup.ts +125 -0
- package/src/bindings/container-docker.ts +49 -34
- package/src/bindings/container.ts +24 -9
- package/src/bindings/do-executor-inprocess.ts +9 -5
- package/src/bindings/do-executor-worker.ts +386 -158
- package/src/bindings/do-executor.ts +23 -1
- package/src/bindings/do-worker-entry.ts +242 -60
- package/src/bindings/do-worker-env.ts +296 -11
- package/src/bindings/durable-object.ts +231 -27
- package/src/bindings/email.ts +6 -0
- package/src/bindings/queue.ts +11 -1
- package/src/bindings/rpc-stub.ts +79 -10
- package/src/bindings/scheduled.ts +37 -30
- package/src/bindings/service-binding.ts +87 -33
- package/src/bindings/static-assets.ts +3 -2
- package/src/bindings/websocket-pair.ts +19 -3
- package/src/bindings/workflow.ts +91 -0
- package/src/cli/dev.ts +106 -41
- package/src/config.ts +6 -3
- package/src/db.ts +1 -0
- package/src/env.ts +40 -21
- package/src/error-page-render.ts +21 -0
- package/src/execution-context.ts +13 -3
- package/src/generation-manager.ts +144 -143
- package/src/generation.ts +150 -306
- package/src/import-graph.ts +140 -0
- package/src/lopata-config.ts +3 -3
- package/src/plugin.ts +7 -0
- package/src/rpc-validate.ts +29 -0
- package/src/setup-globals.ts +6 -17
- package/src/testing/durable-object.ts +5 -3
- package/src/testing/index.ts +8 -4
- package/src/tracing/context.ts +18 -0
- package/src/tracing/span.ts +83 -57
- package/src/tracing/store.ts +41 -3
- package/src/virtual-modules.ts +2 -0
- package/src/vite-plugin/dev-server-plugin.ts +4 -0
- package/src/vite-plugin/index.ts +8 -1
- package/src/worker-registry.ts +15 -2
- package/src/worker-thread/do-protocol.ts +237 -0
- package/src/worker-thread/entry.ts +453 -0
- package/src/worker-thread/execution-context.ts +53 -0
- package/src/worker-thread/executor.ts +595 -0
- package/src/worker-thread/protocol.ts +552 -0
- package/src/worker-thread/remote-trace-store.ts +90 -0
- package/src/worker-thread/rpc-client.ts +5 -0
- package/src/worker-thread/rpc-shared.ts +503 -0
- package/src/worker-thread/serialize.ts +37 -0
- package/src/worker-thread/stream-shared.ts +414 -0
- package/src/worker-thread/thread-env.ts +350 -0
- package/src/worker-thread/wire-handlers.ts +80 -0
- package/src/worker-thread/ws-bridge-shared.ts +482 -0
- package/dist/types/bindings/do-websocket-bridge.d.ts +0 -60
- package/dist/types/module-cache.d.ts +0 -23
- package/src/bindings/do-websocket-bridge.ts +0 -79
- package/src/module-cache.ts +0 -58
- package/src/tracing/global.d.ts +0 -50
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-thread stream plumbing — sender and receiver primitives used by every
|
|
3
|
+
* worker channel (top-level user-worker fetch, DO instance fetch, unified
|
|
4
|
+
* cross-thread binding-fetch RPC) on both request and response sides.
|
|
5
|
+
*
|
|
6
|
+
* `OutboundStreamRegistry` tracks active source pumps so an inbound cancel can
|
|
7
|
+
* stop the reader. `StreamReceiver` reconstructs a `ReadableStream` on the
|
|
8
|
+
* receiving end, buffering chunks that race ahead of `start()` (the controller
|
|
9
|
+
* only registers when the consumer pulls the body, which on Bun lands after
|
|
10
|
+
* the first chunk message in some interleavings). `pumpStream` is the
|
|
11
|
+
* symmetric sender-side helper: it consumes a `ReadableStream`, registers the
|
|
12
|
+
* source reader so an inbound cancel can stop it, and posts channel-specific
|
|
13
|
+
* envelope messages until the body completes or errors.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { SerializedError } from './protocol'
|
|
17
|
+
import { serializeError } from './protocol'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default cross-thread backpressure window (chunk count) for response-body
|
|
21
|
+
* streams. Bounds in-flight chunks so a fast producer (e.g. proxying a large R2
|
|
22
|
+
* object, a tight SSE/generated stream) can't race ahead of a slow consumer and
|
|
23
|
+
* grow memory unbounded. Small enough to bound memory, large enough to keep the
|
|
24
|
+
* pipe full across the postMessage round-trip.
|
|
25
|
+
*
|
|
26
|
+
* The effective ceiling is ~2x this value: the sender is seeded `window`
|
|
27
|
+
* credits up front (so it can burst without waiting a round-trip) AND the
|
|
28
|
+
* receiver's `ReadableStream` grants one more credit per `pull()` while its
|
|
29
|
+
* own queue (highWaterMark = `window`) has room. Both halves are intentional —
|
|
30
|
+
* seeding 0 would clock the pump to one chunk per postMessage round-trip from
|
|
31
|
+
* a cold start. "Window" below means this seeded half; double it for the hard
|
|
32
|
+
* memory bound.
|
|
33
|
+
*/
|
|
34
|
+
export const STREAM_BACKPRESSURE_WINDOW = 8
|
|
35
|
+
|
|
36
|
+
interface OutboundStreamState {
|
|
37
|
+
reader: { cancel(reason?: unknown): Promise<unknown> }
|
|
38
|
+
/** Remaining permits to post a chunk. `Infinity` = no backpressure (eager,
|
|
39
|
+
* the default for channels that don't opt in). */
|
|
40
|
+
credits: number
|
|
41
|
+
/** Resolver for a pump parked in `acquireCredit`, waiting for a grant. */
|
|
42
|
+
waiter: (() => void) | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class OutboundStreamRegistry {
|
|
46
|
+
private _nextStreamId = 1
|
|
47
|
+
private _streams = new Map<number, OutboundStreamState>()
|
|
48
|
+
|
|
49
|
+
allocateId(): number {
|
|
50
|
+
return this._nextStreamId++
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Number of source pumps still running. Reload drain consults this so an
|
|
54
|
+
* in-flight upload / response-body pump isn't force-terminated mid-stream. */
|
|
55
|
+
activeCount(): number {
|
|
56
|
+
return this._streams.size
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
register(
|
|
60
|
+
streamId: number,
|
|
61
|
+
reader: { cancel(reason?: unknown): Promise<unknown> },
|
|
62
|
+
initialCredits: number = Number.POSITIVE_INFINITY,
|
|
63
|
+
): void {
|
|
64
|
+
this._streams.set(streamId, { reader, credits: initialCredits, waiter: null })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sender: take one permit to post a chunk, parking until the receiver grants
|
|
69
|
+
* one if none are left. Returns `false` if the stream was cancelled/disposed
|
|
70
|
+
* while parked (the pump should stop). With the default `Infinity` credits
|
|
71
|
+
* this never blocks — eager behavior, unchanged.
|
|
72
|
+
*/
|
|
73
|
+
async acquireCredit(streamId: number): Promise<boolean> {
|
|
74
|
+
const s = this._streams.get(streamId)
|
|
75
|
+
if (!s) return false
|
|
76
|
+
if (s.credits > 0) {
|
|
77
|
+
s.credits--
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
await new Promise<void>((resolve) => {
|
|
81
|
+
s.waiter = resolve
|
|
82
|
+
})
|
|
83
|
+
const after = this._streams.get(streamId)
|
|
84
|
+
if (!after) return false // cancelled/disposed while parked
|
|
85
|
+
if (after.credits > 0) after.credits--
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Receiver granted `n` more permits — replenish and wake a parked pump. */
|
|
90
|
+
grantCredit(streamId: number, n = 1): void {
|
|
91
|
+
const s = this._streams.get(streamId)
|
|
92
|
+
if (!s) return
|
|
93
|
+
s.credits += n
|
|
94
|
+
const w = s.waiter
|
|
95
|
+
if (w) {
|
|
96
|
+
s.waiter = null
|
|
97
|
+
w()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
complete(streamId: number): void {
|
|
102
|
+
this._streams.delete(streamId)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Receiver-side cancel arrived — stop the source pump if still running. */
|
|
106
|
+
cancel(streamId: number): void {
|
|
107
|
+
const s = this._streams.get(streamId)
|
|
108
|
+
if (!s) return
|
|
109
|
+
this._streams.delete(streamId)
|
|
110
|
+
// Wake a parked pump so it exits instead of hanging on a grant that will
|
|
111
|
+
// never come.
|
|
112
|
+
const w = s.waiter
|
|
113
|
+
if (w) {
|
|
114
|
+
s.waiter = null
|
|
115
|
+
w()
|
|
116
|
+
}
|
|
117
|
+
s.reader.cancel().catch(() => {})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
disposeAll(): void {
|
|
121
|
+
for (const [, s] of this._streams) {
|
|
122
|
+
const w = s.waiter
|
|
123
|
+
if (w) {
|
|
124
|
+
s.waiter = null
|
|
125
|
+
w()
|
|
126
|
+
}
|
|
127
|
+
s.reader.cancel().catch(() => {})
|
|
128
|
+
}
|
|
129
|
+
this._streams.clear()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Receiver-side state for an inbound stream channel. Holds open `ReadableStream`
|
|
135
|
+
* controllers keyed by streamId and a small per-streamId pending-events queue
|
|
136
|
+
* for chunks that arrive before the consumer's `start()` registers the
|
|
137
|
+
* controller.
|
|
138
|
+
*
|
|
139
|
+
* Wiring code (each channel's `onmessage` dispatcher) routes channel-specific
|
|
140
|
+
* `*-chunk` / `*-end` / `*-error` messages into {@link push} / {@link end} /
|
|
141
|
+
* {@link error}. The `onCancel` callback is invoked when the consumer cancels
|
|
142
|
+
* the reconstructed stream, so the wiring code can post the channel-specific
|
|
143
|
+
* `*-cancel` message back to the sender.
|
|
144
|
+
*/
|
|
145
|
+
export interface StreamReceiverOptions {
|
|
146
|
+
/**
|
|
147
|
+
* Enable cross-thread backpressure. When set, the reconstructed
|
|
148
|
+
* `ReadableStream` uses this as its highWaterMark (chunk count) and the
|
|
149
|
+
* receiver grants the sender a credit (via {@link StreamReceiverOptions.onCredit})
|
|
150
|
+
* as it pulls — bounding the number of in-flight chunks instead of letting a
|
|
151
|
+
* fast producer race ahead and grow memory unbounded. Omit for eager
|
|
152
|
+
* (unbounded) behavior — the default.
|
|
153
|
+
*/
|
|
154
|
+
window?: number
|
|
155
|
+
/** Post the channel-specific `*-stream-ack` message granting one credit. Only
|
|
156
|
+
* consulted when `window` is set. */
|
|
157
|
+
onCredit?: (streamId: number) => void
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class StreamReceiver {
|
|
161
|
+
private _controllers = new Map<number, ReadableStreamDefaultController<Uint8Array>>()
|
|
162
|
+
private _pending = new Map<number, StreamEvent[]>()
|
|
163
|
+
private _cancelled = new Set<number>()
|
|
164
|
+
/** Streams reconstructed via `open()` that haven't reached a terminal event
|
|
165
|
+
* (end/error/cancel) yet. Reload drain consults `activeCount()` so a response
|
|
166
|
+
* the client is still downloading (SSE, large proxy) isn't cut off mid-body. */
|
|
167
|
+
private _open = new Set<number>()
|
|
168
|
+
private _onCancel: (streamId: number) => void
|
|
169
|
+
private _window?: number
|
|
170
|
+
private _onCredit?: (streamId: number) => void
|
|
171
|
+
/**
|
|
172
|
+
* Cancels a reconstructed stream that was dropped (garbage-collected) before
|
|
173
|
+
* reaching a terminal event — e.g. a handler that never read a request body, or
|
|
174
|
+
* a caller that ignored a binding-fetch response body. Without this the sender's
|
|
175
|
+
* pump parks in `acquireCredit` forever (holding the locked source reader) until
|
|
176
|
+
* the generation is disposed. GC only collects truly-unreferenced streams, so a
|
|
177
|
+
* body still held by `waitUntil` is never wrongly cancelled.
|
|
178
|
+
*/
|
|
179
|
+
private _finalizer = new FinalizationRegistry<number>((streamId) => {
|
|
180
|
+
if (this._open.has(streamId)) this.cancel(streamId)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
constructor(onCancel: (streamId: number) => void, options: StreamReceiverOptions = {}) {
|
|
184
|
+
this._onCancel = onCancel
|
|
185
|
+
this._window = options.window
|
|
186
|
+
this._onCredit = options.onCredit
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Number of reconstructed streams not yet terminated. */
|
|
190
|
+
activeCount(): number {
|
|
191
|
+
return this._open.size
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
open(streamId: number): ReadableStream<Uint8Array> {
|
|
195
|
+
this._open.add(streamId)
|
|
196
|
+
type Source = {
|
|
197
|
+
start: (controller: ReadableStreamDefaultController<Uint8Array>) => void
|
|
198
|
+
pull?: (controller: ReadableStreamDefaultController<Uint8Array>) => void
|
|
199
|
+
cancel?: (reason?: unknown) => void
|
|
200
|
+
}
|
|
201
|
+
const source: Source = {
|
|
202
|
+
start: (controller) => {
|
|
203
|
+
this._controllers.set(streamId, controller)
|
|
204
|
+
const pending = this._pending.get(streamId)
|
|
205
|
+
if (pending) {
|
|
206
|
+
this._pending.delete(streamId)
|
|
207
|
+
for (const ev of pending) this._apply(streamId, controller, ev)
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
cancel: () => {
|
|
211
|
+
this._controllers.delete(streamId)
|
|
212
|
+
this._pending.delete(streamId)
|
|
213
|
+
this._open.delete(streamId)
|
|
214
|
+
this._cancelled.add(streamId)
|
|
215
|
+
this._onCancel(streamId)
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
// Backpressure mode: bound the queue and grant the sender a credit each
|
|
219
|
+
// time the stream pulls (i.e. has room). `pull` returns undefined, so the
|
|
220
|
+
// stream calls it once per drain rather than spinning. Eager mode (no
|
|
221
|
+
// window) keeps the original unbounded behavior.
|
|
222
|
+
const stream = this._window !== undefined && this._onCredit
|
|
223
|
+
? (() => {
|
|
224
|
+
const onCredit = this._onCredit
|
|
225
|
+
source.pull = () => {
|
|
226
|
+
onCredit(streamId)
|
|
227
|
+
}
|
|
228
|
+
return new ReadableStream<Uint8Array>(source, new CountQueuingStrategy({ highWaterMark: this._window }))
|
|
229
|
+
})()
|
|
230
|
+
: new ReadableStream<Uint8Array>(source)
|
|
231
|
+
// Cancel the sender's pump if this stream is dropped without being read.
|
|
232
|
+
this._finalizer.register(stream, streamId)
|
|
233
|
+
return stream
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Receiver-side cancel for an inbound stream that never reached a consumer
|
|
238
|
+
* (e.g. dispatcher errored before `open()` registered the controller, or the
|
|
239
|
+
* request was torn down post-open). Drops any buffered events, marks the id
|
|
240
|
+
* stale so late chunks are ignored, and signals the sender via `onCancel`.
|
|
241
|
+
*/
|
|
242
|
+
cancel(streamId: number): void {
|
|
243
|
+
this._controllers.delete(streamId)
|
|
244
|
+
this._pending.delete(streamId)
|
|
245
|
+
this._open.delete(streamId)
|
|
246
|
+
this._cancelled.add(streamId)
|
|
247
|
+
this._onCancel(streamId)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
push(streamId: number, chunk: Uint8Array): void {
|
|
251
|
+
this._onEvent(streamId, { kind: 'chunk', chunk })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
end(streamId: number): void {
|
|
255
|
+
this._onEvent(streamId, { kind: 'end' })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
error(streamId: number, err: Error): void {
|
|
259
|
+
this._onEvent(streamId, { kind: 'error', error: err })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
disposeAll(err: Error): void {
|
|
263
|
+
for (const [, controller] of this._controllers) {
|
|
264
|
+
try {
|
|
265
|
+
controller.error(err)
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
this._controllers.clear()
|
|
269
|
+
this._pending.clear()
|
|
270
|
+
this._cancelled.clear()
|
|
271
|
+
this._open.clear()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private _onEvent(streamId: number, ev: StreamEvent): void {
|
|
275
|
+
if (this._cancelled.has(streamId)) {
|
|
276
|
+
// Consumer cancelled the reconstructed stream — drop racing late events
|
|
277
|
+
// from the sender (it hadn't yet seen the cancel). Forget the id once
|
|
278
|
+
// the sender's terminator arrives so we don't grow the Set unbounded.
|
|
279
|
+
if (ev.kind !== 'chunk') this._cancelled.delete(streamId)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
const controller = this._controllers.get(streamId)
|
|
283
|
+
if (!controller) {
|
|
284
|
+
let q = this._pending.get(streamId)
|
|
285
|
+
if (!q) {
|
|
286
|
+
q = []
|
|
287
|
+
this._pending.set(streamId, q)
|
|
288
|
+
}
|
|
289
|
+
q.push(ev)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
this._apply(streamId, controller, ev)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private _apply(
|
|
296
|
+
streamId: number,
|
|
297
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
298
|
+
ev: StreamEvent,
|
|
299
|
+
): void {
|
|
300
|
+
try {
|
|
301
|
+
if (ev.kind === 'chunk') {
|
|
302
|
+
controller.enqueue(ev.chunk)
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
if (ev.kind === 'end') controller.close()
|
|
306
|
+
else controller.error(ev.error)
|
|
307
|
+
} catch {
|
|
308
|
+
// Already closed/errored (consumer cancelled) — drop.
|
|
309
|
+
}
|
|
310
|
+
if (ev.kind !== 'chunk') {
|
|
311
|
+
this._controllers.delete(streamId)
|
|
312
|
+
this._open.delete(streamId)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
type StreamEvent =
|
|
318
|
+
| { kind: 'chunk'; chunk: Uint8Array }
|
|
319
|
+
| { kind: 'end' }
|
|
320
|
+
| { kind: 'error'; error: Error }
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Channel-specific envelope builders. Each callsite supplies its own message
|
|
324
|
+
* shapes (e.g. `{ type: 'stream-chunk', id, chunk }` vs `{ type:
|
|
325
|
+
* 'do-stream-chunk', streamId, chunk }`); the pump loop itself is identical.
|
|
326
|
+
*/
|
|
327
|
+
export interface PumpEnvelopes<TChunk, TEnd, TError> {
|
|
328
|
+
chunk: (streamId: number, chunk: Uint8Array) => TChunk
|
|
329
|
+
end: (streamId: number) => TEnd
|
|
330
|
+
error: (streamId: number, error: SerializedError) => TError
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Read a body to completion and post channel-specific envelopes for each
|
|
335
|
+
* chunk + the terminator. The reader is registered with `registry` so an
|
|
336
|
+
* inbound cancel can stop the source; `complete()` runs on every exit path.
|
|
337
|
+
*
|
|
338
|
+
* `isAlive` is optional — worker-side callers omit it (worker termination
|
|
339
|
+
* kills the loop). Main-side callers pass a closure over their disposal flag
|
|
340
|
+
* so a posted message after teardown is dropped at the source (matches the
|
|
341
|
+
* pre-refactor main-side pumps).
|
|
342
|
+
*/
|
|
343
|
+
export function pumpStream<TChunk, TEnd, TError>(
|
|
344
|
+
streamId: number,
|
|
345
|
+
body: ReadableStream<Uint8Array>,
|
|
346
|
+
registry: OutboundStreamRegistry,
|
|
347
|
+
post: (msg: TChunk | TEnd | TError) => void,
|
|
348
|
+
envelopes: PumpEnvelopes<TChunk, TEnd, TError>,
|
|
349
|
+
isAlive?: () => boolean,
|
|
350
|
+
/** When set, post at most `window` chunks ahead of the receiver's credits
|
|
351
|
+
* (cross-thread backpressure). Requires the receiver's `StreamReceiver` to be
|
|
352
|
+
* constructed with a matching `window` + `onCredit`. Omit for eager. */
|
|
353
|
+
window?: number,
|
|
354
|
+
/** Called once the pump exits for any reason (end / error / cancel / teardown).
|
|
355
|
+
* Lets callers tie per-stream resource cleanup to the body's completion. */
|
|
356
|
+
onComplete?: () => void,
|
|
357
|
+
): void {
|
|
358
|
+
void (async () => {
|
|
359
|
+
try {
|
|
360
|
+
// `getReader()` throws synchronously on a locked/disturbed body — e.g. the
|
|
361
|
+
// user did `await res.text()` then returned `res`. Doing it inside the try
|
|
362
|
+
// (rather than before the IIFE) surfaces that to the receiver as a stream
|
|
363
|
+
// error instead of letting the throw escape AFTER the headers/result were
|
|
364
|
+
// already posted, which would leave the consumer hanging on a body that
|
|
365
|
+
// never produces a chunk or terminator. Runs synchronously (before the
|
|
366
|
+
// first await), so registration stays synchronous as before.
|
|
367
|
+
const reader = body.getReader()
|
|
368
|
+
registry.register(streamId, reader, window ?? Number.POSITIVE_INFINITY)
|
|
369
|
+
while (true) {
|
|
370
|
+
const { done, value } = await reader.read()
|
|
371
|
+
if (isAlive && !isAlive()) {
|
|
372
|
+
// Channel torn down between this read and posting — release the
|
|
373
|
+
// source so it doesn't stay locked. `finally` removes us from the
|
|
374
|
+
// registry, so `disposeAll` won't see this reader.
|
|
375
|
+
reader.cancel().catch(() => {})
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
if (done) break
|
|
379
|
+
if (value) {
|
|
380
|
+
// Backpressure: park until the receiver has room. No-op (immediate)
|
|
381
|
+
// for eager streams (Infinity credits).
|
|
382
|
+
const ok = await registry.acquireCredit(streamId)
|
|
383
|
+
if (!ok) {
|
|
384
|
+
// Cancelled or disposed while parked. On a receiver-initiated cancel
|
|
385
|
+
// the channel is still alive — post a terminator so the receiver
|
|
386
|
+
// clears its cancelled-stream bookkeeping (`_cancelled`) instead of
|
|
387
|
+
// leaking the id until the next `disposeAll`. On teardown
|
|
388
|
+
// (`isAlive()` false) posting is moot. Worker-side pumps omit
|
|
389
|
+
// `isAlive` and only reach here via a receiver cancel, so they post.
|
|
390
|
+
if (!isAlive || isAlive()) post(envelopes.end(streamId))
|
|
391
|
+
reader.cancel().catch(() => {})
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
if (isAlive && !isAlive()) {
|
|
395
|
+
reader.cancel().catch(() => {})
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
post(envelopes.chunk(streamId, value))
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (isAlive && !isAlive()) {
|
|
402
|
+
reader.cancel().catch(() => {})
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
post(envelopes.end(streamId))
|
|
406
|
+
} catch (e) {
|
|
407
|
+
if (isAlive && !isAlive()) return
|
|
408
|
+
post(envelopes.error(streamId, serializeError(e)))
|
|
409
|
+
} finally {
|
|
410
|
+
registry.complete(streamId)
|
|
411
|
+
onComplete?.()
|
|
412
|
+
}
|
|
413
|
+
})()
|
|
414
|
+
}
|