lopata 0.18.3 → 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 +21 -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 +96 -35
  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 +28 -0
  83. package/src/tracing/span.ts +88 -56
  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,552 @@
1
+ /**
2
+ * Message protocol between main thread and worker-thread runtime.
3
+ *
4
+ * Main thread owns Bun.serve, file watcher, GenerationManager, dashboard.
5
+ * Worker thread owns the user module graph + per-thread env. Reload =
6
+ * terminate + respawn.
7
+ */
8
+
9
+ import type { WranglerConfig } from '../config'
10
+ import type { TraceStore } from '../tracing/store'
11
+ import type { SpanData, SpanEventData } from '../tracing/types'
12
+
13
+ /** Parent span context handed to the worker so its spans nest under main's server span. */
14
+ export interface ParentSpanContext {
15
+ traceId: string
16
+ spanId: string
17
+ }
18
+
19
+ export type TraceErrorPayload = Parameters<TraceStore['insertError']>[0]
20
+
21
+ export interface SerializedRequest {
22
+ url: string
23
+ method: string
24
+ headers: [string, string][]
25
+ body: ArrayBuffer | null
26
+ /**
27
+ * When set, the body is streamed (not buffered): `body` is `null` and the
28
+ * sender pumps chunk messages keyed by this id. The channel decides which
29
+ * message family (`req-stream-*` for top-level main→worker fetch,
30
+ * `rpc-req-stream-*` for cross-thread binding fetch, `do-req-stream-*` for
31
+ * main→DO-worker fetch). Receiver reconstructs a `ReadableStream` and uses
32
+ * it as the rebuilt Request's body.
33
+ */
34
+ streamId?: number
35
+ }
36
+
37
+ export interface SerializedResponse {
38
+ status: number
39
+ statusText: string
40
+ headers: [string, string][]
41
+ body: ArrayBuffer | null
42
+ /** When set, the response carries a WebSocket upgrade — main rebuilds a
43
+ * `CFWebSocket` whose peer bridges send/close to this id on the worker. */
44
+ webSocketId?: string
45
+ /**
46
+ * When set, the body is streamed (not buffered): `body` is `null` and the
47
+ * worker pumps `stream-chunk` / `stream-end` / `stream-error` for this id.
48
+ * Main rebuilds a `ReadableStream` so SSE / chunked / otherwise-unbounded
49
+ * responses reach the client incrementally instead of hanging on a never-
50
+ * resolving `arrayBuffer()`.
51
+ */
52
+ streamId?: number
53
+ }
54
+
55
+ export interface SerializedError {
56
+ message: string
57
+ stack?: string
58
+ name?: string
59
+ /** Recursively serialized `error.cause` chain (depth-capped). */
60
+ cause?: SerializedError
61
+ /** Enumerable own-properties (err.code, err.status, err.data, …) that user
62
+ * or library error handling branches on. Only structured-cloneable values
63
+ * are kept (the whole SerializedError must survive postMessage). */
64
+ props?: Record<string, unknown>
65
+ }
66
+
67
+ const MAX_CAUSE_DEPTH = 8
68
+
69
+ /**
70
+ * Serialize a thrown value for postMessage. MUST be total — it runs inside every
71
+ * worker-side catch block, so a secondary throw here (a non-stringifiable value,
72
+ * a throwing getter) would escalate a handled error into `worker.onerror` and
73
+ * tear down the whole generation. Every value read is guarded.
74
+ *
75
+ * Non-Error thrown values (`throw { status: 404 }`, `throw 'boom'`) keep their
76
+ * cloneable own-enumerable properties so `catch (e) { e.status }` survives the
77
+ * hop — class identity is lost (it always was, even in-process the wire is
78
+ * structured-clone), but the payload is preserved.
79
+ */
80
+ export function serializeError(e: unknown, depth = 0): SerializedError {
81
+ const isError = e instanceof Error
82
+ const source = e !== null && typeof e === 'object' ? e : null
83
+
84
+ let message = ''
85
+ let name: string | undefined
86
+ let stack: string | undefined
87
+ try {
88
+ if (isError) {
89
+ message = e.message
90
+ name = e.name
91
+ stack = e.stack
92
+ } else if (source) {
93
+ const m = Reflect.get(source, 'message')
94
+ message = typeof m === 'string' ? m : String(e)
95
+ const n = Reflect.get(source, 'name')
96
+ if (typeof n === 'string') name = n
97
+ } else {
98
+ message = String(e)
99
+ }
100
+ } catch {
101
+ message = 'Unserializable thrown value'
102
+ }
103
+
104
+ const out: SerializedError = { message, stack, name: name ?? (isError ? 'Error' : undefined) }
105
+
106
+ // Preserve cloneable own-enumerable properties. For real Errors these are
107
+ // extras (err.code/status/data/…); for thrown plain objects they ARE the
108
+ // payload. Read each key via Reflect.get inside the try so a throwing getter
109
+ // drops only that key instead of crashing the whole serialize.
110
+ if (source) {
111
+ const props: Record<string, unknown> = {}
112
+ for (const key of Object.keys(source)) {
113
+ if (key === 'message' || key === 'stack' || key === 'name' || key === 'cause') continue
114
+ try {
115
+ const value = Reflect.get(source, key)
116
+ structuredClone(value)
117
+ props[key] = value
118
+ } catch {}
119
+ }
120
+ if (Object.keys(props).length > 0) out.props = props
121
+ }
122
+
123
+ if (isError && e.cause !== undefined && depth < MAX_CAUSE_DEPTH) {
124
+ out.cause = serializeError(e.cause, depth + 1)
125
+ }
126
+ return out
127
+ }
128
+
129
+ export function deserializeError(err: SerializedError): Error {
130
+ const e = new Error(err.message, err.cause ? { cause: deserializeError(err.cause) } : undefined)
131
+ if (err.stack) e.stack = err.stack
132
+ e.name = err.name ?? 'Error'
133
+ if (err.props) Object.assign(e, err.props)
134
+ return e
135
+ }
136
+
137
+ export interface WorkerInitConfig {
138
+ modulePath: string
139
+ /** Wrangler config — already parsed, with `env.<name>` overrides applied. */
140
+ config: WranglerConfig
141
+ baseDir: string
142
+ /**
143
+ * Main's resolved `.lopata` data dir (`getDataDir()`). The worker thread MUST
144
+ * open the same SQLite file / r2 / d1 dirs main and the DO workers use —
145
+ * `baseDir` is per-worker (`dirname(workerDef.config)` in multi-worker mode)
146
+ * and would otherwise split state into a second db. `baseDir` is kept only for
147
+ * `.dev.vars`/`.env`/assets resolution, which IS per-worker.
148
+ */
149
+ dataDir: string
150
+ /** Worker name from `lopata.config.ts` (or single-worker wrangler config). Used
151
+ * for span attribution + error attribution; mirrors `Generation.workerName`. */
152
+ workerName?: string
153
+ /** Browser Rendering local dev config (Chrome wsEndpoint or executable path). */
154
+ browserConfig?: { wsEndpoint?: string; executablePath?: string; headless?: boolean }
155
+ }
156
+
157
+ /** Names of the worker handlers we know how to invoke via RPC. */
158
+ export type WorkerHandlerName = 'fetch' | 'scheduled' | 'email' | 'queue'
159
+
160
+ /**
161
+ * Workflow control operation, routed to the *live* worker-side
162
+ * `SqliteWorkflowBinding` (which owns the real abort controllers / event
163
+ * waiters / sleep resolvers — main's binding is hollow in thread mode).
164
+ * Initiated by the dashboard AND by DO workers, whose `this.env.WF` proxy
165
+ * models create/get/instance methods as these ops. `binding` is the wrangler
166
+ * binding name; `instanceId` targets a specific instance for everything except
167
+ * `create`.
168
+ */
169
+ export type WorkflowControlOp =
170
+ | { kind: 'create'; params: unknown; id?: string }
171
+ // Resume all interrupted (running/waiting) instances. Driven by main AFTER the
172
+ // previous generation's worker is disposed, so an interrupted workflow is never
173
+ // re-executed in the new worker while the old one is still running it.
174
+ | { kind: 'resumeInterrupted' }
175
+ | { kind: 'terminate'; instanceId: string }
176
+ | { kind: 'pause'; instanceId: string }
177
+ | { kind: 'resume'; instanceId: string }
178
+ | { kind: 'restart'; instanceId: string; fromStep?: string }
179
+ | { kind: 'skipSleep'; instanceId: string }
180
+ | { kind: 'sendEvent'; instanceId: string; eventType: string; payload?: unknown }
181
+ // Introspection reads of the worker-side in-memory registries — the dashboard
182
+ // instance detail renders "sleeping" / "waiting for events" from these.
183
+ | { kind: 'isSleeping'; instanceId: string }
184
+ | { kind: 'waitingEventTypes'; instanceId: string }
185
+ // `WorkflowInstance.status()` for DO-worker proxies; also doubles as the
186
+ // existence check behind their `get(id)`.
187
+ | { kind: 'status'; instanceId: string }
188
+
189
+ /** `WorkflowInstance.status()` payload (see `SqliteWorkflowInstance.status`). */
190
+ export interface WorkflowInstanceStatus {
191
+ status: string
192
+ output?: unknown
193
+ error?: { name: string; message: string }
194
+ }
195
+
196
+ /** Result payload of a {@link WorkflowControlOp}. `create` reports the new id,
197
+ * the introspection reads report their value; mutating ops report nothing. */
198
+ export type WorkflowControlResult =
199
+ | { kind: 'create'; id: string }
200
+ | { kind: 'ok' }
201
+ | { kind: 'isSleeping'; value: boolean }
202
+ | { kind: 'waitingEventTypes'; value: string[] }
203
+ | { kind: 'status'; value: WorkflowInstanceStatus }
204
+
205
+ export interface BindingTarget {
206
+ binding: string
207
+ /**
208
+ * DO instance target. When set, the resolving side routes through
209
+ * `env[binding].get(instanceId)` before invoking `method` / `fetch`, so
210
+ * cross-DO and self-DO access via env-RPC lands on the right instance. Used
211
+ * by both the DO-stub proxies and the executors' `_resolveBinding`.
212
+ */
213
+ instanceId?: string
214
+ /**
215
+ * @internal For DO instance targets created via `idFromName(name)`: the
216
+ * original name string. Main reconstructs `DurableObjectIdImpl(instanceId,
217
+ * instanceName)` before calling `binding.get()` so `ctx.id.name` is
218
+ * preserved across the thread boundary (matches the in-process path).
219
+ */
220
+ instanceName?: string
221
+ }
222
+
223
+ /**
224
+ * Unified cross-thread RPC frame. Used in both directions:
225
+ * - user-worker → main (`WorkerThreadExecutor`): stateful binding access from the user's worker
226
+ * - DO-worker → main (`WorkerExecutor`): same, from a DO instance worker
227
+ *
228
+ * `parent` propagates trace context so spans created on the receiving side
229
+ * (including spans inside *further* nested cross-thread hops) link back to the
230
+ * caller's active span. Without it nested service-binding calls float at root.
231
+ */
232
+ export interface RpcCallRequest {
233
+ type: 'rpc-call'
234
+ id: number
235
+ target: BindingTarget
236
+ method: string
237
+ args: unknown[]
238
+ parent?: ParentSpanContext
239
+ }
240
+
241
+ export interface RpcFetchRequest {
242
+ type: 'rpc-fetch'
243
+ id: number
244
+ target: BindingTarget
245
+ request: SerializedRequest
246
+ parent?: ParentSpanContext
247
+ }
248
+
249
+ /**
250
+ * Property read on a cross-thread binding (`await env.SVC.someProp`). The
251
+ * worker-side binding proxy is callable (for method calls) AND thenable (for
252
+ * property reads); awaiting it posts this and main resolves the property on the
253
+ * main-side binding (which itself implements the thenable property-get). The
254
+ * reply reuses {@link RpcCallReply} / {@link RpcCallErrorReply}.
255
+ */
256
+ export interface RpcGetRequest {
257
+ type: 'rpc-call-get'
258
+ id: number
259
+ target: BindingTarget
260
+ property: string
261
+ parent?: ParentSpanContext
262
+ }
263
+
264
+ export interface RpcCallReply {
265
+ type: 'rpc-call-result'
266
+ id: number
267
+ value: unknown
268
+ }
269
+
270
+ export interface RpcCallErrorReply {
271
+ type: 'rpc-call-error'
272
+ id: number
273
+ error: SerializedError
274
+ }
275
+
276
+ export interface RpcFetchReply {
277
+ type: 'rpc-fetch-result'
278
+ id: number
279
+ response: SerializedResponse
280
+ }
281
+
282
+ export interface RpcFetchErrorReply {
283
+ type: 'rpc-fetch-error'
284
+ id: number
285
+ error: SerializedError
286
+ }
287
+
288
+ /**
289
+ * Reverse-direction streaming for {@link RpcFetchReply}. When main's
290
+ * `dispatchRpcFetch` resolves a response with a body, it ships headers + a
291
+ * `streamId` immediately and pumps the body via these messages so SSE / chunked
292
+ * responses returned from a service binding reach the caller incrementally
293
+ * instead of waiting for the source to finish.
294
+ *
295
+ * Direction: main → worker (mirrors `stream-chunk` / `stream-end` /
296
+ * `stream-error` which carry the worker → main top-level fetch path).
297
+ *
298
+ * Id space: independent counter inside `dispatchRpcFetch`; carried in
299
+ * {@link SerializedResponse}.streamId on the matching `rpc-fetch-result`.
300
+ */
301
+ export interface RpcStreamChunk {
302
+ type: 'rpc-stream-chunk'
303
+ streamId: number
304
+ chunk: Uint8Array
305
+ }
306
+ export interface RpcStreamEnd {
307
+ type: 'rpc-stream-end'
308
+ streamId: number
309
+ }
310
+ export interface RpcStreamError {
311
+ type: 'rpc-stream-error'
312
+ streamId: number
313
+ error: SerializedError
314
+ }
315
+
316
+ /** worker → main: the caller cancelled the reconstructed response body
317
+ * (consumer dropped, AbortController, etc). Main stops the source reader. */
318
+ export interface RpcStreamCancel {
319
+ type: 'rpc-stream-cancel'
320
+ streamId: number
321
+ }
322
+
323
+ /** worker → main: the caller consumed an rpc-fetch response-body chunk and
324
+ * grants the sender one more credit (see `STREAM_BACKPRESSURE_WINDOW`). */
325
+ export interface RpcStreamAck {
326
+ type: 'rpc-stream-ack'
327
+ streamId: number
328
+ }
329
+
330
+ /**
331
+ * Forward-direction streaming for {@link RpcFetchRequest}'s body. The sender
332
+ * (worker-side `RpcClient.callFetch`) ships the request shell + a
333
+ * `requestStreamId` immediately and pumps the body via these messages so
334
+ * uploads / streaming proxies reach the binding's `fetch()` incrementally
335
+ * instead of waiting for the full body to buffer.
336
+ *
337
+ * Direction: sender → receiver of the unified RPC channel. For the
338
+ * user-worker channel that's worker → main; for the DO-worker channel that's
339
+ * DO worker → main. Both `WorkerThreadExecutor` and `WorkerExecutor` host an
340
+ * inbound request-stream receiver and route these messages into it.
341
+ *
342
+ * Id space: independent counter inside the sending `RpcClient`; carried in
343
+ * {@link SerializedRequest}.streamId on the matching `rpc-fetch`.
344
+ */
345
+ export interface RpcReqStreamChunk {
346
+ type: 'rpc-req-stream-chunk'
347
+ streamId: number
348
+ chunk: Uint8Array
349
+ }
350
+ export interface RpcReqStreamEnd {
351
+ type: 'rpc-req-stream-end'
352
+ streamId: number
353
+ }
354
+ export interface RpcReqStreamError {
355
+ type: 'rpc-req-stream-error'
356
+ streamId: number
357
+ error: SerializedError
358
+ }
359
+
360
+ /** receiver → sender: the binding consumer cancelled the reconstructed
361
+ * request body. Sender stops the source reader. */
362
+ export interface RpcReqStreamCancel {
363
+ type: 'rpc-req-stream-cancel'
364
+ streamId: number
365
+ }
366
+
367
+ /** receiver → sender: the binding consumer pulled a request-body chunk and
368
+ * grants the sender one more credit (cross-thread backpressure, mirrors
369
+ * {@link RpcStreamAck} on the response side). */
370
+ export interface RpcReqStreamAck {
371
+ type: 'rpc-req-stream-ack'
372
+ streamId: number
373
+ }
374
+
375
+ export type RpcReply =
376
+ | RpcCallReply
377
+ | RpcCallErrorReply
378
+ | RpcFetchReply
379
+ | RpcFetchErrorReply
380
+ | RpcStreamChunk
381
+ | RpcStreamEnd
382
+ | RpcStreamError
383
+ | RpcReqStreamCancel
384
+ | RpcReqStreamAck
385
+
386
+ /** Main → worker */
387
+ export type WorkerCommand =
388
+ | { type: 'init'; config: WorkerInitConfig }
389
+ // `props` carry the service-binding context `props` from the calling worker
390
+ // across to the target's `ExecutionContext.props`. Absent for top-level HTTP.
391
+ | { type: 'fetch'; id: number; request: SerializedRequest; parent?: ParentSpanContext; props?: Record<string, unknown> }
392
+ | { type: 'scheduled'; id: number; cronExpr: string; scheduledTime: number; parent?: ParentSpanContext }
393
+ | { type: 'email'; id: number; messageId: string; from: string; to: string; raw: Uint8Array; parent?: ParentSpanContext }
394
+ | RpcCallReply
395
+ | RpcCallErrorReply
396
+ | RpcFetchReply
397
+ | RpcFetchErrorReply
398
+ | RpcStreamChunk
399
+ | RpcStreamEnd
400
+ | RpcStreamError
401
+ | RpcReqStreamCancel
402
+ | RpcReqStreamAck
403
+ // RPC method call into the worker's user-defined entrypoint class.
404
+ // Sent from main when a `ServiceBinding` RPC callable resolves a target
405
+ // whose entrypoint class lives in a worker thread. `props` carries the
406
+ // `ServiceBinding._props` so `ctx.props` matches the in-process path;
407
+ // `parent` propagates trace context so child spans link to the caller.
408
+ | {
409
+ type: 'entrypoint-rpc'
410
+ id: number
411
+ entrypoint: string | undefined
412
+ method: string
413
+ args: unknown[]
414
+ props?: Record<string, unknown>
415
+ parent?: ParentSpanContext
416
+ }
417
+ // RPC property read on the worker's entrypoint (`await env.SVC.someProp`).
418
+ // Mirrors `entrypoint-rpc` but reads the property instead of invoking it —
419
+ // the worker reports back whether the resolved member is a function (so
420
+ // main can hand back a function-stub that RPCs through) or a plain value.
421
+ | {
422
+ type: 'entrypoint-rpc-get'
423
+ id: number
424
+ entrypoint: string | undefined
425
+ property: string
426
+ props?: Record<string, unknown>
427
+ parent?: ParentSpanContext
428
+ }
429
+ // Dashboard-initiated workflow control (create/terminate/pause/resume/
430
+ // restart/skipSleep/sendEvent + introspection reads). Routed to the worker
431
+ // because the live workflow state machine — abort controllers, event
432
+ // waiters, sleep resolvers — lives in the worker's binding instance, not
433
+ // main's hollow one.
434
+ | {
435
+ type: 'workflow-control'
436
+ id: number
437
+ binding: string
438
+ op: WorkflowControlOp
439
+ parent?: ParentSpanContext
440
+ }
441
+ // WebSocket bridge: a real client connected to main's upgraded ws sent us
442
+ // data / closed; dispatch into the user-facing peer of the worker-side pair.
443
+ | { type: 'ws-client-message'; wsId: string; data: string | ArrayBuffer }
444
+ | { type: 'ws-client-close'; wsId: string; code: number; reason: string; wasClean: boolean }
445
+ // Env-binding fetch returned `Response{status:101, webSocket}`; main adopted the
446
+ // upstream `CFWebSocket` and ships its events to the worker, where a user-facing
447
+ // peer reconstructed via `WsGuestBridge.createBridgedSocket` lets user code
448
+ // `.accept()` / `.addEventListener('message')` it. Mirrors the DO channel.
449
+ | { type: 'env-ws-incoming'; wsId: string; data: string | ArrayBuffer }
450
+ | { type: 'env-ws-close-in'; wsId: string; code: number; reason: string; wasClean: boolean }
451
+ // The reconstructed response stream was cancelled on main (client
452
+ // disconnected). Tell the worker to cancel its reader so an unbounded
453
+ // source (e.g. SSE) stops pumping instead of running forever.
454
+ | { type: 'stream-cancel'; id: number }
455
+ // Backpressure: main consumed a response-body chunk and grants the worker
456
+ // one more credit to post (see `STREAM_BACKPRESSURE_WINDOW`).
457
+ | { type: 'stream-ack'; id: number }
458
+ // Request-body streaming for the top-level user-worker fetch (main → worker).
459
+ // Mirrors `stream-chunk` / `stream-end` / `stream-error` in the opposite
460
+ // direction. Allows large or unbounded request bodies (uploads, streaming
461
+ // proxies) to reach user code incrementally instead of being buffered into
462
+ // an ArrayBuffer before crossing the worker boundary.
463
+ | { type: 'req-stream-chunk'; streamId: number; chunk: Uint8Array }
464
+ | { type: 'req-stream-end'; streamId: number }
465
+ | { type: 'req-stream-error'; streamId: number; error: SerializedError }
466
+ // Reload drain: stop the worker's queue consumers from claiming NEW messages.
467
+ // Without this the OLD generation keeps polling the shared queue for the whole
468
+ // grace period, competing with the new generation. In-flight batches finish
469
+ // (tracked via wait-until so drain waits for them).
470
+ | { type: 'stop-queue-consumers' }
471
+ // The client disconnected (main's Bun.serve `request.signal` aborted). Abort
472
+ // the worker's per-fetch AbortController so the rebuilt Request's `signal`
473
+ // fires — user code listening on `request.signal` (SSE / long-poll cleanup)
474
+ // gets the same hook it would in-process.
475
+ | { type: 'fetch-abort'; id: number }
476
+
477
+ /** Worker → main */
478
+ export type WorkerMessage =
479
+ | { type: 'need-init' }
480
+ /**
481
+ * Sent after the user module imports successfully.
482
+ *
483
+ * `doAlarmHandlers` maps each DO/container class name declared in the
484
+ * wrangler config to whether its prototype defines an `alarm()` method. Main
485
+ * uses it to forward `_setAlarmHandlerHint` to each namespace — without this,
486
+ * thread-mode `hasAlarmHandler()` would always return `false` (the main-side
487
+ * namespace doesn't load the user module). Missing exports report `false`.
488
+ */
489
+ | { type: 'ready'; doAlarmHandlers: Record<string, boolean> }
490
+ | { type: 'init-error'; error: SerializedError }
491
+ | { type: 'fetch-result'; id: number; response: SerializedResponse }
492
+ | { type: 'fetch-error'; id: number; error: SerializedError }
493
+ | { type: 'scheduled-result'; id: number }
494
+ | { type: 'scheduled-error'; id: number; error: SerializedError; noHandler?: boolean }
495
+ | { type: 'email-result'; id: number }
496
+ | { type: 'email-error'; id: number; error: SerializedError; noHandler?: boolean }
497
+ | { type: 'entrypoint-rpc-result'; id: number; value: unknown }
498
+ | { type: 'entrypoint-rpc-error'; id: number; error: SerializedError }
499
+ | { type: 'entrypoint-rpc-get-result'; id: number; kind: 'value'; value: unknown }
500
+ | { type: 'entrypoint-rpc-get-result'; id: number; kind: 'function' }
501
+ | { type: 'entrypoint-rpc-get-error'; id: number; error: SerializedError }
502
+ | { type: 'workflow-control-result'; id: number; result: WorkflowControlResult }
503
+ | { type: 'workflow-control-error'; id: number; error: SerializedError }
504
+ | RpcCallRequest
505
+ | RpcGetRequest
506
+ | RpcFetchRequest
507
+ | RpcStreamCancel
508
+ | RpcStreamAck
509
+ | RpcReqStreamChunk
510
+ | RpcReqStreamEnd
511
+ | RpcReqStreamError
512
+ // `ctx.waitUntil(p)` and its settlement. Main tracks in-flight ids so reload
513
+ // drain waits for background work the response no longer carries. Per-id
514
+ // (vs. counter) makes double-add/double-settle impossible to silently desync.
515
+ | { type: 'wait-until-add'; id: number }
516
+ | { type: 'wait-until-settle'; id: number }
517
+ // Trace store forwarding. The worker holds a `RemoteTraceStore` that posts
518
+ // each operation here; main writes to the single real `TraceStore` so the
519
+ // dashboard's subscribers fire normally.
520
+ //
521
+ // INVARIANT: wire shape == TraceStore row shape. If `SpanData` /
522
+ // `SpanEventData` / `insertError` params gain a non-optional field, bump a
523
+ // protocol version and translate at the dispatch site in `executor.ts`.
524
+ | { type: 'trace-span-insert'; span: SpanData }
525
+ | { type: 'trace-span-end'; spanId: string; endTime: number; status: 'ok' | 'error'; statusMessage: string | null }
526
+ | { type: 'trace-span-status'; spanId: string; status: 'ok' | 'error'; statusMessage: string | null }
527
+ | { type: 'trace-span-attrs'; spanId: string; attrs: Record<string, unknown> }
528
+ | { type: 'trace-span-event'; event: Omit<SpanEventData, 'id'> }
529
+ | { type: 'trace-error'; error: TraceErrorPayload }
530
+ // WebSocket bridge: user code on the worker sent data / closed the socket.
531
+ // Main dispatches into its local `CFWebSocket` so the cli/dev.ts WS handler
532
+ // forwards to the real client.
533
+ | { type: 'ws-worker-send'; wsId: string; data: string | ArrayBuffer }
534
+ | { type: 'ws-worker-close'; wsId: string; code: number; reason: string }
535
+ // User code emitted bytes / closed on a CFWebSocket reconstructed from an
536
+ // env-binding fetch upgrade. Forward to the upstream peer main adopted.
537
+ | { type: 'env-ws-outgoing'; wsId: string; data: string | ArrayBuffer }
538
+ | { type: 'env-ws-close-out'; wsId: string; code: number; reason: string; wasClean: boolean }
539
+ // Response-body streaming. The worker reads its `Response.body` reader and
540
+ // pumps chunks here; main enqueues them on the reconstructed `ReadableStream`
541
+ // it handed to `Bun.serve`. Chunks that arrive before main registers the
542
+ // stream controller are buffered (see `_pendingStreamEvents`).
543
+ | { type: 'stream-chunk'; id: number; chunk: Uint8Array }
544
+ | { type: 'stream-end'; id: number }
545
+ | { type: 'stream-error'; id: number; error: SerializedError }
546
+ // Worker → main: user code cancelled the reconstructed request body
547
+ // (e.g. `request.body.cancel()`). Stop the source pump on main.
548
+ | { type: 'req-stream-cancel'; streamId: number }
549
+ // Worker → main: the worker consumed a request-body chunk and grants main's
550
+ // pump one more credit (cross-thread backpressure for the top-level fetch
551
+ // request body, mirrors `stream-ack` on the response side).
552
+ | { type: 'req-stream-ack'; streamId: number }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Worker-side stand-in for `TraceStore`. Each operation is forwarded to
3
+ * main via postMessage so the single persistent store + dashboard
4
+ * subscribers stay the source of truth. We keep a tiny local mirror of
5
+ * span statuses because `startSpan` reads `getSpanStatus(spanId)`
6
+ * synchronously and the bridge is async-only.
7
+ */
8
+
9
+ import type { TraceStore } from '../tracing/store'
10
+ import type { SpanData, SpanEventData } from '../tracing/types'
11
+ import type { TraceErrorPayload, WorkerMessage } from './protocol'
12
+
13
+ /** Methods of `TraceStore` actually called from worker-side code. Adding a new
14
+ * call site against the store from `span.ts` / `instrument.ts` / `setup-globals.ts`
15
+ * must extend this list (and `RemoteTraceStore` below). */
16
+ type RemotedMethods = 'insertSpan' | 'endSpan' | 'setSpanStatus' | 'getSpanStatus' | 'updateAttributes' | 'addEvent' | 'insertError'
17
+
18
+ /**
19
+ * Keep only structured-clone-safe values so the `postMessage` to main can't
20
+ * throw a `DataCloneError`. Trace attributes are user-controlled (`setAttribute`,
21
+ * `addEvent`, logged objects) and may carry functions, symbols or class
22
+ * instances that survive in-process but not across the worker boundary. Mirrors
23
+ * `serializeError`'s per-key cloneable filter: a non-cloneable value is dropped,
24
+ * never thrown — a trace attribute is diagnostic and must never fail the request.
25
+ * Allocates a copy only when something actually has to be dropped.
26
+ */
27
+ function sanitizeAttributes(attrs: Record<string, unknown>): Record<string, unknown> {
28
+ let safe: Record<string, unknown> | null = null
29
+ for (const key of Object.keys(attrs)) {
30
+ try {
31
+ structuredClone(attrs[key])
32
+ } catch {
33
+ if (!safe) safe = { ...attrs }
34
+ delete safe[key]
35
+ }
36
+ }
37
+ return safe ?? attrs
38
+ }
39
+
40
+ export class RemoteTraceStore implements Pick<TraceStore, RemotedMethods> {
41
+ private _statuses = new Map<string, 'ok' | 'error' | 'unset'>()
42
+ private _post: (msg: WorkerMessage) => void
43
+
44
+ constructor(post: (msg: WorkerMessage) => void) {
45
+ this._post = post
46
+ }
47
+
48
+ insertSpan(span: SpanData): void {
49
+ this._statuses.set(span.spanId, span.status)
50
+ try {
51
+ this._post({ type: 'trace-span-insert', span })
52
+ } catch {
53
+ this._post({ type: 'trace-span-insert', span: { ...span, attributes: sanitizeAttributes(span.attributes) } })
54
+ }
55
+ }
56
+
57
+ endSpan(spanId: string, endTime: number, status: 'ok' | 'error', statusMessage?: string): void {
58
+ this._post({ type: 'trace-span-end', spanId, endTime, status, statusMessage: statusMessage ?? null })
59
+ this._statuses.delete(spanId)
60
+ }
61
+
62
+ setSpanStatus(spanId: string, status: 'ok' | 'error', statusMessage: string | null): void {
63
+ this._statuses.set(spanId, status)
64
+ this._post({ type: 'trace-span-status', spanId, status, statusMessage })
65
+ }
66
+
67
+ getSpanStatus(spanId: string): string | null {
68
+ return this._statuses.get(spanId) ?? null
69
+ }
70
+
71
+ updateAttributes(spanId: string, attrs: Record<string, unknown>): void {
72
+ try {
73
+ this._post({ type: 'trace-span-attrs', spanId, attrs })
74
+ } catch {
75
+ this._post({ type: 'trace-span-attrs', spanId, attrs: sanitizeAttributes(attrs) })
76
+ }
77
+ }
78
+
79
+ addEvent(event: Omit<SpanEventData, 'id'>): void {
80
+ try {
81
+ this._post({ type: 'trace-span-event', event })
82
+ } catch {
83
+ this._post({ type: 'trace-span-event', event: { ...event, attributes: sanitizeAttributes(event.attributes) } })
84
+ }
85
+ }
86
+
87
+ insertError(opts: TraceErrorPayload): void {
88
+ this._post({ type: 'trace-error', error: opts })
89
+ }
90
+ }
@@ -0,0 +1,5 @@
1
+ /** Re-export of the shared cross-thread RPC client. Kept as a stable import
2
+ * path for callers in `./thread-env.ts`; new code should import directly
3
+ * from `./rpc-shared`. */
4
+
5
+ export { RpcClient } from './rpc-shared'