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.
Files changed (106) hide show
  1. package/dist/types/api/dispatch.d.ts +2 -2
  2. package/dist/types/api/handlers/workflows.d.ts +2 -2
  3. package/dist/types/bindings/container-cleanup.d.ts +43 -0
  4. package/dist/types/bindings/container-docker.d.ts +24 -0
  5. package/dist/types/bindings/container.d.ts +4 -2
  6. package/dist/types/bindings/do-executor-worker.d.ts +73 -89
  7. package/dist/types/bindings/do-executor.d.ts +22 -1
  8. package/dist/types/bindings/do-worker-env.d.ts +16 -7
  9. package/dist/types/bindings/durable-object.d.ts +66 -4
  10. package/dist/types/bindings/queue.d.ts +4 -1
  11. package/dist/types/bindings/rpc-stub.d.ts +23 -1
  12. package/dist/types/bindings/scheduled.d.ts +13 -3
  13. package/dist/types/bindings/service-binding.d.ts +13 -4
  14. package/dist/types/bindings/static-assets.d.ts +1 -1
  15. package/dist/types/bindings/websocket-pair.d.ts +12 -3
  16. package/dist/types/bindings/workflow.d.ts +29 -0
  17. package/dist/types/config.d.ts +2 -0
  18. package/dist/types/env.d.ts +6 -0
  19. package/dist/types/error-page-render.d.ts +6 -0
  20. package/dist/types/execution-context.d.ts +8 -0
  21. package/dist/types/generation-manager.d.ts +21 -2
  22. package/dist/types/generation.d.ts +16 -21
  23. package/dist/types/import-graph.d.ts +28 -0
  24. package/dist/types/lopata-config.d.ts +3 -3
  25. package/dist/types/plugin.d.ts +4 -1
  26. package/dist/types/rpc-validate.d.ts +9 -0
  27. package/dist/types/setup-globals.d.ts +5 -1
  28. package/dist/types/tracing/context.d.ts +12 -0
  29. package/dist/types/tracing/span.d.ts +21 -3
  30. package/dist/types/tracing/store.d.ts +17 -0
  31. package/dist/types/tsconfig.tsbuildinfo +1 -1
  32. package/dist/types/vite-plugin/index.d.ts +8 -1
  33. package/dist/types/worker-registry.d.ts +11 -4
  34. package/dist/types/worker-thread/do-protocol.d.ts +256 -0
  35. package/dist/types/worker-thread/entry.d.ts +2 -0
  36. package/dist/types/worker-thread/execution-context.d.ts +22 -0
  37. package/dist/types/worker-thread/executor.d.ts +123 -0
  38. package/dist/types/worker-thread/protocol.d.ts +553 -0
  39. package/dist/types/worker-thread/remote-trace-store.d.ts +27 -0
  40. package/dist/types/worker-thread/rpc-client.d.ts +4 -0
  41. package/dist/types/worker-thread/rpc-shared.d.ts +138 -0
  42. package/dist/types/worker-thread/serialize.d.ts +12 -0
  43. package/dist/types/worker-thread/stream-shared.d.ts +147 -0
  44. package/dist/types/worker-thread/thread-env.d.ts +42 -0
  45. package/dist/types/worker-thread/wire-handlers.d.ts +16 -0
  46. package/dist/types/worker-thread/ws-bridge-shared.d.ts +163 -0
  47. package/package.json +1 -1
  48. package/src/api/handlers/containers.ts +2 -1
  49. package/src/api/handlers/workflows.ts +39 -34
  50. package/src/bindings/container-cleanup.ts +125 -0
  51. package/src/bindings/container-docker.ts +49 -34
  52. package/src/bindings/container.ts +24 -9
  53. package/src/bindings/do-executor-inprocess.ts +9 -5
  54. package/src/bindings/do-executor-worker.ts +386 -158
  55. package/src/bindings/do-executor.ts +23 -1
  56. package/src/bindings/do-worker-entry.ts +242 -60
  57. package/src/bindings/do-worker-env.ts +296 -11
  58. package/src/bindings/durable-object.ts +231 -27
  59. package/src/bindings/email.ts +6 -0
  60. package/src/bindings/queue.ts +11 -1
  61. package/src/bindings/rpc-stub.ts +79 -10
  62. package/src/bindings/scheduled.ts +37 -30
  63. package/src/bindings/service-binding.ts +87 -33
  64. package/src/bindings/static-assets.ts +3 -2
  65. package/src/bindings/websocket-pair.ts +19 -3
  66. package/src/bindings/workflow.ts +91 -0
  67. package/src/cli/dev.ts +106 -41
  68. package/src/config.ts +6 -3
  69. package/src/db.ts +1 -0
  70. package/src/env.ts +40 -21
  71. package/src/error-page-render.ts +21 -0
  72. package/src/execution-context.ts +13 -3
  73. package/src/generation-manager.ts +144 -143
  74. package/src/generation.ts +150 -306
  75. package/src/import-graph.ts +140 -0
  76. package/src/lopata-config.ts +3 -3
  77. package/src/plugin.ts +7 -0
  78. package/src/rpc-validate.ts +29 -0
  79. package/src/setup-globals.ts +6 -17
  80. package/src/testing/durable-object.ts +5 -3
  81. package/src/testing/index.ts +8 -4
  82. package/src/tracing/context.ts +18 -0
  83. package/src/tracing/span.ts +83 -57
  84. package/src/tracing/store.ts +41 -3
  85. package/src/virtual-modules.ts +2 -0
  86. package/src/vite-plugin/dev-server-plugin.ts +4 -0
  87. package/src/vite-plugin/index.ts +8 -1
  88. package/src/worker-registry.ts +15 -2
  89. package/src/worker-thread/do-protocol.ts +237 -0
  90. package/src/worker-thread/entry.ts +453 -0
  91. package/src/worker-thread/execution-context.ts +53 -0
  92. package/src/worker-thread/executor.ts +595 -0
  93. package/src/worker-thread/protocol.ts +552 -0
  94. package/src/worker-thread/remote-trace-store.ts +90 -0
  95. package/src/worker-thread/rpc-client.ts +5 -0
  96. package/src/worker-thread/rpc-shared.ts +503 -0
  97. package/src/worker-thread/serialize.ts +37 -0
  98. package/src/worker-thread/stream-shared.ts +414 -0
  99. package/src/worker-thread/thread-env.ts +350 -0
  100. package/src/worker-thread/wire-handlers.ts +80 -0
  101. package/src/worker-thread/ws-bridge-shared.ts +482 -0
  102. package/dist/types/bindings/do-websocket-bridge.d.ts +0 -60
  103. package/dist/types/module-cache.d.ts +0 -23
  104. package/src/bindings/do-websocket-bridge.ts +0 -79
  105. package/src/module-cache.ts +0 -58
  106. 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
+ }