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