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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import type { WranglerConfig } from '../config'
|
|
2
3
|
import type { ContainerConfig } from './container'
|
|
3
4
|
import type { DurableObjectBase, DurableObjectIdImpl, DurableObjectLimits } from './durable-object'
|
|
4
5
|
|
|
@@ -7,11 +8,24 @@ export interface ExecutorConfig {
|
|
|
7
8
|
db: Database
|
|
8
9
|
namespaceName: string
|
|
9
10
|
cls: new(ctx: any, env: unknown) => DurableObjectBase
|
|
10
|
-
env: unknown
|
|
11
|
+
env: Record<string, unknown>
|
|
11
12
|
dataDir?: string
|
|
12
13
|
limits?: DurableObjectLimits
|
|
13
14
|
containerConfig?: ContainerConfig
|
|
14
15
|
onAlarmSet?: (time: number | null) => void
|
|
16
|
+
/** @internal Worker-thread DO executors re-import the user module + config
|
|
17
|
+
* inside their Bun Worker; the factory injects these paths. */
|
|
18
|
+
_modulePath?: string
|
|
19
|
+
_configPath?: string
|
|
20
|
+
/** @internal Main's already-parsed, env-overridden wrangler config. Passed to
|
|
21
|
+
* the DO worker so it doesn't re-load from `_configPath` WITHOUT the `--env`
|
|
22
|
+
* overrides (which the re-parse silently dropped). */
|
|
23
|
+
_wranglerConfig?: WranglerConfig
|
|
24
|
+
/** @internal Disposal of the PRIOR executor for this same id, still in flight
|
|
25
|
+
* (its Docker container is being `docker rm`'d). A container DO awaits this
|
|
26
|
+
* before its first command so a fresh `docker run` for the same name doesn't
|
|
27
|
+
* race the teardown — which could `docker rm -f` the replacement. */
|
|
28
|
+
_priorDisposal?: Promise<void>
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
export interface DOExecutor {
|
|
@@ -39,6 +53,11 @@ export interface DOExecutor {
|
|
|
39
53
|
/** Whether the instance has been aborted */
|
|
40
54
|
isAborted(): boolean
|
|
41
55
|
|
|
56
|
+
/** Whether the underlying executor is dead (e.g. its Worker crashed). A
|
|
57
|
+
* disposed executor is unusable — the namespace drops it and recreates a
|
|
58
|
+
* fresh one on next access. Optional: executors that can't die report `false`. */
|
|
59
|
+
isDisposed?(): boolean
|
|
60
|
+
|
|
42
61
|
/** Hot-swap the DO class and env without disposing (preserves WebSocket connections) */
|
|
43
62
|
reloadClass?(cls: new(ctx: any, env: unknown) => DurableObjectBase, env: unknown): void
|
|
44
63
|
|
|
@@ -48,4 +67,7 @@ export interface DOExecutor {
|
|
|
48
67
|
|
|
49
68
|
export interface DOExecutorFactory {
|
|
50
69
|
create(config: ExecutorConfig): DOExecutor
|
|
70
|
+
/** Tell the factory where user code lives + the already-parsed env-overridden
|
|
71
|
+
* config (only used by worker-thread DO executors). */
|
|
72
|
+
configure?(modulePath: string, configPath: string, wranglerConfig?: WranglerConfig): void
|
|
51
73
|
}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* postMessage from the main thread (handshake), then initializes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { dirname } from 'node:path'
|
|
9
|
+
import { deserializeError, serializeError } from '../worker-thread/protocol'
|
|
10
|
+
import { OutboundStreamRegistry, pumpStream, STREAM_BACKPRESSURE_WINDOW, StreamReceiver } from '../worker-thread/stream-shared'
|
|
8
11
|
import type { DOCommand, DOMainMessage, DOResult, DOWorkerMessage } from './do-executor-worker'
|
|
9
12
|
|
|
10
13
|
declare var self: Worker
|
|
@@ -12,6 +15,10 @@ declare var self: Worker
|
|
|
12
15
|
interface WorkerConfig {
|
|
13
16
|
modulePath: string
|
|
14
17
|
configPath: string
|
|
18
|
+
/** Main's parsed, env-overridden config. When present it's used verbatim so
|
|
19
|
+
* the DO env honors `--env` overrides; absent (e.g. standalone test factory)
|
|
20
|
+
* we fall back to re-loading from `configPath`. */
|
|
21
|
+
wranglerConfig?: import('../config').WranglerConfig
|
|
15
22
|
dataDir: string
|
|
16
23
|
namespaceName: string
|
|
17
24
|
idStr: string
|
|
@@ -28,17 +35,12 @@ self.onmessage = async (event: MessageEvent) => {
|
|
|
28
35
|
try {
|
|
29
36
|
await initWorker(workerConfig)
|
|
30
37
|
} catch (e) {
|
|
31
|
-
const
|
|
38
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
32
39
|
postMessage(
|
|
33
40
|
{
|
|
34
41
|
type: 'result',
|
|
35
42
|
id: -1,
|
|
36
|
-
result: {
|
|
37
|
-
type: 'error',
|
|
38
|
-
message: `Worker init failed: ${error.message}`,
|
|
39
|
-
stack: error.stack,
|
|
40
|
-
name: error.name,
|
|
41
|
-
},
|
|
43
|
+
result: { type: 'error', error: serializeError(new Error(`Worker init failed: ${message}`, { cause: e })) },
|
|
42
44
|
} satisfies DOMainMessage,
|
|
43
45
|
)
|
|
44
46
|
}
|
|
@@ -51,25 +53,44 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
51
53
|
// Register Bun plugins for cloudflare:workers etc.
|
|
52
54
|
await import('../plugin')
|
|
53
55
|
|
|
54
|
-
const {
|
|
55
|
-
const { buildWorkerEnv } = await import('./do-worker-env')
|
|
56
|
+
const { buildWorkerEnv, createDoEnvRpc } = await import('./do-worker-env')
|
|
56
57
|
const { DurableObjectStateImpl, DurableObjectIdImpl } = await import('./durable-object')
|
|
57
|
-
const {
|
|
58
|
+
const { CFWebSocket } = await import('./websocket-pair')
|
|
59
|
+
const { WsGuestBridge } = await import('../worker-thread/ws-bridge-shared')
|
|
60
|
+
const { ContainerBase, ContainerContext, ContainerRuntime } = await import('./container')
|
|
61
|
+
const { DockerManager } = await import('./container-docker')
|
|
62
|
+
const { containerLabels } = await import('./container-cleanup')
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
// Prefer main's already-parsed, env-overridden config; only re-load from disk
|
|
65
|
+
// (WITHOUT --env overrides) when no parsed config was threaded through — e.g.
|
|
66
|
+
// the standalone test factory.
|
|
67
|
+
const config = workerConfig.wranglerConfig ?? await (await import('../config')).loadConfig(workerConfig.configPath)
|
|
68
|
+
// Per-worker dir for `.dev.vars`/`.env`/assets — the config file's directory.
|
|
69
|
+
const baseDir = dirname(workerConfig.configPath)
|
|
70
|
+
const envRpc = createDoEnvRpc(msg => postMessage(msg))
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Bridge for `Response{webSocket}` returned from env-binding fetches the
|
|
74
|
+
* DO worker calls (e.g. `this.env.SVC.fetch('/ws')`). The user-facing peer
|
|
75
|
+
* lives here; bytes sent / closes on it travel through this bridge back to
|
|
76
|
+
* the upstream CFWebSocket adopted on main.
|
|
77
|
+
*/
|
|
78
|
+
const envWsBridge = new WsGuestBridge<DOMainMessage>(msg => postMessage(msg), {
|
|
79
|
+
remoteMessage: (wsId, data) => ({ type: 'env-ws-outgoing', wsId, data }),
|
|
80
|
+
remoteClose: (wsId, code, reason, wasClean) => ({ type: 'env-ws-close-out', wsId, code, reason, wasClean }),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const { db, env } = buildWorkerEnv(config, workerConfig.dataDir, baseDir, envRpc, workerConfig.namespaceName, envWsBridge)
|
|
84
|
+
|
|
85
|
+
// Publish env to `globalEnv` so top-level `import { env } from
|
|
86
|
+
// 'cloudflare:workers'` in the user module sees this DO worker's env (not
|
|
87
|
+
// an empty default). Must happen BEFORE the dynamic import below.
|
|
88
|
+
const { setGlobalEnv } = await import('../env')
|
|
89
|
+
setGlobalEnv(env)
|
|
61
90
|
|
|
62
91
|
// Import user's worker module
|
|
63
92
|
const workerModule = await import(workerConfig.modulePath)
|
|
64
93
|
|
|
65
|
-
// Wire DO classes for nested DOs
|
|
66
|
-
for (const entry of doNamespaces) {
|
|
67
|
-
const cls = workerModule[entry.className]
|
|
68
|
-
if (cls) {
|
|
69
|
-
entry.namespace._setClass(cls as any, env)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
94
|
// Create this DO's instance
|
|
74
95
|
const id = new DurableObjectIdImpl(workerConfig.idStr, workerConfig.idName)
|
|
75
96
|
const cls = workerModule[workerConfig.namespaceName]
|
|
@@ -78,11 +99,106 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
78
99
|
}
|
|
79
100
|
|
|
80
101
|
const state = new DurableObjectStateImpl(id, db, workerConfig.namespaceName, workerConfig.dataDir)
|
|
102
|
+
|
|
103
|
+
// Mirror the instance's abort/block lifecycle to main so the idle reaper
|
|
104
|
+
// evicts an aborted instance (every subsequent command throws — it must be
|
|
105
|
+
// recreated fresh) and never evicts mid-blockConcurrencyWhile. Main only sees
|
|
106
|
+
// commands/WS traffic, so without these signals it can't observe either.
|
|
107
|
+
const postState = () => {
|
|
108
|
+
postMessage({ type: 'do-state', aborted: state._isAborted(), blocked: state._isBlocked() } satisfies DOMainMessage)
|
|
109
|
+
}
|
|
110
|
+
const originalAbort = state.abort.bind(state)
|
|
111
|
+
state.abort = (reason?: string) => {
|
|
112
|
+
originalAbort(reason)
|
|
113
|
+
postState()
|
|
114
|
+
}
|
|
115
|
+
const originalBlock = state.blockConcurrencyWhile.bind(state)
|
|
116
|
+
state.blockConcurrencyWhile = <T>(cb: () => Promise<T>): Promise<T> => {
|
|
117
|
+
const p = originalBlock(cb)
|
|
118
|
+
postState() // entered the block
|
|
119
|
+
// left it — both-callbacks form so a rejecting callback (the caller's
|
|
120
|
+
// to handle via the returned `p`) can't float an unhandled rejection
|
|
121
|
+
p.then(postState, postState)
|
|
122
|
+
return p
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Mirrors the `ContainerRuntime` wiring `InProcessExecutor` did on main —
|
|
126
|
+
// without it, `ContainerBase` instances fail with "Container runtime not
|
|
127
|
+
// initialized" on the first `startAndWaitForPorts` call.
|
|
128
|
+
const containerEntry = config.containers?.find(c => c.class_name === workerConfig.namespaceName)
|
|
129
|
+
let containerRuntime: InstanceType<typeof ContainerRuntime> | undefined
|
|
130
|
+
if (containerEntry) {
|
|
131
|
+
// `DockerManager` itself stays per-worker (the hot path is `docker
|
|
132
|
+
// inspect` polling — no point routing every health check through
|
|
133
|
+
// main). Cleanup tracking, however, lives on main: the worker posts
|
|
134
|
+
// `container-registered` / `container-removed` so main's `exit`
|
|
135
|
+
// handler can rm -f everything regardless of which worker spawned it.
|
|
136
|
+
const dockerManager = new DockerManager({
|
|
137
|
+
onRegister: name => postMessage({ type: 'container-registered', name } satisfies DOMainMessage),
|
|
138
|
+
onRemove: name => postMessage({ type: 'container-removed', name } satisfies DOMainMessage),
|
|
139
|
+
labels: containerLabels(),
|
|
140
|
+
})
|
|
141
|
+
containerRuntime = new ContainerRuntime(
|
|
142
|
+
workerConfig.namespaceName,
|
|
143
|
+
id.toString(),
|
|
144
|
+
containerEntry.image,
|
|
145
|
+
dockerManager,
|
|
146
|
+
)
|
|
147
|
+
state.container = new ContainerContext(containerRuntime)
|
|
148
|
+
}
|
|
149
|
+
|
|
81
150
|
const instance = new (cls as any)(state, env)
|
|
82
151
|
|
|
152
|
+
if (containerRuntime && instance instanceof ContainerBase) {
|
|
153
|
+
instance._wireRuntime(containerRuntime)
|
|
154
|
+
}
|
|
155
|
+
|
|
83
156
|
state._setInstanceResolver(() => instance)
|
|
84
157
|
|
|
85
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Bridge for `Response{webSocket}` returned by the DO's own fetch(). Forwards
|
|
160
|
+
* the user-facing peer's events up to main and dispatches inbound real-client
|
|
161
|
+
* events onto the user-facing peer.
|
|
162
|
+
*/
|
|
163
|
+
const fetchWsBridge = new WsGuestBridge<DOMainMessage>(msg => postMessage(msg), {
|
|
164
|
+
remoteMessage: (wsId, data) => ({ type: 'fetch-ws-outgoing', wsId, data }),
|
|
165
|
+
remoteClose: (wsId, code, reason, wasClean) => ({ type: 'fetch-ws-close-out', wsId, code, reason, wasClean }),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
/** Active body pumps for streamed DO-fetch responses, keyed by `streamId`,
|
|
169
|
+
* so an inbound `do-stream-cancel` can stop the source reader. */
|
|
170
|
+
const fetchStreams = new OutboundStreamRegistry()
|
|
171
|
+
|
|
172
|
+
/** Active reconstructed DO-fetch request bodies (main → DO worker). Chunks
|
|
173
|
+
* arriving before the controller registers queue inside the receiver. */
|
|
174
|
+
const requestStreams = new StreamReceiver(
|
|
175
|
+
(streamId) => {
|
|
176
|
+
postMessage({ type: 'do-req-stream-cancel', streamId } satisfies DOMainMessage)
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
window: STREAM_BACKPRESSURE_WINDOW,
|
|
180
|
+
onCredit: (streamId) => postMessage({ type: 'do-req-stream-ack', streamId } satisfies DOMainMessage),
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
function pumpFetchBody(streamId: number, body: ReadableStream<Uint8Array>): void {
|
|
185
|
+
type Chunk = Extract<DOMainMessage, { type: 'do-stream-chunk' }>
|
|
186
|
+
type End = Extract<DOMainMessage, { type: 'do-stream-end' }>
|
|
187
|
+
type Err = Extract<DOMainMessage, { type: 'do-stream-error' }>
|
|
188
|
+
pumpStream<Chunk, End, Err>(
|
|
189
|
+
streamId,
|
|
190
|
+
body,
|
|
191
|
+
fetchStreams,
|
|
192
|
+
msg => postMessage(msg),
|
|
193
|
+
{
|
|
194
|
+
chunk: (id, chunk) => ({ type: 'do-stream-chunk', streamId: id, chunk }),
|
|
195
|
+
end: (id) => ({ type: 'do-stream-end', streamId: id }),
|
|
196
|
+
error: (id, error) => ({ type: 'do-stream-error', streamId: id, error }),
|
|
197
|
+
},
|
|
198
|
+
undefined,
|
|
199
|
+
STREAM_BACKPRESSURE_WINDOW,
|
|
200
|
+
)
|
|
201
|
+
}
|
|
86
202
|
|
|
87
203
|
// Wire alarm callback
|
|
88
204
|
state.storage._setAlarmCallback((time: number | null) => {
|
|
@@ -91,7 +207,20 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
91
207
|
|
|
92
208
|
// --- Command handler ---
|
|
93
209
|
|
|
94
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Some commands need to perform side effects *after* their result message is
|
|
212
|
+
* posted — specifically, streamed fetch responses must wait for main to see
|
|
213
|
+
* the `streamId` on the `result` before chunk messages arrive (otherwise the
|
|
214
|
+
* first chunks land before `start()` registers the controller and have to
|
|
215
|
+
* sit in `_pendingStreamEvents`). The handler returns a continuation so the
|
|
216
|
+
* dispatcher can post the result first and then start the pump.
|
|
217
|
+
*/
|
|
218
|
+
interface HandledCommand {
|
|
219
|
+
result: DOResult
|
|
220
|
+
afterPost?: () => void
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function handleCommand(cmd: DOCommand): Promise<HandledCommand> {
|
|
95
224
|
switch (cmd.type) {
|
|
96
225
|
case 'fetch': {
|
|
97
226
|
await state._enter()
|
|
@@ -100,21 +229,61 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
100
229
|
if (typeof fetchFn !== 'function') {
|
|
101
230
|
throw new Error('Durable Object does not implement fetch()')
|
|
102
231
|
}
|
|
232
|
+
const reqBody = cmd.streamId !== undefined ? requestStreams.open(cmd.streamId) : cmd.body
|
|
103
233
|
const request = new Request(cmd.url, {
|
|
104
234
|
method: cmd.method,
|
|
105
235
|
headers: cmd.headers,
|
|
106
|
-
body:
|
|
236
|
+
body: reqBody,
|
|
107
237
|
})
|
|
108
|
-
|
|
109
|
-
|
|
238
|
+
let response: Response
|
|
239
|
+
try {
|
|
240
|
+
response = await fetchFn.call(instance, request)
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// The instance's fetch errored without draining the streamed
|
|
243
|
+
// request body — cancel the receiver so main stops pumping and
|
|
244
|
+
// buffered chunks are dropped. Mirrors dispatchRpcFetch's contract.
|
|
245
|
+
if (cmd.streamId !== undefined) requestStreams.cancel(cmd.streamId)
|
|
246
|
+
throw e
|
|
247
|
+
}
|
|
248
|
+
// Resolved but the body wasn't consumed (e.g. a 401 before reading) —
|
|
249
|
+
// cancel the receiver so main's pump doesn't park forever holding the
|
|
250
|
+
// source. DO fetch has no ctx.waitUntil, so nothing reads it later.
|
|
251
|
+
if (cmd.streamId !== undefined && !request.bodyUsed) {
|
|
252
|
+
requestStreams.cancel(cmd.streamId)
|
|
253
|
+
}
|
|
254
|
+
const clientWs = (response as { webSocket?: unknown }).webSocket
|
|
255
|
+
const hasWebSocket = response.status === 101 && clientWs instanceof CFWebSocket
|
|
110
256
|
const resHeaders: [string, string][] = []
|
|
111
257
|
response.headers.forEach((v: string, k: string) => resHeaders.push([k, v]))
|
|
258
|
+
|
|
259
|
+
let fetchWebSocketId: string | undefined
|
|
260
|
+
if (hasWebSocket) {
|
|
261
|
+
const cw = clientWs as InstanceType<typeof CFWebSocket>
|
|
262
|
+
// Main pins the executor on this id (hibernation or plain socket
|
|
263
|
+
// alike) when it processes the result and unpins on close, so no
|
|
264
|
+
// separate accept signal is needed.
|
|
265
|
+
fetchWebSocketId = fetchWsBridge.register(cw)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let streamId: number | undefined
|
|
269
|
+
let afterPost: (() => void) | undefined
|
|
270
|
+
if (!hasWebSocket && response.body) {
|
|
271
|
+
streamId = fetchStreams.allocateId()
|
|
272
|
+
const body = response.body
|
|
273
|
+
afterPost = () => pumpFetchBody(streamId!, body)
|
|
274
|
+
}
|
|
275
|
+
|
|
112
276
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
277
|
+
result: {
|
|
278
|
+
type: 'fetch',
|
|
279
|
+
status: response.status,
|
|
280
|
+
statusText: response.statusText,
|
|
281
|
+
headers: resHeaders,
|
|
282
|
+
body: null,
|
|
283
|
+
fetchWebSocketId,
|
|
284
|
+
streamId,
|
|
285
|
+
},
|
|
286
|
+
afterPost,
|
|
118
287
|
}
|
|
119
288
|
} finally {
|
|
120
289
|
state._exit()
|
|
@@ -129,7 +298,7 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
129
298
|
throw new Error(`"${cmd.method}" is not a method on the Durable Object`)
|
|
130
299
|
}
|
|
131
300
|
const result = await val.call(instance, ...cmd.args)
|
|
132
|
-
return { type: 'rpc-call', value: result }
|
|
301
|
+
return { result: { type: 'rpc-call', value: result } }
|
|
133
302
|
} finally {
|
|
134
303
|
state._exit()
|
|
135
304
|
}
|
|
@@ -140,9 +309,9 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
140
309
|
try {
|
|
141
310
|
const val = (instance as any)[cmd.prop]
|
|
142
311
|
if (typeof val === 'function') {
|
|
143
|
-
return { type: 'rpc-get',
|
|
312
|
+
return { result: { type: 'rpc-get', kind: 'function' } }
|
|
144
313
|
}
|
|
145
|
-
return { type: 'rpc-get', value: val }
|
|
314
|
+
return { result: { type: 'rpc-get', kind: 'value', value: val } }
|
|
146
315
|
} finally {
|
|
147
316
|
state._exit()
|
|
148
317
|
}
|
|
@@ -158,18 +327,17 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
158
327
|
isRetry: cmd.retryCount > 0,
|
|
159
328
|
})
|
|
160
329
|
}
|
|
161
|
-
return { type: 'alarm' }
|
|
330
|
+
return { result: { type: 'alarm' } }
|
|
162
331
|
} finally {
|
|
163
332
|
state._exit()
|
|
164
333
|
}
|
|
165
334
|
}
|
|
166
335
|
|
|
167
|
-
case '
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return { type: 'ws-created', wsId: cmd.wsId }
|
|
336
|
+
case 'cleanup': {
|
|
337
|
+
// Tear down the Docker container (rm -f + stop timers) before main
|
|
338
|
+
// terminates this thread. No-op for non-container DOs.
|
|
339
|
+
await containerRuntime?.cleanup()
|
|
340
|
+
return { result: { type: 'cleanup' } }
|
|
173
341
|
}
|
|
174
342
|
|
|
175
343
|
default:
|
|
@@ -181,37 +349,51 @@ async function initWorker(workerConfig: WorkerConfig) {
|
|
|
181
349
|
self.onmessage = async (event: MessageEvent<DOWorkerMessage>) => {
|
|
182
350
|
const msg = event.data
|
|
183
351
|
|
|
352
|
+
// Env-binding RPC replies from main (service-binding fetches, etc.)
|
|
353
|
+
if (envRpc.handle(msg as { type: string })) return
|
|
354
|
+
|
|
184
355
|
if (msg.type === 'command') {
|
|
185
356
|
try {
|
|
186
|
-
const result = await handleCommand(msg.command)
|
|
357
|
+
const { result, afterPost } = await handleCommand(msg.command)
|
|
187
358
|
postMessage({ type: 'result', id: msg.id, result } satisfies DOMainMessage)
|
|
359
|
+
// Post-result side effects: for streamed fetch responses, start the
|
|
360
|
+
// body pump *after* `result` ships so main has registered the
|
|
361
|
+
// `streamId` before any chunk arrives.
|
|
362
|
+
afterPost?.()
|
|
363
|
+
// Surface any abort/block transition this command caused (e.g. it
|
|
364
|
+
// called state.abort()). The wrappers above catch abort/block from
|
|
365
|
+
// outside a command (timers, WS handlers); this catches in-command.
|
|
366
|
+
postState()
|
|
188
367
|
} catch (e) {
|
|
189
|
-
const error = e instanceof Error ? e : new Error(String(e))
|
|
190
368
|
postMessage(
|
|
191
369
|
{
|
|
192
370
|
type: 'result',
|
|
193
371
|
id: msg.id,
|
|
194
|
-
result: {
|
|
195
|
-
type: 'error',
|
|
196
|
-
message: error.message,
|
|
197
|
-
stack: error.stack,
|
|
198
|
-
name: error.name,
|
|
199
|
-
},
|
|
372
|
+
result: { type: 'error', error: serializeError(e) },
|
|
200
373
|
} satisfies DOMainMessage,
|
|
201
374
|
)
|
|
375
|
+
// The command may have thrown because the instance was aborted
|
|
376
|
+
// (state._enter() rejects once aborted) — make sure main learns of it.
|
|
377
|
+
postState()
|
|
202
378
|
}
|
|
203
|
-
} else if (msg.type === '
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
379
|
+
} else if (msg.type === 'do-stream-ack') {
|
|
380
|
+
fetchStreams.grantCredit(msg.streamId)
|
|
381
|
+
} else if (msg.type === 'do-stream-cancel') {
|
|
382
|
+
fetchStreams.cancel(msg.streamId)
|
|
383
|
+
} else if (msg.type === 'do-req-stream-chunk') {
|
|
384
|
+
requestStreams.push(msg.streamId, msg.chunk)
|
|
385
|
+
} else if (msg.type === 'do-req-stream-end') {
|
|
386
|
+
requestStreams.end(msg.streamId)
|
|
387
|
+
} else if (msg.type === 'do-req-stream-error') {
|
|
388
|
+
requestStreams.error(msg.streamId, deserializeError(msg.error))
|
|
389
|
+
} else if (msg.type === 'fetch-ws-incoming') {
|
|
390
|
+
fetchWsBridge.deliverClientMessage(msg.wsId, msg.data)
|
|
391
|
+
} else if (msg.type === 'fetch-ws-close-in') {
|
|
392
|
+
fetchWsBridge.deliverClientClose(msg.wsId, msg.code, msg.reason, msg.wasClean)
|
|
393
|
+
} else if (msg.type === 'env-ws-incoming') {
|
|
394
|
+
envWsBridge.deliverClientMessage(msg.wsId, msg.data)
|
|
395
|
+
} else if (msg.type === 'env-ws-close-in') {
|
|
396
|
+
envWsBridge.deliverClientClose(msg.wsId, msg.code, msg.reason, msg.wasClean)
|
|
215
397
|
}
|
|
216
398
|
}
|
|
217
399
|
|