lopata 0.18.4 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/api/dispatch.d.ts +2 -2
- package/dist/types/api/handlers/workflows.d.ts +2 -2
- package/dist/types/bindings/container-cleanup.d.ts +43 -0
- package/dist/types/bindings/container-docker.d.ts +24 -0
- package/dist/types/bindings/container.d.ts +4 -2
- package/dist/types/bindings/do-executor-worker.d.ts +73 -89
- package/dist/types/bindings/do-executor.d.ts +22 -1
- package/dist/types/bindings/do-worker-env.d.ts +16 -7
- package/dist/types/bindings/durable-object.d.ts +66 -4
- package/dist/types/bindings/queue.d.ts +4 -1
- package/dist/types/bindings/rpc-stub.d.ts +23 -1
- package/dist/types/bindings/scheduled.d.ts +13 -3
- package/dist/types/bindings/service-binding.d.ts +13 -4
- package/dist/types/bindings/static-assets.d.ts +1 -1
- package/dist/types/bindings/websocket-pair.d.ts +12 -3
- package/dist/types/bindings/workflow.d.ts +29 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/env.d.ts +6 -0
- package/dist/types/error-page-render.d.ts +6 -0
- package/dist/types/execution-context.d.ts +8 -0
- package/dist/types/generation-manager.d.ts +21 -2
- package/dist/types/generation.d.ts +16 -21
- package/dist/types/import-graph.d.ts +28 -0
- package/dist/types/lopata-config.d.ts +3 -3
- package/dist/types/plugin.d.ts +4 -1
- package/dist/types/rpc-validate.d.ts +9 -0
- package/dist/types/setup-globals.d.ts +5 -1
- package/dist/types/tracing/context.d.ts +12 -0
- package/dist/types/tracing/span.d.ts +21 -3
- package/dist/types/tracing/store.d.ts +17 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/vite-plugin/index.d.ts +8 -1
- package/dist/types/worker-registry.d.ts +11 -4
- package/dist/types/worker-thread/do-protocol.d.ts +256 -0
- package/dist/types/worker-thread/entry.d.ts +2 -0
- package/dist/types/worker-thread/execution-context.d.ts +22 -0
- package/dist/types/worker-thread/executor.d.ts +123 -0
- package/dist/types/worker-thread/protocol.d.ts +553 -0
- package/dist/types/worker-thread/remote-trace-store.d.ts +27 -0
- package/dist/types/worker-thread/rpc-client.d.ts +4 -0
- package/dist/types/worker-thread/rpc-shared.d.ts +138 -0
- package/dist/types/worker-thread/serialize.d.ts +12 -0
- package/dist/types/worker-thread/stream-shared.d.ts +147 -0
- package/dist/types/worker-thread/thread-env.d.ts +42 -0
- package/dist/types/worker-thread/wire-handlers.d.ts +16 -0
- package/dist/types/worker-thread/ws-bridge-shared.d.ts +163 -0
- package/package.json +1 -1
- package/src/api/handlers/containers.ts +2 -1
- package/src/api/handlers/workflows.ts +39 -34
- package/src/bindings/container-cleanup.ts +125 -0
- package/src/bindings/container-docker.ts +49 -34
- package/src/bindings/container.ts +24 -9
- package/src/bindings/do-executor-inprocess.ts +9 -5
- package/src/bindings/do-executor-worker.ts +386 -158
- package/src/bindings/do-executor.ts +23 -1
- package/src/bindings/do-worker-entry.ts +242 -60
- package/src/bindings/do-worker-env.ts +296 -11
- package/src/bindings/durable-object.ts +231 -27
- package/src/bindings/email.ts +6 -0
- package/src/bindings/queue.ts +11 -1
- package/src/bindings/rpc-stub.ts +79 -10
- package/src/bindings/scheduled.ts +37 -30
- package/src/bindings/service-binding.ts +87 -33
- package/src/bindings/static-assets.ts +3 -2
- package/src/bindings/websocket-pair.ts +19 -3
- package/src/bindings/workflow.ts +91 -0
- package/src/cli/dev.ts +106 -41
- package/src/config.ts +6 -3
- package/src/db.ts +1 -0
- package/src/env.ts +40 -21
- package/src/error-page-render.ts +21 -0
- package/src/execution-context.ts +13 -3
- package/src/generation-manager.ts +144 -143
- package/src/generation.ts +150 -306
- package/src/import-graph.ts +140 -0
- package/src/lopata-config.ts +3 -3
- package/src/plugin.ts +7 -0
- package/src/rpc-validate.ts +29 -0
- package/src/setup-globals.ts +6 -17
- package/src/testing/durable-object.ts +5 -3
- package/src/testing/index.ts +8 -4
- package/src/tracing/context.ts +18 -0
- package/src/tracing/span.ts +83 -57
- package/src/tracing/store.ts +41 -3
- package/src/virtual-modules.ts +2 -0
- package/src/vite-plugin/dev-server-plugin.ts +4 -0
- package/src/vite-plugin/index.ts +8 -1
- package/src/worker-registry.ts +15 -2
- package/src/worker-thread/do-protocol.ts +237 -0
- package/src/worker-thread/entry.ts +453 -0
- package/src/worker-thread/execution-context.ts +53 -0
- package/src/worker-thread/executor.ts +595 -0
- package/src/worker-thread/protocol.ts +552 -0
- package/src/worker-thread/remote-trace-store.ts +90 -0
- package/src/worker-thread/rpc-client.ts +5 -0
- package/src/worker-thread/rpc-shared.ts +503 -0
- package/src/worker-thread/serialize.ts +37 -0
- package/src/worker-thread/stream-shared.ts +414 -0
- package/src/worker-thread/thread-env.ts +350 -0
- package/src/worker-thread/wire-handlers.ts +80 -0
- package/src/worker-thread/ws-bridge-shared.ts +482 -0
- package/dist/types/bindings/do-websocket-bridge.d.ts +0 -60
- package/dist/types/module-cache.d.ts +0 -23
- package/src/bindings/do-websocket-bridge.ts +0 -79
- package/src/module-cache.ts +0 -58
- package/src/tracing/global.d.ts +0 -50
|
@@ -6,42 +6,47 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { dirname, resolve } from 'node:path'
|
|
9
|
+
import type { WranglerConfig } from '../config'
|
|
10
|
+
import type {
|
|
11
|
+
DOCommand,
|
|
12
|
+
DOMainMessage,
|
|
13
|
+
DoReqStreamChunk,
|
|
14
|
+
DoReqStreamEnd,
|
|
15
|
+
DoReqStreamError,
|
|
16
|
+
DOResult,
|
|
17
|
+
DOWorkerMessage,
|
|
18
|
+
} from '../worker-thread/do-protocol'
|
|
19
|
+
import type { BindingTarget, RpcReply } from '../worker-thread/protocol'
|
|
20
|
+
import { deserializeError } from '../worker-thread/protocol'
|
|
21
|
+
import { RpcHostChannel } from '../worker-thread/rpc-shared'
|
|
22
|
+
import { OutboundStreamRegistry, pumpStream, STREAM_BACKPRESSURE_WINDOW, StreamReceiver } from '../worker-thread/stream-shared'
|
|
23
|
+
import { WsHostBridge } from '../worker-thread/ws-bridge-shared'
|
|
24
|
+
import { registerContainer, unregisterContainer } from './container-cleanup'
|
|
9
25
|
import type { DOExecutor, DOExecutorFactory, ExecutorConfig } from './do-executor'
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| { type: 'ws-message'; wsId: string; data: string | ArrayBuffer }
|
|
35
|
-
| { type: 'ws-close'; wsId: string; code: number; reason: string; wasClean: boolean }
|
|
36
|
-
| { type: 'ws-error'; wsId: string }
|
|
37
|
-
|
|
38
|
-
/** Messages from worker → main thread */
|
|
39
|
-
export type DOMainMessage =
|
|
40
|
-
| { type: 'need-init' }
|
|
41
|
-
| { type: 'ready' }
|
|
42
|
-
| { type: 'result'; id: number; result: DOResult }
|
|
43
|
-
| { type: 'alarm-set'; time: number | null }
|
|
44
|
-
| { type: 'ws-bridge'; payload: WsBridgeOutbound }
|
|
26
|
+
import { DurableObjectIdImpl } from './durable-object'
|
|
27
|
+
import { CFWebSocket, type ResponseWithWebSocket } from './websocket-pair'
|
|
28
|
+
|
|
29
|
+
// The DO-channel message protocol (DOCommand / DOResult / DOWorkerMessage /
|
|
30
|
+
// DOMainMessage / Do*Stream* / WsAcceptSignal) lives in
|
|
31
|
+
// `worker-thread/do-protocol.ts`, beside the user-worker channel's `protocol.ts`.
|
|
32
|
+
// Re-exported here so existing `import … from './do-executor-worker'` sites keep
|
|
33
|
+
// working.
|
|
34
|
+
export type {
|
|
35
|
+
DOCommand,
|
|
36
|
+
DOMainMessage,
|
|
37
|
+
DoReqStreamAck,
|
|
38
|
+
DoReqStreamCancel,
|
|
39
|
+
DoReqStreamChunk,
|
|
40
|
+
DoReqStreamEnd,
|
|
41
|
+
DoReqStreamError,
|
|
42
|
+
DOResult,
|
|
43
|
+
DoStreamAck,
|
|
44
|
+
DoStreamCancel,
|
|
45
|
+
DoStreamChunk,
|
|
46
|
+
DoStreamEnd,
|
|
47
|
+
DoStreamError,
|
|
48
|
+
DOWorkerMessage,
|
|
49
|
+
} from '../worker-thread/do-protocol'
|
|
45
50
|
|
|
46
51
|
// --- Pending command tracking ---
|
|
47
52
|
|
|
@@ -59,13 +64,75 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
59
64
|
private _worker: Worker | null = null
|
|
60
65
|
private _ready: Promise<void> | null = null
|
|
61
66
|
private _readyResolve: (() => void) | null = null
|
|
67
|
+
private _readyReject: ((err: Error) => void) | null = null
|
|
62
68
|
private _pending = new Map<number, PendingCommand>()
|
|
63
69
|
private _nextId = 1
|
|
64
70
|
private _disposed = false
|
|
71
|
+
/** Swallowed copy of `_priorDisposal`, created lazily on the first command.
|
|
72
|
+
* Shared so CONCURRENT first commands all await the prior container's
|
|
73
|
+
* teardown — a boolean gate would let the second racer skip straight to
|
|
74
|
+
* `docker run` while the old container's `docker rm` is still in flight. */
|
|
75
|
+
private _priorDisposalGate: Promise<void> | null = null
|
|
65
76
|
private _inFlightCount = 0
|
|
77
|
+
/** Mirrors of the DO worker's `state` lifecycle, fed by `do-state` signals. */
|
|
66
78
|
private _blocked = false
|
|
67
|
-
private
|
|
68
|
-
|
|
79
|
+
private _aborted = false
|
|
80
|
+
/**
|
|
81
|
+
* `wsId`s of every open WebSocket this DO's fetch handler returned — both
|
|
82
|
+
* hibernation (`state.acceptWebSocket`) and plain (`new WebSocketPair`) ones.
|
|
83
|
+
* Each pins the executor against the idle reaper until it closes. Added when
|
|
84
|
+
* `executeFetch` registers the bridge socket; removed on the `fetch-ws-close-out`
|
|
85
|
+
* dispatch (server-side close) and the `_fetchBridge` clientClose envelope
|
|
86
|
+
* (client disconnect) — both delete from the same set so neither double-counts.
|
|
87
|
+
*/
|
|
88
|
+
private _openFetchWsIds = new Set<string>()
|
|
89
|
+
/**
|
|
90
|
+
* Main-side WS bridge for `Response{webSocket}` returned by the DO worker's
|
|
91
|
+
* fetch handler. The bridge's peer forwards outgoing bytes (Bun.serve → here)
|
|
92
|
+
* down to the DO worker as `fetch-ws-incoming` / `fetch-ws-close-in`.
|
|
93
|
+
*/
|
|
94
|
+
private _fetchBridge: WsHostBridge<DOWorkerMessage> | null = null
|
|
95
|
+
/**
|
|
96
|
+
* Main-side WS bridge for upstream CFWebSockets adopted from env-binding
|
|
97
|
+
* fetches the DO worker initiated (e.g. `this.env.SVC.fetch('/ws')` returning
|
|
98
|
+
* a 101). Direction is opposite of `_fetchBridge`: we already hold the real
|
|
99
|
+
* CFWebSocket from main's `resolveBinding(...).fetch()`, the DO worker side
|
|
100
|
+
* holds the synthetic user-facing peer.
|
|
101
|
+
*/
|
|
102
|
+
private _envBindingWsBridge: WsHostBridge<DOWorkerMessage> | null = null
|
|
103
|
+
/** Main-side host of the unified cross-thread RPC channel — env-binding
|
|
104
|
+
* call/get/fetch from inside the DO worker (`this.env.SVC.…`), with
|
|
105
|
+
* response/request-body streaming. Shared with the user-worker executor;
|
|
106
|
+
* channel-specifics injected as hooks. */
|
|
107
|
+
private _rpcChannel = new RpcHostChannel({
|
|
108
|
+
resolveBinding: (target) => this._resolveBinding(target),
|
|
109
|
+
post: (reply) => {
|
|
110
|
+
this._worker?.postMessage(reply satisfies RpcReply)
|
|
111
|
+
},
|
|
112
|
+
isAlive: () => !this._disposed && this._worker !== null,
|
|
113
|
+
decorateResponse: (response, serialized) => {
|
|
114
|
+
const ws = (response as ResponseWithWebSocket).webSocket
|
|
115
|
+
if (response.status === 101 && ws instanceof CFWebSocket && this._envBindingWsBridge) {
|
|
116
|
+
serialized.webSocketId = this._envBindingWsBridge.adoptExisting(ws, { bridgeEvents: true })
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
/** Reconstruction of streamed DO-fetch response bodies (DO worker → main). */
|
|
121
|
+
private _fetchStreams = new StreamReceiver(
|
|
122
|
+
(streamId) => {
|
|
123
|
+
this._worker?.postMessage({ type: 'do-stream-cancel', streamId } satisfies DOWorkerMessage)
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
window: STREAM_BACKPRESSURE_WINDOW,
|
|
127
|
+
onCredit: (streamId) => {
|
|
128
|
+
this._worker?.postMessage({ type: 'do-stream-ack', streamId } satisfies DOWorkerMessage)
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
/** Outbound request-body pumps for DO-fetch (main → DO worker). A
|
|
133
|
+
* `do-req-stream-cancel` from the DO worker (instance code cancelled the
|
|
134
|
+
* body) stops the source reader. */
|
|
135
|
+
private _fetchRequestStreams = new OutboundStreamRegistry()
|
|
69
136
|
|
|
70
137
|
constructor(config: ExecutorConfig) {
|
|
71
138
|
this._config = config
|
|
@@ -78,12 +145,37 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
78
145
|
const config = this._config
|
|
79
146
|
const worker = new Worker(WORKER_ENTRY_PATH)
|
|
80
147
|
|
|
81
|
-
this._ready = new Promise<void>((resolve) => {
|
|
148
|
+
this._ready = new Promise<void>((resolve, reject) => {
|
|
82
149
|
this._readyResolve = resolve
|
|
150
|
+
this._readyReject = reject
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
this._fetchBridge = new WsHostBridge<DOWorkerMessage>(msg => worker.postMessage(msg), {
|
|
154
|
+
clientMessage: (wsId, data) => ({ type: 'fetch-ws-incoming', wsId, data }),
|
|
155
|
+
clientClose: (wsId, code, reason, wasClean) => {
|
|
156
|
+
// A real client disconnect ends the WS regardless of whether user code
|
|
157
|
+
// calls `ws.close()` in its `webSocketClose` handler (that call is
|
|
158
|
+
// optional in CF). Drop the pin here so it doesn't leak and keep the DO
|
|
159
|
+
// worker alive past eviction. The matching `fetch-ws-close-out` (when
|
|
160
|
+
// the server peer also closes) then finds the id already gone and is a
|
|
161
|
+
// no-op, so this never double-counts.
|
|
162
|
+
this._openFetchWsIds.delete(wsId)
|
|
163
|
+
return { type: 'fetch-ws-close-in', wsId, code, reason, wasClean }
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
this._envBindingWsBridge = new WsHostBridge<DOWorkerMessage>(msg => worker.postMessage(msg), {
|
|
167
|
+
clientMessage: (wsId, data) => ({ type: 'env-ws-incoming', wsId, data }),
|
|
168
|
+
clientClose: (wsId, code, reason, wasClean) => ({ type: 'env-ws-close-in', wsId, code, reason, wasClean }),
|
|
83
169
|
})
|
|
84
170
|
|
|
85
171
|
worker.onmessage = (event: MessageEvent<DOMainMessage>) => {
|
|
172
|
+
// Drop late messages after dispose/onerror — the shared RPC dispatchers
|
|
173
|
+
// would otherwise commit side effects (KV write, R2 put, queue send)
|
|
174
|
+
// from a dying generation before their reply gets filtered by
|
|
175
|
+
// `hooks.isAlive()`. Mirrors `WorkerThreadExecutor._handleMessage`.
|
|
176
|
+
if (this._disposed) return
|
|
86
177
|
const msg = event.data
|
|
178
|
+
if (this._rpcChannel.handle(msg)) return
|
|
87
179
|
|
|
88
180
|
switch (msg.type) {
|
|
89
181
|
case 'need-init':
|
|
@@ -93,6 +185,9 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
93
185
|
config: {
|
|
94
186
|
modulePath: this._resolveModulePath(),
|
|
95
187
|
configPath: this._resolveConfigPath(),
|
|
188
|
+
// Main's parsed, env-overridden config — the DO worker uses this
|
|
189
|
+
// instead of re-loading from configPath WITHOUT the --env overrides.
|
|
190
|
+
wranglerConfig: this._config._wranglerConfig,
|
|
96
191
|
dataDir: this._resolveDataDir(),
|
|
97
192
|
namespaceName: config.namespaceName,
|
|
98
193
|
idStr: config.id.toString(),
|
|
@@ -107,16 +202,12 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
107
202
|
|
|
108
203
|
case 'result': {
|
|
109
204
|
if (msg.id === -1 && msg.result.type === 'error') {
|
|
110
|
-
// Worker init error —
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this._pending.clear()
|
|
117
|
-
// Resolve ready so _sendCommand doesn't hang forever
|
|
118
|
-
this._readyResolve?.()
|
|
119
|
-
this._disposed = true
|
|
205
|
+
// Worker init error — tear down with the real cause so awaiting
|
|
206
|
+
// callers surface it instead of a generic "Worker terminated"
|
|
207
|
+
// after `_disposed` flips. The failed thread would otherwise
|
|
208
|
+
// linger (SQLite handle, container/health timers) until the 30s
|
|
209
|
+
// idle reaper or next access disposes it.
|
|
210
|
+
this._teardown(deserializeError(msg.result.error))
|
|
120
211
|
break
|
|
121
212
|
}
|
|
122
213
|
const pending = this._pending.get(msg.id)
|
|
@@ -132,19 +223,71 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
132
223
|
config.onAlarmSet?.(msg.time)
|
|
133
224
|
break
|
|
134
225
|
|
|
135
|
-
case '
|
|
136
|
-
|
|
226
|
+
case 'do-state':
|
|
227
|
+
// Mirror the DO instance's abort/block lifecycle so the idle reaper
|
|
228
|
+
// evicts an aborted instance (→ recreated fresh) and never evicts
|
|
229
|
+
// mid-blockConcurrencyWhile.
|
|
230
|
+
this._aborted = msg.aborted
|
|
231
|
+
this._blocked = msg.blocked
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
case 'fetch-ws-outgoing':
|
|
235
|
+
this._fetchBridge?.deliverRemoteMessage(msg.wsId, msg.data)
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
case 'fetch-ws-close-out':
|
|
239
|
+
this._fetchBridge?.deliverRemoteClose(msg.wsId, msg.code, msg.reason, msg.wasClean)
|
|
240
|
+
this._openFetchWsIds.delete(msg.wsId)
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
case 'env-ws-outgoing':
|
|
244
|
+
this._envBindingWsBridge?.deliverRemoteMessage(msg.wsId, msg.data)
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
case 'env-ws-close-out':
|
|
248
|
+
this._envBindingWsBridge?.deliverRemoteClose(msg.wsId, msg.code, msg.reason, msg.wasClean)
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
case 'do-stream-chunk':
|
|
252
|
+
this._fetchStreams.push(msg.streamId, msg.chunk)
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
case 'do-stream-end':
|
|
256
|
+
this._fetchStreams.end(msg.streamId)
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
case 'do-stream-error':
|
|
260
|
+
this._fetchStreams.error(msg.streamId, deserializeError(msg.error))
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
case 'do-req-stream-cancel':
|
|
264
|
+
this._fetchRequestStreams.cancel(msg.streamId)
|
|
265
|
+
break
|
|
266
|
+
case 'do-req-stream-ack':
|
|
267
|
+
this._fetchRequestStreams.grantCredit(msg.streamId)
|
|
268
|
+
break
|
|
269
|
+
|
|
270
|
+
case 'container-registered':
|
|
271
|
+
registerContainer(msg.name)
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
case 'container-removed':
|
|
275
|
+
unregisterContainer(msg.name)
|
|
137
276
|
break
|
|
138
277
|
}
|
|
139
278
|
}
|
|
140
279
|
|
|
141
|
-
worker.onerror = (event) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
280
|
+
worker.onerror = (event: ErrorEvent) => {
|
|
281
|
+
if (this._disposed) return
|
|
282
|
+
// Mark dead so the namespace drops this executor and recreates a fresh
|
|
283
|
+
// one on next access (see `isDisposed()` + `_getOrCreateExecutor`),
|
|
284
|
+
// instead of posting to a terminated Worker (which would hang). An
|
|
285
|
+
// uncaught worker error does NOT end the thread (Web Worker semantics),
|
|
286
|
+
// so `_teardown` terminating it here is what prevents the leak (SQLite
|
|
287
|
+
// handle, container/health timers, WS bridges). Mirrors
|
|
288
|
+
// WorkerThreadExecutor's onerror → _failAll.
|
|
289
|
+
const detail = event.error?.stack ?? event.message ?? 'unknown'
|
|
290
|
+
this._teardown(new Error(`Worker error: ${detail}`))
|
|
148
291
|
}
|
|
149
292
|
|
|
150
293
|
this._worker = worker
|
|
@@ -152,19 +295,27 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
152
295
|
}
|
|
153
296
|
|
|
154
297
|
private _resolveModulePath(): string {
|
|
155
|
-
return
|
|
298
|
+
return this._config._modulePath ?? ''
|
|
156
299
|
}
|
|
157
300
|
|
|
158
301
|
private _resolveConfigPath(): string {
|
|
159
|
-
return
|
|
302
|
+
return this._config._configPath ?? ''
|
|
160
303
|
}
|
|
161
304
|
|
|
162
305
|
private _resolveDataDir(): string {
|
|
163
306
|
return this._config.dataDir ?? ''
|
|
164
307
|
}
|
|
165
308
|
|
|
166
|
-
private async _sendCommand(command: DOCommand): Promise<DOResult> {
|
|
309
|
+
private async _sendCommand(command: DOCommand, afterPost?: () => void): Promise<DOResult> {
|
|
167
310
|
const worker = this._ensureWorker()
|
|
311
|
+
// Wait for the prior executor's container teardown (docker rm) before any
|
|
312
|
+
// command — a container DO's first fetch triggers `docker run` for the
|
|
313
|
+
// same name, which would otherwise race the teardown. Every command awaits
|
|
314
|
+
// the shared gate (a settled promise after the first), so concurrent first
|
|
315
|
+
// commands serialize too. No-op for non-container DOs (`_priorDisposal`
|
|
316
|
+
// undefined).
|
|
317
|
+
this._priorDisposalGate ??= this._config._priorDisposal?.catch(() => {}) ?? Promise.resolve()
|
|
318
|
+
await this._priorDisposalGate
|
|
168
319
|
await this._ready
|
|
169
320
|
|
|
170
321
|
if (this._disposed) throw new Error('Worker terminated')
|
|
@@ -177,10 +328,7 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
177
328
|
resolve: (result) => {
|
|
178
329
|
this._inFlightCount--
|
|
179
330
|
if (result.type === 'error') {
|
|
180
|
-
|
|
181
|
-
err.name = result.name ?? 'Error'
|
|
182
|
-
if (result.stack) err.stack = result.stack
|
|
183
|
-
reject(err)
|
|
331
|
+
reject(deserializeError(result.error))
|
|
184
332
|
} else {
|
|
185
333
|
resolve(result)
|
|
186
334
|
}
|
|
@@ -190,98 +338,126 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
190
338
|
reject(err)
|
|
191
339
|
},
|
|
192
340
|
})
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
ws.close(payload.code, payload.reason)
|
|
210
|
-
this._bridgedWebSockets.delete(payload.wsId)
|
|
211
|
-
this._wsCount--
|
|
212
|
-
}
|
|
213
|
-
break
|
|
341
|
+
try {
|
|
342
|
+
worker.postMessage({ type: 'command', id, command } satisfies DOWorkerMessage)
|
|
343
|
+
// Runs only AFTER the command is posted, which is after `await this._ready`
|
|
344
|
+
// — so the DO worker's full message handler is installed (it's set right
|
|
345
|
+
// before the worker posts `ready`). Starting the request-body pump here
|
|
346
|
+
// (not synchronously in executeFetch) means its chunks can't race ahead
|
|
347
|
+
// of `ready` and get dropped by the init-phase handler.
|
|
348
|
+
afterPost?.()
|
|
349
|
+
} catch (e) {
|
|
350
|
+
// A synchronous postMessage throw (DataCloneError on non-cloneable
|
|
351
|
+
// args) would otherwise leak the pending entry and the `_inFlightCount`
|
|
352
|
+
// bump, leaving isActive() true forever and exempting the executor from
|
|
353
|
+
// idle eviction (thread + DB connection pinned until reload).
|
|
354
|
+
this._pending.delete(id)
|
|
355
|
+
this._inFlightCount--
|
|
356
|
+
reject(e instanceof Error ? e : new Error(String(e)))
|
|
214
357
|
}
|
|
215
|
-
case 'ws-accept': {
|
|
216
|
-
// WebSocket was accepted by the DO — increment count
|
|
217
|
-
this._wsCount++
|
|
218
|
-
break
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Register a real WebSocket for bridging to the worker */
|
|
224
|
-
_bridgeWebSocket(wsId: string, ws: WebSocket): void {
|
|
225
|
-
this._bridgedWebSockets.set(wsId, ws)
|
|
226
|
-
|
|
227
|
-
// Forward events from real WS to worker
|
|
228
|
-
ws.addEventListener('message', (event: MessageEvent) => {
|
|
229
|
-
this._worker?.postMessage(
|
|
230
|
-
{
|
|
231
|
-
type: 'ws-message',
|
|
232
|
-
wsId,
|
|
233
|
-
data: event.data,
|
|
234
|
-
} satisfies DOWorkerMessage,
|
|
235
|
-
)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
ws.addEventListener('close', (event: CloseEvent) => {
|
|
239
|
-
this._worker?.postMessage(
|
|
240
|
-
{
|
|
241
|
-
type: 'ws-close',
|
|
242
|
-
wsId,
|
|
243
|
-
code: event.code,
|
|
244
|
-
reason: event.reason,
|
|
245
|
-
wasClean: event.wasClean,
|
|
246
|
-
} satisfies DOWorkerMessage,
|
|
247
|
-
)
|
|
248
|
-
this._bridgedWebSockets.delete(wsId)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
ws.addEventListener('error', () => {
|
|
252
|
-
this._worker?.postMessage(
|
|
253
|
-
{
|
|
254
|
-
type: 'ws-error',
|
|
255
|
-
wsId,
|
|
256
|
-
} satisfies DOWorkerMessage,
|
|
257
|
-
)
|
|
258
358
|
})
|
|
259
359
|
}
|
|
260
360
|
|
|
261
361
|
// --- DOExecutor interface ---
|
|
262
362
|
|
|
263
363
|
async executeFetch(request: Request): Promise<Response> {
|
|
264
|
-
// Serialize Request
|
|
265
364
|
const headers: [string, string][] = []
|
|
266
365
|
request.headers.forEach((v, k) => headers.push([k, v]))
|
|
267
|
-
const body = request.body
|
|
366
|
+
const body = request.body
|
|
367
|
+
const streamId = body ? this._fetchRequestStreams.allocateId() : undefined
|
|
368
|
+
|
|
369
|
+
// The request-body pump starts in `afterPost` — i.e. AFTER `_sendCommand`
|
|
370
|
+
// has awaited `_ready` and posted the 'fetch' command. On a COLD worker the
|
|
371
|
+
// init-phase handler drops every message except `init`, so starting the pump
|
|
372
|
+
// synchronously here (as before) dropped the head chunks — and a dropped
|
|
373
|
+
// `end` left `await request.text()` hanging forever, pinning the executor.
|
|
374
|
+
let pumpStarted = false
|
|
375
|
+
const resultPromise = this._sendCommand(
|
|
376
|
+
{
|
|
377
|
+
type: 'fetch',
|
|
378
|
+
url: request.url,
|
|
379
|
+
method: request.method,
|
|
380
|
+
headers,
|
|
381
|
+
body: null,
|
|
382
|
+
streamId,
|
|
383
|
+
},
|
|
384
|
+
body && streamId !== undefined
|
|
385
|
+
? () => {
|
|
386
|
+
pumpStarted = true
|
|
387
|
+
this._pumpFetchRequestBody(streamId, body)
|
|
388
|
+
}
|
|
389
|
+
: undefined,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if (body && streamId !== undefined) {
|
|
393
|
+
// If `resultPromise` rejects (worker disposed before `_ready`, or errored
|
|
394
|
+
// mid-flight), release the source body: cancel via the registry if the
|
|
395
|
+
// pump started (the reader is locked there), else cancel the body itself
|
|
396
|
+
// so it isn't left dangling unread.
|
|
397
|
+
resultPromise.catch(() => {
|
|
398
|
+
if (pumpStarted) this._fetchRequestStreams.cancel(streamId)
|
|
399
|
+
else body.cancel().catch(() => {})
|
|
400
|
+
})
|
|
401
|
+
}
|
|
268
402
|
|
|
269
|
-
const result = await
|
|
270
|
-
type: 'fetch',
|
|
271
|
-
url: request.url,
|
|
272
|
-
method: request.method,
|
|
273
|
-
headers,
|
|
274
|
-
body,
|
|
275
|
-
})
|
|
403
|
+
const result = await resultPromise
|
|
276
404
|
|
|
277
405
|
if (result.type !== 'fetch') throw new Error('Unexpected result type')
|
|
278
406
|
|
|
279
|
-
|
|
280
|
-
return new Response(result.body, {
|
|
407
|
+
const init: ResponseInit & { webSocket?: CFWebSocket } = {
|
|
281
408
|
status: result.status,
|
|
282
409
|
statusText: result.statusText,
|
|
283
410
|
headers: result.headers,
|
|
284
|
-
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (result.fetchWebSocketId) {
|
|
414
|
+
if (!this._fetchBridge) throw new Error('Fetch WS bridge not initialised — worker must be alive')
|
|
415
|
+
init.webSocket = this._fetchBridge.register(result.fetchWebSocketId)
|
|
416
|
+
// Pin the executor for as long as the WS is open — hibernation AND plain
|
|
417
|
+
// sockets alike (a chatty non-hibernation WS bypasses _sendCommand, so
|
|
418
|
+
// without this the idle reaper would evict it and 1012 the client).
|
|
419
|
+
this._openFetchWsIds.add(result.fetchWebSocketId)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (result.streamId !== undefined) {
|
|
423
|
+
const stream = this._fetchStreams.open(result.streamId)
|
|
424
|
+
return new Response(stream, init)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return new Response(result.body, init)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private _pumpFetchRequestBody(streamId: number, body: ReadableStream<Uint8Array>): void {
|
|
431
|
+
pumpStream<DoReqStreamChunk, DoReqStreamEnd, DoReqStreamError>(
|
|
432
|
+
streamId,
|
|
433
|
+
body,
|
|
434
|
+
this._fetchRequestStreams,
|
|
435
|
+
msg => this._worker?.postMessage(msg satisfies DOWorkerMessage),
|
|
436
|
+
{
|
|
437
|
+
chunk: (id, chunk) => ({ type: 'do-req-stream-chunk', streamId: id, chunk }),
|
|
438
|
+
end: (id) => ({ type: 'do-req-stream-end', streamId: id }),
|
|
439
|
+
error: (id, error) => ({ type: 'do-req-stream-error', streamId: id, error }),
|
|
440
|
+
},
|
|
441
|
+
() => !this._disposed,
|
|
442
|
+
STREAM_BACKPRESSURE_WINDOW,
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Resolve a binding from main's env. Mirrors the user-worker channel:
|
|
448
|
+
* when `target.instanceId` is set, route through the namespace's `.get()`
|
|
449
|
+
* so DO-stub redirection works (cross-DO and self-DO access via env-RPC).
|
|
450
|
+
*/
|
|
451
|
+
private _resolveBinding = (target: BindingTarget): Record<string, unknown> => {
|
|
452
|
+
const binding = this._config.env[target.binding] as Record<string, unknown> | undefined
|
|
453
|
+
if (!binding) throw new Error(`Binding "${target.binding}" not found on main env`)
|
|
454
|
+
if (target.instanceId === undefined) return binding
|
|
455
|
+
const get = binding.get
|
|
456
|
+
if (typeof get !== 'function') {
|
|
457
|
+
throw new Error(`Binding "${target.binding}" cannot resolve instance "${target.instanceId}" — no .get() method`)
|
|
458
|
+
}
|
|
459
|
+
const doId = new DurableObjectIdImpl(target.instanceId, target.instanceName)
|
|
460
|
+
return (get as (id: DurableObjectIdImpl) => Record<string, unknown>).call(binding, doId)
|
|
285
461
|
}
|
|
286
462
|
|
|
287
463
|
async executeRpc(method: string, args: unknown[]): Promise<unknown> {
|
|
@@ -303,7 +479,7 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
303
479
|
|
|
304
480
|
if (result.type !== 'rpc-get') throw new Error('Unexpected result type')
|
|
305
481
|
// Functions can't cross the boundary — return a callable stub
|
|
306
|
-
if (result.
|
|
482
|
+
if (result.kind === 'function') {
|
|
307
483
|
return (...args: unknown[]) => this.executeRpc(prop, args)
|
|
308
484
|
}
|
|
309
485
|
return result.value
|
|
@@ -315,14 +491,17 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
315
491
|
retryCount,
|
|
316
492
|
})
|
|
317
493
|
if (result.type === 'error') {
|
|
318
|
-
|
|
319
|
-
if (result.stack) err.stack = result.stack
|
|
320
|
-
throw err
|
|
494
|
+
throw deserializeError(result.error)
|
|
321
495
|
}
|
|
322
496
|
}
|
|
323
497
|
|
|
324
498
|
isActive(): boolean {
|
|
325
|
-
|
|
499
|
+
// Open body streams keep the instance active: the fetch promise settles
|
|
500
|
+
// at response HEADERS, so a long-lived body (SSE, slow download) would
|
|
501
|
+
// otherwise look idle and get evicted mid-stream — dispose() error()s
|
|
502
|
+
// the body the caller is still reading. Mirrors the top-level
|
|
503
|
+
// generation drain's openStreamCount() guard.
|
|
504
|
+
return this._inFlightCount > 0 || this._fetchStreams.activeCount() > 0 || this._fetchRequestStreams.activeCount() > 0
|
|
326
505
|
}
|
|
327
506
|
|
|
328
507
|
isBlocked(): boolean {
|
|
@@ -330,26 +509,70 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
330
509
|
}
|
|
331
510
|
|
|
332
511
|
activeWebSocketCount(): number {
|
|
333
|
-
return this.
|
|
512
|
+
return this._openFetchWsIds.size
|
|
334
513
|
}
|
|
335
514
|
|
|
336
515
|
isAborted(): boolean {
|
|
337
|
-
return
|
|
516
|
+
return this._aborted
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
isDisposed(): boolean {
|
|
520
|
+
return this._disposed
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Ask the DO worker to tear down its Docker container before we terminate the
|
|
525
|
+
* thread. terminate() kills the worker's activity/health timers but leaves the
|
|
526
|
+
* Docker process running — mirrors `InProcessExecutor.dispose()`'s
|
|
527
|
+
* `containerRuntime.cleanup()`. Bounded so a hung `docker rm` can't block
|
|
528
|
+
* reload; the thread is terminated right after regardless.
|
|
529
|
+
*/
|
|
530
|
+
private _cleanupContainer(): Promise<void> {
|
|
531
|
+
let timer: ReturnType<typeof setTimeout>
|
|
532
|
+
const timeout = new Promise<void>((res) => {
|
|
533
|
+
timer = setTimeout(res, 3000)
|
|
534
|
+
})
|
|
535
|
+
return Promise.race([
|
|
536
|
+
this._sendCommand({ type: 'cleanup' }).then(() => {}, () => {}),
|
|
537
|
+
timeout,
|
|
538
|
+
]).finally(() => clearTimeout(timer))
|
|
338
539
|
}
|
|
339
540
|
|
|
340
541
|
async dispose(): Promise<void> {
|
|
542
|
+
// Container DOs: stop the Docker container before terminating the thread.
|
|
543
|
+
// Non-container DOs skip the round-trip so reload stays fast.
|
|
544
|
+
if (!this._disposed && this._worker && this._config.containerConfig) {
|
|
545
|
+
try {
|
|
546
|
+
await this._cleanupContainer()
|
|
547
|
+
} catch {}
|
|
548
|
+
}
|
|
549
|
+
this._teardown(new Error('Worker terminated'))
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* The one teardown sequence — shared by `dispose()` (planned), `onerror`
|
|
554
|
+
* (worker crashed) and the init-error path, so the three can't drift: mark
|
|
555
|
+
* disposed, terminate the thread, reject every pending command and the ready
|
|
556
|
+
* promise with `error`, and dispose every bridge/stream/rpc registry.
|
|
557
|
+
* Idempotent. Counterpart of `WorkerThreadExecutor._failAll`.
|
|
558
|
+
*/
|
|
559
|
+
private _teardown(error: Error): void {
|
|
341
560
|
this._disposed = true
|
|
342
561
|
if (this._worker) {
|
|
343
562
|
this._worker.terminate()
|
|
344
563
|
this._worker = null
|
|
345
564
|
}
|
|
346
|
-
|
|
347
|
-
const error = new Error('Worker terminated')
|
|
565
|
+
this._readyReject?.(error)
|
|
348
566
|
for (const [, pending] of this._pending) {
|
|
349
567
|
pending.reject(error)
|
|
350
568
|
}
|
|
351
569
|
this._pending.clear()
|
|
352
|
-
this.
|
|
570
|
+
this._openFetchWsIds.clear()
|
|
571
|
+
this._fetchBridge?.disposeAll()
|
|
572
|
+
this._envBindingWsBridge?.disposeAll()
|
|
573
|
+
this._rpcChannel.disposeAll(error)
|
|
574
|
+
this._fetchStreams.disposeAll(error)
|
|
575
|
+
this._fetchRequestStreams.disposeAll()
|
|
353
576
|
}
|
|
354
577
|
}
|
|
355
578
|
|
|
@@ -358,21 +581,26 @@ export class WorkerExecutor implements DOExecutor {
|
|
|
358
581
|
export class WorkerExecutorFactory implements DOExecutorFactory {
|
|
359
582
|
private _modulePath?: string
|
|
360
583
|
private _configPath?: string
|
|
584
|
+
private _wranglerConfig?: WranglerConfig
|
|
361
585
|
|
|
362
586
|
/**
|
|
363
|
-
* Set the module
|
|
364
|
-
* Called by the generation manager after
|
|
587
|
+
* Set the module + config paths and the parsed env-overridden config for all
|
|
588
|
+
* executors created by this factory. Called by the generation manager after
|
|
589
|
+
* loading config.
|
|
365
590
|
*/
|
|
366
|
-
configure(modulePath: string, configPath: string): void {
|
|
591
|
+
configure(modulePath: string, configPath: string, wranglerConfig?: WranglerConfig): void {
|
|
367
592
|
this._modulePath = modulePath
|
|
368
593
|
this._configPath = configPath
|
|
594
|
+
this._wranglerConfig = wranglerConfig
|
|
369
595
|
}
|
|
370
596
|
|
|
371
597
|
create(config: ExecutorConfig): DOExecutor {
|
|
372
|
-
// Attach paths to the config for the executor to use
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
598
|
+
// Attach paths + parsed config to the config for the executor to use
|
|
599
|
+
return new WorkerExecutor({
|
|
600
|
+
...config,
|
|
601
|
+
_modulePath: this._modulePath ?? '',
|
|
602
|
+
_configPath: this._configPath ?? '',
|
|
603
|
+
_wranglerConfig: this._wranglerConfig,
|
|
604
|
+
})
|
|
377
605
|
}
|
|
378
606
|
}
|