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,453 @@
|
|
|
1
|
+
/** Worker-thread entry: imports user module, builds env, dispatches fetch. */
|
|
2
|
+
|
|
3
|
+
import { ForwardableEmailMessage } from '../bindings/email'
|
|
4
|
+
import { createScheduledController } from '../bindings/scheduled'
|
|
5
|
+
import { resolveEntrypointTarget } from '../bindings/service-binding'
|
|
6
|
+
import { CFWebSocket, type ResponseWithWebSocket } from '../bindings/websocket-pair'
|
|
7
|
+
import { getDatabase } from '../db'
|
|
8
|
+
import { getActiveContext, runWithParentContext } from '../tracing/context'
|
|
9
|
+
import { setTraceStoreOverride } from '../tracing/store'
|
|
10
|
+
import { trackBackgroundWork, WorkerExecutionContext } from './execution-context'
|
|
11
|
+
import type {
|
|
12
|
+
ParentSpanContext,
|
|
13
|
+
SerializedResponse,
|
|
14
|
+
WorkerCommand,
|
|
15
|
+
WorkerHandlerName,
|
|
16
|
+
WorkerInitConfig,
|
|
17
|
+
WorkerMessage,
|
|
18
|
+
WorkflowControlOp,
|
|
19
|
+
WorkflowControlResult,
|
|
20
|
+
} from './protocol'
|
|
21
|
+
import { deserializeError, serializeError } from './protocol'
|
|
22
|
+
import { RemoteTraceStore } from './remote-trace-store'
|
|
23
|
+
import { RpcClient } from './rpc-shared'
|
|
24
|
+
import { deserializeRequest } from './serialize'
|
|
25
|
+
import { OutboundStreamRegistry, pumpStream, STREAM_BACKPRESSURE_WINDOW, StreamReceiver } from './stream-shared'
|
|
26
|
+
import { buildThreadEnv } from './thread-env'
|
|
27
|
+
import { startThreadQueueConsumers, wireWorkflows } from './wire-handlers'
|
|
28
|
+
import { WsGuestBridge } from './ws-bridge-shared'
|
|
29
|
+
|
|
30
|
+
declare var self: Worker
|
|
31
|
+
|
|
32
|
+
function post(msg: WorkerMessage): void {
|
|
33
|
+
postMessage(msg)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Outbound response-body pumps for the top-level fetch response (worker →
|
|
37
|
+
* main). Inbound `stream-cancel` (client disconnected) stops the source. */
|
|
38
|
+
const responseStreams = new OutboundStreamRegistry()
|
|
39
|
+
/** Active reconstructed top-level request bodies (main → worker). Chunks
|
|
40
|
+
* arriving before user code pulls the body queue inside the receiver. */
|
|
41
|
+
const requestStreams = new StreamReceiver(
|
|
42
|
+
(streamId) => {
|
|
43
|
+
post({ type: 'req-stream-cancel', streamId })
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
window: STREAM_BACKPRESSURE_WINDOW,
|
|
47
|
+
onCredit: (streamId) => post({ type: 'req-stream-ack', streamId }),
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Serialize response status + headers only — the body is never buffered here.
|
|
53
|
+
* WS upgrades carry a `webSocketId`; every other body is streamed via a
|
|
54
|
+
* `streamId` (see `pumpResponseBody`). Buffering bodies via `arrayBuffer()` is
|
|
55
|
+
* what made unbounded responses (SSE, chunked) hang forever.
|
|
56
|
+
*/
|
|
57
|
+
function serializeResponse(response: Response, ws: WsGuestBridge<WorkerMessage>): SerializedResponse {
|
|
58
|
+
const cfSocket = (response as ResponseWithWebSocket).webSocket
|
|
59
|
+
const headers: [string, string][] = []
|
|
60
|
+
response.headers.forEach((v, k) => headers.push([k, v]))
|
|
61
|
+
const base = { status: response.status, statusText: response.statusText, headers, body: null }
|
|
62
|
+
if (response.status === 101 && cfSocket) {
|
|
63
|
+
// Always a CFWebSocket: `new WebSocketPair()` peers are, and a peer that
|
|
64
|
+
// came back through a binding fetch was reconstructed as one by
|
|
65
|
+
// `createBridgedSocket` (thread-env's proxyFetch) — reshipping it just
|
|
66
|
+
// re-registers the bridged peer here (double-bridge to the client).
|
|
67
|
+
if (!(cfSocket instanceof CFWebSocket)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'Response.webSocket is not a CFWebSocket — '
|
|
70
|
+
+ 'lopata can only ship WebSockets created via `new WebSocketPair()` or returned from a binding fetch.',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return { ...base, webSocketId: ws.register(cfSocket) }
|
|
74
|
+
}
|
|
75
|
+
if (response.body) {
|
|
76
|
+
return { ...base, streamId: responseStreams.allocateId() }
|
|
77
|
+
}
|
|
78
|
+
return base
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type StreamChunkMsg = Extract<WorkerMessage, { type: 'stream-chunk' }>
|
|
82
|
+
type StreamEndMsg = Extract<WorkerMessage, { type: 'stream-end' }>
|
|
83
|
+
type StreamErrorMsg = Extract<WorkerMessage, { type: 'stream-error' }>
|
|
84
|
+
|
|
85
|
+
/** Per-in-flight-fetch AbortControllers, keyed by the main-side fetch id. Main
|
|
86
|
+
* posts `fetch-abort` on client disconnect; aborting fires the rebuilt Request's
|
|
87
|
+
* `signal` so user code's `request.signal.addEventListener('abort', …)` runs. */
|
|
88
|
+
const fetchAbortControllers = new Map<number, AbortController>()
|
|
89
|
+
|
|
90
|
+
/** Pump a response body to main as `stream-chunk`s, terminated by `stream-end`
|
|
91
|
+
* or `stream-error`. Started only after `fetch-result` is posted so main has
|
|
92
|
+
* the `streamId` registered before chunks arrive. */
|
|
93
|
+
function pumpResponseBody(streamId: number, body: ReadableStream<Uint8Array>, onComplete?: () => void): void {
|
|
94
|
+
pumpStream<StreamChunkMsg, StreamEndMsg, StreamErrorMsg>(
|
|
95
|
+
streamId,
|
|
96
|
+
body,
|
|
97
|
+
responseStreams,
|
|
98
|
+
post,
|
|
99
|
+
{
|
|
100
|
+
chunk: (id, chunk) => ({ type: 'stream-chunk', id, chunk }),
|
|
101
|
+
end: (id) => ({ type: 'stream-end', id }),
|
|
102
|
+
error: (id, error) => ({ type: 'stream-error', id, error }),
|
|
103
|
+
},
|
|
104
|
+
undefined,
|
|
105
|
+
STREAM_BACKPRESSURE_WINDOW,
|
|
106
|
+
onComplete,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
self.onmessage = async (event: MessageEvent<WorkerCommand>) => {
|
|
111
|
+
const msg = event.data
|
|
112
|
+
if (msg.type !== 'init') return
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await initRuntime(msg.config)
|
|
116
|
+
} catch (e) {
|
|
117
|
+
post({ type: 'init-error', error: serializeError(e) })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
post({ type: 'need-init' })
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Dispatch a request to a legacy service-worker `fetch` handler via a minimal
|
|
125
|
+
* `FetchEvent` shim. `respondWith` must be called synchronously during dispatch
|
|
126
|
+
* (the spec allows passing it a promise); resolve with whatever it's handed.
|
|
127
|
+
*/
|
|
128
|
+
function dispatchServiceWorkerFetch(
|
|
129
|
+
handler: (event: unknown) => void,
|
|
130
|
+
request: Request,
|
|
131
|
+
ctx: WorkerExecutionContext,
|
|
132
|
+
): Promise<Response> {
|
|
133
|
+
return new Promise<Response>((resolve, reject) => {
|
|
134
|
+
let responded = false
|
|
135
|
+
const event = {
|
|
136
|
+
type: 'fetch',
|
|
137
|
+
request,
|
|
138
|
+
respondWith(r: Response | Promise<Response>) {
|
|
139
|
+
responded = true
|
|
140
|
+
Promise.resolve(r).then(resolve, reject)
|
|
141
|
+
},
|
|
142
|
+
waitUntil(p: Promise<unknown>) {
|
|
143
|
+
ctx.waitUntil(p)
|
|
144
|
+
},
|
|
145
|
+
passThroughOnException() {
|
|
146
|
+
ctx.passThroughOnException()
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
handler(event)
|
|
151
|
+
} catch (e) {
|
|
152
|
+
reject(e)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (!responded) {
|
|
156
|
+
reject(new Error('Service worker "fetch" handler did not call event.respondWith()'))
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function initRuntime(init: WorkerInitConfig) {
|
|
162
|
+
// Plugin import must run before user code so Bun.plugin().module() intercepts
|
|
163
|
+
// `cloudflare:workers` etc. and `globalThis.caches` is patched in.
|
|
164
|
+
const plugin = await import('../plugin')
|
|
165
|
+
|
|
166
|
+
// Route all tracing operations through main so the dashboard's subscribers fire.
|
|
167
|
+
setTraceStoreOverride(new RemoteTraceStore(post))
|
|
168
|
+
|
|
169
|
+
const getParent = (): ParentSpanContext | undefined => {
|
|
170
|
+
const active = getActiveContext()
|
|
171
|
+
return active ? { traceId: active.traceId, spanId: active.spanId } : undefined
|
|
172
|
+
}
|
|
173
|
+
const rpc = new RpcClient(post, getParent)
|
|
174
|
+
const wsBridge = new WsGuestBridge<WorkerMessage>(post, {
|
|
175
|
+
remoteMessage: (wsId, data) => ({ type: 'ws-worker-send', wsId, data }),
|
|
176
|
+
// User-worker channel doesn't propagate wasClean — drop it (matches the
|
|
177
|
+
// pre-refactor wire format in `protocol.ts`).
|
|
178
|
+
remoteClose: (wsId, code, reason, _wasClean) => ({ type: 'ws-worker-close', wsId, code, reason }),
|
|
179
|
+
})
|
|
180
|
+
// Bridge for `Response{webSocket}` returned by env-binding fetches the worker
|
|
181
|
+
// calls (`env.DO.fetch('/ws')`). The user-facing peer lives here; user code can
|
|
182
|
+
// consume it, and reshipping it registers it on `wsBridge` (double-bridge).
|
|
183
|
+
const envWsBridge = new WsGuestBridge<WorkerMessage>(post, {
|
|
184
|
+
remoteMessage: (wsId, data) => ({ type: 'env-ws-outgoing', wsId, data }),
|
|
185
|
+
remoteClose: (wsId, code, reason, wasClean) => ({ type: 'env-ws-close-out', wsId, code, reason, wasClean }),
|
|
186
|
+
})
|
|
187
|
+
const built = buildThreadEnv({
|
|
188
|
+
config: init.config,
|
|
189
|
+
baseDir: init.baseDir,
|
|
190
|
+
dataDir: init.dataDir,
|
|
191
|
+
rpc,
|
|
192
|
+
browserConfig: init.browserConfig,
|
|
193
|
+
envWsBridge,
|
|
194
|
+
})
|
|
195
|
+
const { env } = built
|
|
196
|
+
|
|
197
|
+
// Make env visible to top-level `import { env } from 'cloudflare:workers'`
|
|
198
|
+
// in the user module — that import resolves to `globalEnv` from `src/env.ts`,
|
|
199
|
+
// which is empty until we publish it. Must happen BEFORE the dynamic import
|
|
200
|
+
// below.
|
|
201
|
+
const { setGlobalEnv } = await import('../env')
|
|
202
|
+
setGlobalEnv(env)
|
|
203
|
+
|
|
204
|
+
// Route binding-RPC replies BEFORE importing the user module. A module that
|
|
205
|
+
// touches a stateful binding at top level (`await env.SVC.fetch()`,
|
|
206
|
+
// `env.QUEUE.send()`, a DO stub call) posts an RPC during `import()`; until the
|
|
207
|
+
// full handler below is installed `self.onmessage` is the bootstrap handler that
|
|
208
|
+
// drops everything but `init`, so the reply would never arrive, the import's
|
|
209
|
+
// await would hang, `ready` would never post, and `GenerationManager.reload()`
|
|
210
|
+
// would wait on `executor.ready()` forever. fetch/scheduled/email never arrive
|
|
211
|
+
// pre-ready (main awaits `ready` before sending them), so handling RPC replies
|
|
212
|
+
// is enough.
|
|
213
|
+
self.onmessage = (event: MessageEvent<WorkerCommand>) => {
|
|
214
|
+
rpc.handle(event.data as { type: string })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const workerModule = await import(init.modulePath)
|
|
218
|
+
const defaultExport = workerModule.default
|
|
219
|
+
|
|
220
|
+
// Introspect DO + container classes for `alarm()` so main's
|
|
221
|
+
// `DurableObjectNamespaceImpl.hasAlarmHandler()` returns the right value in
|
|
222
|
+
// thread mode (main itself doesn't load the user module, so without this
|
|
223
|
+
// hint it would shortcircuit to `false`).
|
|
224
|
+
const doAlarmHandlers: Record<string, boolean> = {}
|
|
225
|
+
const collectAlarmHandler = (className: string) => {
|
|
226
|
+
if (doAlarmHandlers[className] !== undefined) return
|
|
227
|
+
const cls = (workerModule as Record<string, unknown>)[className] as
|
|
228
|
+
| { prototype?: { alarm?: unknown } }
|
|
229
|
+
| undefined
|
|
230
|
+
doAlarmHandlers[className] = typeof cls?.prototype?.alarm === 'function'
|
|
231
|
+
}
|
|
232
|
+
for (const binding of init.config.durable_objects?.bindings ?? []) {
|
|
233
|
+
collectAlarmHandler(binding.class_name)
|
|
234
|
+
}
|
|
235
|
+
for (const container of init.config.containers ?? []) {
|
|
236
|
+
collectAlarmHandler(container.class_name)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
wireWorkflows(built, workerModule)
|
|
240
|
+
// Queue consumers run as setIntervals in this thread. A reload drains the old
|
|
241
|
+
// generation: main posts `stop-queue-consumers` so we stop claiming NEW
|
|
242
|
+
// messages (instead of competing with the new generation for the whole grace
|
|
243
|
+
// period), while in-flight batches finish — each registered with reload drain
|
|
244
|
+
// via `trackBackgroundWork` so `isIdle()` waits for them.
|
|
245
|
+
const queueConsumers = startThreadQueueConsumers(
|
|
246
|
+
init.config,
|
|
247
|
+
built.db,
|
|
248
|
+
env,
|
|
249
|
+
workerModule,
|
|
250
|
+
init.workerName,
|
|
251
|
+
(p) => trackBackgroundWork(post, p),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
const invokeEntrypointRpc = async (
|
|
255
|
+
entrypoint: string | undefined,
|
|
256
|
+
method: string,
|
|
257
|
+
args: unknown[],
|
|
258
|
+
props?: Record<string, unknown>,
|
|
259
|
+
): Promise<unknown> => {
|
|
260
|
+
const ctx = new WorkerExecutionContext(post, props)
|
|
261
|
+
const target = resolveEntrypointTarget(workerModule, entrypoint, ctx, env)
|
|
262
|
+
const member = target?.[method]
|
|
263
|
+
if (typeof member !== 'function') {
|
|
264
|
+
throw new Error(`Service binding RPC: "${method}" is not a function on the ${entrypoint ?? 'default'} entrypoint`)
|
|
265
|
+
}
|
|
266
|
+
return await (member as (...a: unknown[]) => unknown).call(target, ...args)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const invokeEntrypointPropertyGet = (
|
|
270
|
+
entrypoint: string | undefined,
|
|
271
|
+
property: string,
|
|
272
|
+
props?: Record<string, unknown>,
|
|
273
|
+
): { kind: 'value'; value: unknown } | { kind: 'function' } => {
|
|
274
|
+
const ctx = new WorkerExecutionContext(post, props)
|
|
275
|
+
const target = resolveEntrypointTarget(workerModule, entrypoint, ctx, env)
|
|
276
|
+
const member = target?.[property]
|
|
277
|
+
if (typeof member === 'function') return { kind: 'function' }
|
|
278
|
+
return { kind: 'value', value: member }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const invokeWorkflowControl = async (bindingName: string, op: WorkflowControlOp): Promise<WorkflowControlResult> => {
|
|
282
|
+
const wf = built.workflows.find(w => w.bindingName === bindingName)
|
|
283
|
+
if (!wf) throw new Error(`Workflow binding "${bindingName}" not found`)
|
|
284
|
+
return wf.binding.executeControl(op)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const callFetch = async (request: Request, props?: Record<string, unknown>): Promise<Response> => {
|
|
288
|
+
const ctx = new WorkerExecutionContext(post, props)
|
|
289
|
+
if (typeof defaultExport === 'function' && defaultExport.prototype?.fetch) {
|
|
290
|
+
const Ctor = defaultExport as new(ctx: unknown, env: unknown) => { fetch: (r: Request) => Promise<Response> }
|
|
291
|
+
const instance = new Ctor(ctx, env)
|
|
292
|
+
return instance.fetch(request)
|
|
293
|
+
}
|
|
294
|
+
if (defaultExport && typeof defaultExport.fetch === 'function') {
|
|
295
|
+
return defaultExport.fetch(request, env, ctx) as Promise<Response>
|
|
296
|
+
}
|
|
297
|
+
// Legacy service-worker syntax: `addEventListener('fetch', e => e.respondWith(...))`.
|
|
298
|
+
// The plugin shim captured the handler at module-import time.
|
|
299
|
+
const swFetch = plugin.getServiceWorkerFetchHandler()
|
|
300
|
+
if (swFetch) {
|
|
301
|
+
return dispatchServiceWorkerFetch(swFetch, request, ctx)
|
|
302
|
+
}
|
|
303
|
+
throw new Error('Worker module does not export a fetch handler (and no addEventListener("fetch") handler was registered)')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Resolve a named handler honoring class- vs object-based exports. */
|
|
307
|
+
function resolveHandler(name: WorkerHandlerName, ctx: WorkerExecutionContext): ((...args: unknown[]) => Promise<unknown>) | null {
|
|
308
|
+
if (typeof defaultExport === 'function' && defaultExport.prototype) {
|
|
309
|
+
const fn = defaultExport.prototype[name]
|
|
310
|
+
if (typeof fn !== 'function') return null
|
|
311
|
+
const Ctor = defaultExport as new(ctx: unknown, env: unknown) => Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
312
|
+
const instance = new Ctor(ctx, env)
|
|
313
|
+
return instance[name]!.bind(instance)
|
|
314
|
+
}
|
|
315
|
+
const obj = defaultExport as Record<string, unknown> | null | undefined
|
|
316
|
+
const fn = obj?.[name]
|
|
317
|
+
return typeof fn === 'function' ? (fn as (...a: unknown[]) => Promise<unknown>).bind(obj) : null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const callScheduled = async (cronExpr: string, scheduledTime: number): Promise<{ ok: boolean; noHandler?: boolean }> => {
|
|
321
|
+
const ctx = new WorkerExecutionContext(post)
|
|
322
|
+
const handler = resolveHandler('scheduled', ctx)
|
|
323
|
+
if (!handler) return { ok: false, noHandler: true }
|
|
324
|
+
const controller = createScheduledController(cronExpr, scheduledTime)
|
|
325
|
+
await handler(controller, env, ctx)
|
|
326
|
+
return { ok: true }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const callEmail = async (messageId: string, from: string, to: string, raw: Uint8Array): Promise<{ ok: boolean; noHandler?: boolean }> => {
|
|
330
|
+
const ctx = new WorkerExecutionContext(post)
|
|
331
|
+
const handler = resolveHandler('email', ctx)
|
|
332
|
+
if (!handler) return { ok: false, noHandler: true }
|
|
333
|
+
const message = new ForwardableEmailMessage(getDatabase(), messageId, from, to, raw)
|
|
334
|
+
await handler(message, env, ctx)
|
|
335
|
+
return { ok: true }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// When `noHandler:true` the `error.message` field is a wire-format placeholder —
|
|
339
|
+
// the main-side executor resolves with `ok:false` rather than rejecting, so the
|
|
340
|
+
// message is never surfaced to user code.
|
|
341
|
+
self.onmessage = async (event: MessageEvent<WorkerCommand>) => {
|
|
342
|
+
const cmd = event.data
|
|
343
|
+
if (rpc.handle(cmd as { type: string })) return
|
|
344
|
+
switch (cmd.type) {
|
|
345
|
+
case 'fetch': {
|
|
346
|
+
const abortController = new AbortController()
|
|
347
|
+
fetchAbortControllers.set(cmd.id, abortController)
|
|
348
|
+
try {
|
|
349
|
+
const reqBody = cmd.request.streamId !== undefined ? requestStreams.open(cmd.request.streamId) : undefined
|
|
350
|
+
const request = deserializeRequest(cmd.request, reqBody, abortController.signal)
|
|
351
|
+
const response = await runWithParentContext(cmd.parent, () => callFetch(request, cmd.props))
|
|
352
|
+
const serialized = serializeResponse(response, wsBridge)
|
|
353
|
+
post({ type: 'fetch-result', id: cmd.id, response: serialized })
|
|
354
|
+
if (serialized.streamId !== undefined && response.body) {
|
|
355
|
+
// Keep the AbortController alive until the streamed body finishes
|
|
356
|
+
// (an SSE loop may still observe request.signal); drop it on end.
|
|
357
|
+
pumpResponseBody(serialized.streamId, response.body, () => fetchAbortControllers.delete(cmd.id))
|
|
358
|
+
} else {
|
|
359
|
+
fetchAbortControllers.delete(cmd.id)
|
|
360
|
+
}
|
|
361
|
+
} catch (e) {
|
|
362
|
+
fetchAbortControllers.delete(cmd.id)
|
|
363
|
+
post({ type: 'fetch-error', id: cmd.id, error: serializeError(e) })
|
|
364
|
+
}
|
|
365
|
+
break
|
|
366
|
+
}
|
|
367
|
+
case 'scheduled':
|
|
368
|
+
try {
|
|
369
|
+
const result = await runWithParentContext(cmd.parent, () => callScheduled(cmd.cronExpr, cmd.scheduledTime))
|
|
370
|
+
if (!result.ok) post({ type: 'scheduled-error', id: cmd.id, error: { message: 'no-handler' }, noHandler: true })
|
|
371
|
+
else post({ type: 'scheduled-result', id: cmd.id })
|
|
372
|
+
} catch (e) {
|
|
373
|
+
post({ type: 'scheduled-error', id: cmd.id, error: serializeError(e) })
|
|
374
|
+
}
|
|
375
|
+
break
|
|
376
|
+
case 'email':
|
|
377
|
+
try {
|
|
378
|
+
const result = await runWithParentContext(cmd.parent, () => callEmail(cmd.messageId, cmd.from, cmd.to, cmd.raw))
|
|
379
|
+
if (!result.ok) post({ type: 'email-error', id: cmd.id, error: { message: 'no-handler' }, noHandler: true })
|
|
380
|
+
else post({ type: 'email-result', id: cmd.id })
|
|
381
|
+
} catch (e) {
|
|
382
|
+
post({ type: 'email-error', id: cmd.id, error: serializeError(e) })
|
|
383
|
+
}
|
|
384
|
+
break
|
|
385
|
+
case 'ws-client-message':
|
|
386
|
+
wsBridge.deliverClientMessage(cmd.wsId, cmd.data)
|
|
387
|
+
break
|
|
388
|
+
case 'ws-client-close':
|
|
389
|
+
wsBridge.deliverClientClose(cmd.wsId, cmd.code, cmd.reason, cmd.wasClean)
|
|
390
|
+
break
|
|
391
|
+
case 'env-ws-incoming':
|
|
392
|
+
envWsBridge.deliverClientMessage(cmd.wsId, cmd.data)
|
|
393
|
+
break
|
|
394
|
+
case 'env-ws-close-in':
|
|
395
|
+
envWsBridge.deliverClientClose(cmd.wsId, cmd.code, cmd.reason, cmd.wasClean)
|
|
396
|
+
break
|
|
397
|
+
case 'stream-cancel':
|
|
398
|
+
responseStreams.cancel(cmd.id)
|
|
399
|
+
break
|
|
400
|
+
case 'stream-ack':
|
|
401
|
+
responseStreams.grantCredit(cmd.id)
|
|
402
|
+
break
|
|
403
|
+
case 'req-stream-chunk':
|
|
404
|
+
requestStreams.push(cmd.streamId, cmd.chunk)
|
|
405
|
+
break
|
|
406
|
+
case 'req-stream-end':
|
|
407
|
+
requestStreams.end(cmd.streamId)
|
|
408
|
+
break
|
|
409
|
+
case 'req-stream-error':
|
|
410
|
+
requestStreams.error(cmd.streamId, deserializeError(cmd.error))
|
|
411
|
+
break
|
|
412
|
+
case 'stop-queue-consumers':
|
|
413
|
+
// Reload drain — stop polling for new messages. In-flight batches keep
|
|
414
|
+
// running and are awaited by main via their wait-until registration.
|
|
415
|
+
for (const consumer of queueConsumers) consumer.stop()
|
|
416
|
+
break
|
|
417
|
+
case 'fetch-abort':
|
|
418
|
+
// Client disconnected — fire the request's signal so user cleanup runs.
|
|
419
|
+
fetchAbortControllers.get(cmd.id)?.abort()
|
|
420
|
+
break
|
|
421
|
+
case 'entrypoint-rpc':
|
|
422
|
+
try {
|
|
423
|
+
const value = await runWithParentContext(cmd.parent, () => invokeEntrypointRpc(cmd.entrypoint, cmd.method, cmd.args, cmd.props))
|
|
424
|
+
post({ type: 'entrypoint-rpc-result', id: cmd.id, value })
|
|
425
|
+
} catch (e) {
|
|
426
|
+
post({ type: 'entrypoint-rpc-error', id: cmd.id, error: serializeError(e) })
|
|
427
|
+
}
|
|
428
|
+
break
|
|
429
|
+
case 'entrypoint-rpc-get':
|
|
430
|
+
try {
|
|
431
|
+
const result = runWithParentContext(cmd.parent, () => invokeEntrypointPropertyGet(cmd.entrypoint, cmd.property, cmd.props))
|
|
432
|
+
if (result.kind === 'function') {
|
|
433
|
+
post({ type: 'entrypoint-rpc-get-result', id: cmd.id, kind: 'function' })
|
|
434
|
+
} else {
|
|
435
|
+
post({ type: 'entrypoint-rpc-get-result', id: cmd.id, kind: 'value', value: result.value })
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
post({ type: 'entrypoint-rpc-get-error', id: cmd.id, error: serializeError(e) })
|
|
439
|
+
}
|
|
440
|
+
break
|
|
441
|
+
case 'workflow-control':
|
|
442
|
+
try {
|
|
443
|
+
const result = await runWithParentContext(cmd.parent, () => invokeWorkflowControl(cmd.binding, cmd.op))
|
|
444
|
+
post({ type: 'workflow-control-result', id: cmd.id, result })
|
|
445
|
+
} catch (e) {
|
|
446
|
+
post({ type: 'workflow-control-error', id: cmd.id, error: serializeError(e) })
|
|
447
|
+
}
|
|
448
|
+
break
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
post({ type: 'ready', doAlarmHandlers })
|
|
453
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread implementation of CF's `ExecutionContext`. Tracks
|
|
3
|
+
* `waitUntil()` promises locally (they capture worker-side closures) and
|
|
4
|
+
* notifies main of each add/settle so reload drain can wait for them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logIfRejected } from '../execution-context'
|
|
8
|
+
import type { WorkerMessage } from './protocol'
|
|
9
|
+
|
|
10
|
+
// Worker-thread-global wait-until id sequence. Ids never cross thread
|
|
11
|
+
// boundaries (each generation has its own worker), so a module-level counter
|
|
12
|
+
// is enough to keep main's `_pendingWaitUntil` Set unambiguous across
|
|
13
|
+
// concurrent fetches.
|
|
14
|
+
let nextWaitUntilId = 1
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a background promise with main's reload-drain accounting (same
|
|
18
|
+
* `wait-until-add` / `wait-until-settle` protocol as `ctx.waitUntil`). Used for
|
|
19
|
+
* worker-side work that isn't tied to an `ExecutionContext` — e.g. an in-flight
|
|
20
|
+
* queue batch, so reload drain waits for it instead of terminating mid-batch.
|
|
21
|
+
*/
|
|
22
|
+
export function trackBackgroundWork(post: (msg: WorkerMessage) => void, promise: Promise<unknown>): void {
|
|
23
|
+
const id = nextWaitUntilId++
|
|
24
|
+
post({ type: 'wait-until-add', id })
|
|
25
|
+
logIfRejected(promise).finally(() => {
|
|
26
|
+
post({ type: 'wait-until-settle', id })
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class WorkerExecutionContext {
|
|
31
|
+
/** `ctx.props` — carries the calling worker's service-binding `props` for
|
|
32
|
+
* `entrypoint-rpc` / `fetch` dispatch; `{}` for top-level HTTP. */
|
|
33
|
+
readonly props: Record<string, unknown>
|
|
34
|
+
private _post: (msg: WorkerMessage) => void
|
|
35
|
+
|
|
36
|
+
constructor(post: (msg: WorkerMessage) => void, props?: Record<string, unknown>) {
|
|
37
|
+
this._post = post
|
|
38
|
+
this.props = props ?? {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
waitUntil(promise: Promise<unknown>): void {
|
|
42
|
+
const id = nextWaitUntilId++
|
|
43
|
+
this._post({ type: 'wait-until-add', id })
|
|
44
|
+
logIfRejected(promise).finally(() => {
|
|
45
|
+
this._post({ type: 'wait-until-settle', id })
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
passThroughOnException(): void {
|
|
50
|
+
// No origin in local dev — no-op matches CF semantics when the runtime
|
|
51
|
+
// has nowhere to pass through to.
|
|
52
|
+
}
|
|
53
|
+
}
|