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
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless-binding env builder for the main worker-thread runtime.
|
|
3
|
+
*
|
|
4
|
+
* Builds the bindings whose state lives on disk (.lopata SQLite/files) —
|
|
5
|
+
* the same physical files the main thread uses. WAL mode + busy_timeout
|
|
6
|
+
* make multiple `bun:sqlite` handles to the same file safe.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Database } from 'bun:sqlite'
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { AiBinding } from '../bindings/ai'
|
|
13
|
+
import { SqliteAnalyticsEngine } from '../bindings/analytics-engine'
|
|
14
|
+
import { BrowserBinding } from '../bindings/browser'
|
|
15
|
+
import { openD1Database } from '../bindings/d1'
|
|
16
|
+
import { DurableObjectIdImpl, hashIdFromName, randomUniqueIdHex } from '../bindings/durable-object'
|
|
17
|
+
import { EmailMessage } from '../bindings/email'
|
|
18
|
+
import { HyperdriveBinding } from '../bindings/hyperdrive'
|
|
19
|
+
import { ImagesBinding } from '../bindings/images'
|
|
20
|
+
import { SqliteKVNamespace } from '../bindings/kv'
|
|
21
|
+
import { MediaBinding } from '../bindings/media'
|
|
22
|
+
import { FileR2Bucket } from '../bindings/r2'
|
|
23
|
+
import { makeBindingProxy } from '../bindings/rpc-stub'
|
|
24
|
+
import { serviceBindingConnectError } from '../bindings/service-binding'
|
|
25
|
+
import { StaticAssets } from '../bindings/static-assets'
|
|
26
|
+
import type { ResponseWithWebSocket } from '../bindings/websocket-pair'
|
|
27
|
+
import { SqliteWorkflowBinding } from '../bindings/workflow'
|
|
28
|
+
import type { WranglerConfig } from '../config'
|
|
29
|
+
import { runMigrations } from '../db'
|
|
30
|
+
import { parseDevVars } from '../env'
|
|
31
|
+
import { warnCrossThreadRpcArgs, warnInvalidRpcArgs } from '../rpc-validate'
|
|
32
|
+
import { getActiveContext } from '../tracing/context'
|
|
33
|
+
import { instrumentBinding, instrumentD1 } from '../tracing/instrument'
|
|
34
|
+
import type { BindingTarget, WorkerMessage } from './protocol'
|
|
35
|
+
import type { RpcClient } from './rpc-client'
|
|
36
|
+
import { tagCloneable } from './rpc-shared'
|
|
37
|
+
import type { WsGuestBridge } from './ws-bridge-shared'
|
|
38
|
+
|
|
39
|
+
export interface ThreadEnvOptions {
|
|
40
|
+
config: WranglerConfig
|
|
41
|
+
/** Per-worker dir for `.dev.vars`/`.env`/assets resolution only. */
|
|
42
|
+
baseDir: string
|
|
43
|
+
/** Shared `.lopata` data dir (main's `getDataDir()`) — the SQLite/r2/d1 files
|
|
44
|
+
* main and the DO workers use. Distinct from `baseDir` in multi-worker mode. */
|
|
45
|
+
dataDir: string
|
|
46
|
+
rpc: RpcClient
|
|
47
|
+
/** Guest-side bridge for WebSockets returned by env-binding fetches. */
|
|
48
|
+
envWsBridge: WsGuestBridge<WorkerMessage>
|
|
49
|
+
browserConfig?: { wsEndpoint?: string; executablePath?: string; headless?: boolean }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ThreadEnvBuilt {
|
|
53
|
+
env: Record<string, unknown>
|
|
54
|
+
/** Thread-local DB handle — shared with the workflow + queue consumer wiring
|
|
55
|
+
* the caller does after the user module loads. */
|
|
56
|
+
db: Database
|
|
57
|
+
/** Workflows the caller still needs to wire after the user module loads. */
|
|
58
|
+
workflows: { bindingName: string; className: string; binding: SqliteWorkflowBinding }[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildThreadEnv({ config, baseDir, dataDir, rpc, envWsBridge, browserConfig }: ThreadEnvOptions): ThreadEnvBuilt {
|
|
62
|
+
mkdirSync(dataDir, { recursive: true })
|
|
63
|
+
mkdirSync(path.join(dataDir, 'r2'), { recursive: true })
|
|
64
|
+
mkdirSync(path.join(dataDir, 'd1'), { recursive: true })
|
|
65
|
+
|
|
66
|
+
const db = new Database(path.join(dataDir, 'data.sqlite'), { create: true })
|
|
67
|
+
db.run('PRAGMA journal_mode=WAL')
|
|
68
|
+
db.run('PRAGMA busy_timeout=5000')
|
|
69
|
+
runMigrations(db)
|
|
70
|
+
|
|
71
|
+
const env: Record<string, unknown> = {}
|
|
72
|
+
|
|
73
|
+
if (config.vars) {
|
|
74
|
+
for (const [key, value] of Object.entries(config.vars)) {
|
|
75
|
+
env[key] = value
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const devVarsPath = path.join(baseDir, '.dev.vars')
|
|
80
|
+
const envPath = path.join(baseDir, '.env')
|
|
81
|
+
const filePath = existsSync(devVarsPath) ? devVarsPath : existsSync(envPath) ? envPath : null
|
|
82
|
+
if (filePath) {
|
|
83
|
+
const devVars = parseDevVars(readFileSync(filePath, 'utf-8'))
|
|
84
|
+
for (const [key, value] of Object.entries(devVars)) {
|
|
85
|
+
env[key] = value
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const kv of config.kv_namespaces ?? []) {
|
|
90
|
+
env[kv.binding] = instrumentBinding(new SqliteKVNamespace(db, kv.id), {
|
|
91
|
+
type: 'kv',
|
|
92
|
+
name: kv.binding,
|
|
93
|
+
methods: ['get', 'getWithMetadata', 'put', 'delete', 'list'],
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const r2 of config.r2_buckets ?? []) {
|
|
98
|
+
env[r2.binding] = instrumentBinding(new FileR2Bucket(db, r2.bucket_name, dataDir), {
|
|
99
|
+
type: 'r2',
|
|
100
|
+
name: r2.binding,
|
|
101
|
+
methods: ['get', 'put', 'delete', 'list', 'head', 'createMultipartUpload'],
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const d1 of config.d1_databases ?? []) {
|
|
106
|
+
env[d1.binding] = instrumentD1(openD1Database(dataDir, d1.database_name), d1.binding)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const producer of config.queues?.producers ?? []) {
|
|
110
|
+
env[producer.binding] = makeQueueProducerProxy(producer.binding, rpc, envWsBridge)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const email of config.send_email ?? []) {
|
|
114
|
+
env[email.name] = makeSendEmailProxy(email.name, rpc, envWsBridge)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const svc of config.services ?? []) {
|
|
118
|
+
env[svc.binding] = makeServiceBindingProxy(svc.binding, rpc, envWsBridge)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const doBinding of config.durable_objects?.bindings ?? []) {
|
|
122
|
+
env[doBinding.name] = makeDONamespaceProxy(doBinding.name, rpc, envWsBridge)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Container DOs whose class isn't already declared under `durable_objects`
|
|
126
|
+
// — main's `buildEnv` synthesises a DO namespace for them, so we need the
|
|
127
|
+
// matching proxy in the worker env or the binding is missing at runtime.
|
|
128
|
+
const doClassNames = new Set((config.durable_objects?.bindings ?? []).map(b => b.class_name))
|
|
129
|
+
for (const container of config.containers ?? []) {
|
|
130
|
+
if (doClassNames.has(container.class_name)) continue
|
|
131
|
+
const bindingName = container.name ?? container.class_name
|
|
132
|
+
env[bindingName] = makeDONamespaceProxy(bindingName, rpc, envWsBridge)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Workflows live entirely in the worker thread — class refs and the
|
|
136
|
+
// state machine are both here; main never sees the binding. Caller wires
|
|
137
|
+
// the class once the user module is imported.
|
|
138
|
+
const workflows: ThreadEnvBuilt['workflows'] = []
|
|
139
|
+
for (const wf of config.workflows ?? []) {
|
|
140
|
+
const binding = new SqliteWorkflowBinding(db, wf.binding, wf.class_name, wf.limits)
|
|
141
|
+
env[wf.binding] = binding
|
|
142
|
+
workflows.push({ bindingName: wf.binding, className: wf.class_name, binding })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.assets?.binding) {
|
|
146
|
+
const assetsDir = path.resolve(baseDir, config.assets.directory)
|
|
147
|
+
env[config.assets.binding] = instrumentBinding(
|
|
148
|
+
new StaticAssets(assetsDir, config.assets.html_handling, config.assets.not_found_handling),
|
|
149
|
+
{ type: 'assets', name: config.assets.binding, methods: ['fetch'] },
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (config.images) {
|
|
154
|
+
env[config.images.binding] = instrumentBinding(new ImagesBinding(), {
|
|
155
|
+
type: 'images',
|
|
156
|
+
name: config.images.binding,
|
|
157
|
+
methods: ['info'],
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (config.media) {
|
|
162
|
+
env[config.media.binding] = instrumentBinding(new MediaBinding(), {
|
|
163
|
+
type: 'media',
|
|
164
|
+
name: config.media.binding,
|
|
165
|
+
methods: [],
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const hd of config.hyperdrive ?? []) {
|
|
170
|
+
env[hd.binding] = new HyperdriveBinding(hd.localConnectionString ?? '')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (config.browser) {
|
|
174
|
+
env[config.browser.binding] = instrumentBinding(new BrowserBinding(browserConfig ?? {}), {
|
|
175
|
+
type: 'browser',
|
|
176
|
+
name: config.browser.binding,
|
|
177
|
+
methods: ['launch', 'connect', 'sessions'],
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (config.ai) {
|
|
182
|
+
const accountId = typeof env.CLOUDFLARE_ACCOUNT_ID === 'string' ? env.CLOUDFLARE_ACCOUNT_ID : process.env.CLOUDFLARE_ACCOUNT_ID
|
|
183
|
+
const apiToken = typeof env.CLOUDFLARE_API_TOKEN === 'string' ? env.CLOUDFLARE_API_TOKEN : process.env.CLOUDFLARE_API_TOKEN
|
|
184
|
+
env[config.ai.binding] = instrumentBinding(new AiBinding(db, accountId, apiToken), {
|
|
185
|
+
type: 'ai',
|
|
186
|
+
name: config.ai.binding,
|
|
187
|
+
methods: ['run', 'models'],
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const ae of config.analytics_engine_datasets ?? []) {
|
|
192
|
+
env[ae.binding] = instrumentBinding(new SqliteAnalyticsEngine(db, ae.dataset ?? ae.binding), {
|
|
193
|
+
type: 'analytics_engine',
|
|
194
|
+
name: ae.binding,
|
|
195
|
+
methods: ['writeDataPoint'],
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (config.version_metadata) {
|
|
200
|
+
env[config.version_metadata.binding] = {
|
|
201
|
+
id: 'local-dev',
|
|
202
|
+
tag: '',
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { env, db, workflows }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function makeQueueProducerProxy(bindingName: string, rpc: RpcClient, envWsBridge: WsGuestBridge<WorkerMessage>): Record<string, unknown> {
|
|
211
|
+
return makeRpcProxy({ binding: bindingName }, rpc, envWsBridge)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function materializeEmailRaw(raw: unknown): Promise<Uint8Array | ArrayBuffer | string> {
|
|
215
|
+
if (typeof raw === 'string' || raw instanceof Uint8Array || raw instanceof ArrayBuffer) return raw
|
|
216
|
+
if (raw && typeof (raw as ReadableStream).getReader === 'function') {
|
|
217
|
+
return new Response(raw as ReadableStream).arrayBuffer()
|
|
218
|
+
}
|
|
219
|
+
throw new Error('EmailMessage.raw must be a string, Uint8Array, ArrayBuffer, or ReadableStream')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function proxyFetch(
|
|
223
|
+
target: BindingTarget,
|
|
224
|
+
rpc: RpcClient,
|
|
225
|
+
envWsBridge: WsGuestBridge<WorkerMessage>,
|
|
226
|
+
input: Request | string | URL,
|
|
227
|
+
init?: RequestInit,
|
|
228
|
+
): Promise<Response> {
|
|
229
|
+
const url = input instanceof URL ? input.toString() : input
|
|
230
|
+
const request = typeof url === 'string' ? new Request(url, init) : url
|
|
231
|
+
const serialized = await rpc.callFetch(target, request)
|
|
232
|
+
const response = rpc.makeResponse(serialized) as ResponseWithWebSocket
|
|
233
|
+
// If the binding's response carried a WebSocket upgrade, main adopted the
|
|
234
|
+
// upstream peer (bridgeEvents) under `webSocketId`. Reconstruct a real
|
|
235
|
+
// user-facing CFWebSocket bridged to it so user code can `.accept()` /
|
|
236
|
+
// `.addEventListener('message')` it — and reshipping it just re-registers it on
|
|
237
|
+
// the top-level WS bridge in entry.ts (double-bridge through to the client).
|
|
238
|
+
if (serialized.webSocketId !== undefined) {
|
|
239
|
+
response.webSocket = envWsBridge.createBridgedSocket(serialized.webSocketId)
|
|
240
|
+
}
|
|
241
|
+
return response
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Per-top-level-request subrequest budget, counted on the WORKER side. The
|
|
246
|
+
* main-side `ServiceBinding._checkSubrequestLimit` is effectively dead in thread
|
|
247
|
+
* mode — every binding call arrives as its own RPC dispatch wrapped in
|
|
248
|
+
* `runWithParentContext`, which re-seeds `count: 0`, so it never trips. The
|
|
249
|
+
* worker's fetch context (seeded once per request and shared across all its
|
|
250
|
+
* binding calls via AsyncLocalStorage) is where the budget actually accumulates.
|
|
251
|
+
*/
|
|
252
|
+
const MAX_SUBREQUESTS = 1000
|
|
253
|
+
|
|
254
|
+
function checkSubrequestLimit(): void {
|
|
255
|
+
const counter = getActiveContext()?.subrequests
|
|
256
|
+
if (!counter) return
|
|
257
|
+
counter.count++
|
|
258
|
+
if (counter.count > MAX_SUBREQUESTS) {
|
|
259
|
+
throw new Error(`Subrequest limit exceeded (max ${MAX_SUBREQUESTS} per request)`)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function makeRpcProxy(
|
|
264
|
+
target: BindingTarget,
|
|
265
|
+
rpc: RpcClient,
|
|
266
|
+
envWsBridge: WsGuestBridge<WorkerMessage>,
|
|
267
|
+
extras: Record<string | symbol, unknown> = {},
|
|
268
|
+
): Record<string, unknown> {
|
|
269
|
+
return makeBindingProxy(
|
|
270
|
+
{
|
|
271
|
+
fetch: (input, init) => {
|
|
272
|
+
checkSubrequestLimit()
|
|
273
|
+
return proxyFetch(target, rpc, envWsBridge, input, init)
|
|
274
|
+
},
|
|
275
|
+
call: (prop, args) => {
|
|
276
|
+
// Restore the in-process path's dev-time warning for args that won't
|
|
277
|
+
// survive the cross-thread hop (functions, RpcTargets, …) — otherwise
|
|
278
|
+
// the postMessage just throws an opaque DataCloneError with no guidance.
|
|
279
|
+
warnInvalidRpcArgs(args, prop)
|
|
280
|
+
warnCrossThreadRpcArgs(args, prop)
|
|
281
|
+
checkSubrequestLimit()
|
|
282
|
+
return rpc.call(target, prop, args)
|
|
283
|
+
},
|
|
284
|
+
getProperty: prop => rpc.callGet(target, prop),
|
|
285
|
+
},
|
|
286
|
+
extras,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function makeDOStubProxy(
|
|
291
|
+
bindingName: string,
|
|
292
|
+
idStr: string,
|
|
293
|
+
id: DurableObjectIdImpl,
|
|
294
|
+
rpc: RpcClient,
|
|
295
|
+
envWsBridge: WsGuestBridge<WorkerMessage>,
|
|
296
|
+
): unknown {
|
|
297
|
+
return makeRpcProxy({ binding: bindingName, instanceId: idStr, instanceName: id.name }, rpc, envWsBridge, { id, name: id.name })
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function makeDONamespaceProxy(bindingName: string, rpc: RpcClient, envWsBridge: WsGuestBridge<WorkerMessage>): Record<string, unknown> {
|
|
301
|
+
const stubs = new Map<string, unknown>()
|
|
302
|
+
const idFromName = (name: string) => new DurableObjectIdImpl(hashIdFromName(name), name)
|
|
303
|
+
const idFromString = (idStr: string) => new DurableObjectIdImpl(idStr)
|
|
304
|
+
const newUniqueId = (_opts?: { jurisdiction?: string }) => new DurableObjectIdImpl(randomUniqueIdHex())
|
|
305
|
+
const get = (id: DurableObjectIdImpl) => {
|
|
306
|
+
const idStr = id.toString()
|
|
307
|
+
// Cache key includes name so a nameless `idFromString(hash)` and a named
|
|
308
|
+
// `idFromName(...)` resolving to the same hash get distinct stubs — each
|
|
309
|
+
// preserving its caller's `id.name`. Same id-shape → same cached stub.
|
|
310
|
+
const key = `${idStr}:${id.name ?? ''}`
|
|
311
|
+
let stub = stubs.get(key)
|
|
312
|
+
if (!stub) {
|
|
313
|
+
stub = makeDOStubProxy(bindingName, idStr, id, rpc, envWsBridge)
|
|
314
|
+
stubs.set(key, stub)
|
|
315
|
+
}
|
|
316
|
+
return stub
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
idFromName,
|
|
320
|
+
idFromString,
|
|
321
|
+
newUniqueId,
|
|
322
|
+
get,
|
|
323
|
+
getByName: (name: string) => get(idFromName(name)),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function makeServiceBindingProxy(bindingName: string, rpc: RpcClient, envWsBridge: WsGuestBridge<WorkerMessage>): unknown {
|
|
328
|
+
return makeRpcProxy({ binding: bindingName }, rpc, envWsBridge, {
|
|
329
|
+
connect: () => {
|
|
330
|
+
throw serviceBindingConnectError(bindingName)
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function makeSendEmailProxy(bindingName: string, rpc: RpcClient, envWsBridge: WsGuestBridge<WorkerMessage>): Record<string, unknown> {
|
|
336
|
+
const target: BindingTarget = { binding: bindingName }
|
|
337
|
+
const taggedSend = async (message: unknown) => {
|
|
338
|
+
// Structured-clone strips EmailMessage's class identity, so tag it and
|
|
339
|
+
// let main rebuild via `reifyArgs` (registered reviver in email.ts).
|
|
340
|
+
const arg = message instanceof EmailMessage
|
|
341
|
+
? tagCloneable('EmailMessage', {
|
|
342
|
+
from: message.from,
|
|
343
|
+
to: message.to,
|
|
344
|
+
raw: await materializeEmailRaw(message.raw),
|
|
345
|
+
})
|
|
346
|
+
: message
|
|
347
|
+
return rpc.call(target, 'send', [arg])
|
|
348
|
+
}
|
|
349
|
+
return makeRpcProxy(target, rpc, envWsBridge, { send: taggedSend })
|
|
350
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow + queue wiring that runs after the user module loads in the worker
|
|
3
|
+
* thread. Kept out of `thread-env.ts` so the env builder doesn't need to
|
|
4
|
+
* receive the user module — buildThreadEnv runs before `import(modulePath)`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from 'bun:sqlite'
|
|
8
|
+
import { QueueConsumer } from '../bindings/queue'
|
|
9
|
+
import { wireWorkflowClass } from '../bindings/workflow'
|
|
10
|
+
import type { WranglerConfig } from '../config'
|
|
11
|
+
import type { ThreadEnvBuilt } from './thread-env'
|
|
12
|
+
|
|
13
|
+
export function wireWorkflows(built: ThreadEnvBuilt, workerModule: Record<string, unknown>): void {
|
|
14
|
+
for (const wf of built.workflows) {
|
|
15
|
+
wireWorkflowClass(wf.binding, wf.className, workerModule, built.env)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Spawn queue consumers in the worker thread. Shared SQLite means the consumer
|
|
21
|
+
* can poll, manage leases, and apply ack/retry decisions locally — exactly
|
|
22
|
+
* like the in-process flow — without any cross-thread RPC.
|
|
23
|
+
*/
|
|
24
|
+
export function startThreadQueueConsumers(
|
|
25
|
+
config: WranglerConfig,
|
|
26
|
+
db: Database,
|
|
27
|
+
env: Record<string, unknown>,
|
|
28
|
+
workerModule: Record<string, unknown>,
|
|
29
|
+
workerName?: string,
|
|
30
|
+
trackBatch?: (p: Promise<unknown>) => void,
|
|
31
|
+
): QueueConsumer[] {
|
|
32
|
+
const handler = resolveQueueHandler(workerModule)
|
|
33
|
+
if (!handler) return []
|
|
34
|
+
const consumers: QueueConsumer[] = []
|
|
35
|
+
for (const cfg of config.queues?.consumers ?? []) {
|
|
36
|
+
const consumer = new QueueConsumer(
|
|
37
|
+
db,
|
|
38
|
+
{
|
|
39
|
+
queue: cfg.queue,
|
|
40
|
+
maxBatchSize: cfg.max_batch_size ?? 10,
|
|
41
|
+
maxBatchTimeout: cfg.max_batch_timeout ?? 5,
|
|
42
|
+
maxRetries: cfg.max_retries ?? 3,
|
|
43
|
+
deadLetterQueue: cfg.dead_letter_queue ?? null,
|
|
44
|
+
maxConcurrency: cfg.max_concurrency ?? null,
|
|
45
|
+
retryDelay: cfg.retry_delay ?? null,
|
|
46
|
+
},
|
|
47
|
+
handler,
|
|
48
|
+
env,
|
|
49
|
+
workerName,
|
|
50
|
+
undefined,
|
|
51
|
+
trackBatch,
|
|
52
|
+
)
|
|
53
|
+
consumer.start()
|
|
54
|
+
consumers.push(consumer)
|
|
55
|
+
}
|
|
56
|
+
return consumers
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Wrap whatever the user returns into the QueueHandler signature (`Promise<void>`). */
|
|
60
|
+
function resolveQueueHandler(workerModule: Record<string, unknown>): ((batch: unknown, env: unknown, ctx: unknown) => Promise<void>) | null {
|
|
61
|
+
const def = workerModule.default
|
|
62
|
+
if (typeof def === 'function' && def.prototype) {
|
|
63
|
+
const proto = def.prototype as Record<string, unknown>
|
|
64
|
+
if (typeof proto.queue !== 'function') return null
|
|
65
|
+
// Class-based: construct a fresh instance per batch — same per-batch
|
|
66
|
+
// construction `resolveHandler` does for fetch/scheduled/email in entry.ts.
|
|
67
|
+
const Ctor = def as new(ctx: unknown, env: unknown) => Record<string, (...a: unknown[]) => Promise<unknown>>
|
|
68
|
+
return async (batch, env, ctx) => {
|
|
69
|
+
const instance = new Ctor(ctx, env)
|
|
70
|
+
await instance.queue!(batch, env, ctx)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const obj = def as Record<string, unknown> | null | undefined
|
|
74
|
+
const queueFn = obj?.queue
|
|
75
|
+
if (typeof queueFn !== 'function') return null
|
|
76
|
+
const fn = queueFn as (batch: unknown, env: unknown, ctx: unknown) => Promise<unknown>
|
|
77
|
+
return async (batch, env, ctx) => {
|
|
78
|
+
await fn.call(obj, batch, env, ctx)
|
|
79
|
+
}
|
|
80
|
+
}
|