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.
- 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 +21 -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 +96 -35
- 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 +28 -0
- package/src/tracing/span.ts +88 -56
- 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,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
|
+
}
|