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.
Files changed (109) hide show
  1. package/dist/types/api/dispatch.d.ts +2 -2
  2. package/dist/types/api/handlers/workflows.d.ts +2 -2
  3. package/dist/types/bindings/container-cleanup.d.ts +43 -0
  4. package/dist/types/bindings/container-docker.d.ts +24 -0
  5. package/dist/types/bindings/container.d.ts +4 -2
  6. package/dist/types/bindings/do-executor-worker.d.ts +73 -89
  7. package/dist/types/bindings/do-executor.d.ts +22 -1
  8. package/dist/types/bindings/do-worker-entry.d.ts +1 -1
  9. package/dist/types/bindings/do-worker-env.d.ts +16 -7
  10. package/dist/types/bindings/durable-object.d.ts +66 -4
  11. package/dist/types/bindings/queue.d.ts +4 -1
  12. package/dist/types/bindings/rpc-stub.d.ts +23 -1
  13. package/dist/types/bindings/scheduled.d.ts +13 -3
  14. package/dist/types/bindings/service-binding.d.ts +13 -4
  15. package/dist/types/bindings/static-assets.d.ts +1 -1
  16. package/dist/types/bindings/websocket-pair.d.ts +12 -3
  17. package/dist/types/bindings/workflow.d.ts +29 -0
  18. package/dist/types/config.d.ts +2 -0
  19. package/dist/types/env.d.ts +6 -0
  20. package/dist/types/error-page-render.d.ts +6 -0
  21. package/dist/types/execution-context.d.ts +8 -0
  22. package/dist/types/generation-manager.d.ts +21 -2
  23. package/dist/types/generation.d.ts +16 -21
  24. package/dist/types/import-graph.d.ts +28 -0
  25. package/dist/types/lopata-config.d.ts +3 -3
  26. package/dist/types/plugin.d.ts +4 -1
  27. package/dist/types/rpc-validate.d.ts +9 -0
  28. package/dist/types/setup-globals.d.ts +5 -1
  29. package/dist/types/tracing/context.d.ts +12 -0
  30. package/dist/types/tracing/span.d.ts +21 -3
  31. package/dist/types/tracing/store.d.ts +17 -0
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/dist/types/vite-plugin/index.d.ts +8 -1
  34. package/dist/types/worker-registry.d.ts +11 -4
  35. package/dist/types/worker-thread/do-protocol.d.ts +256 -0
  36. package/dist/types/worker-thread/entry.d.ts +2 -0
  37. package/dist/types/worker-thread/execution-context.d.ts +22 -0
  38. package/dist/types/worker-thread/executor.d.ts +123 -0
  39. package/dist/types/worker-thread/protocol.d.ts +553 -0
  40. package/dist/types/worker-thread/remote-trace-store.d.ts +27 -0
  41. package/dist/types/worker-thread/request-clone-fix.d.ts +24 -0
  42. package/dist/types/worker-thread/rpc-client.d.ts +4 -0
  43. package/dist/types/worker-thread/rpc-shared.d.ts +138 -0
  44. package/dist/types/worker-thread/serialize.d.ts +12 -0
  45. package/dist/types/worker-thread/stream-shared.d.ts +147 -0
  46. package/dist/types/worker-thread/thread-env.d.ts +42 -0
  47. package/dist/types/worker-thread/wire-handlers.d.ts +16 -0
  48. package/dist/types/worker-thread/ws-bridge-shared.d.ts +163 -0
  49. package/package.json +1 -1
  50. package/src/api/handlers/containers.ts +2 -1
  51. package/src/api/handlers/workflows.ts +39 -34
  52. package/src/bindings/container-cleanup.ts +125 -0
  53. package/src/bindings/container-docker.ts +49 -34
  54. package/src/bindings/container.ts +24 -9
  55. package/src/bindings/do-executor-inprocess.ts +9 -5
  56. package/src/bindings/do-executor-worker.ts +386 -158
  57. package/src/bindings/do-executor.ts +23 -1
  58. package/src/bindings/do-worker-entry.ts +243 -60
  59. package/src/bindings/do-worker-env.ts +296 -11
  60. package/src/bindings/durable-object.ts +231 -27
  61. package/src/bindings/email.ts +6 -0
  62. package/src/bindings/queue.ts +11 -1
  63. package/src/bindings/rpc-stub.ts +79 -10
  64. package/src/bindings/scheduled.ts +37 -30
  65. package/src/bindings/service-binding.ts +87 -33
  66. package/src/bindings/static-assets.ts +3 -2
  67. package/src/bindings/websocket-pair.ts +19 -3
  68. package/src/bindings/workflow.ts +91 -0
  69. package/src/cli/dev.ts +106 -41
  70. package/src/config.ts +6 -3
  71. package/src/db.ts +1 -0
  72. package/src/env.ts +40 -21
  73. package/src/error-page-render.ts +21 -0
  74. package/src/execution-context.ts +13 -3
  75. package/src/generation-manager.ts +144 -143
  76. package/src/generation.ts +150 -306
  77. package/src/import-graph.ts +140 -0
  78. package/src/lopata-config.ts +3 -3
  79. package/src/plugin.ts +7 -0
  80. package/src/rpc-validate.ts +29 -0
  81. package/src/setup-globals.ts +6 -17
  82. package/src/testing/durable-object.ts +5 -3
  83. package/src/testing/index.ts +8 -4
  84. package/src/tracing/context.ts +18 -0
  85. package/src/tracing/span.ts +83 -57
  86. package/src/tracing/store.ts +41 -3
  87. package/src/virtual-modules.ts +2 -0
  88. package/src/vite-plugin/dev-server-plugin.ts +4 -0
  89. package/src/vite-plugin/index.ts +8 -1
  90. package/src/worker-registry.ts +15 -2
  91. package/src/worker-thread/do-protocol.ts +237 -0
  92. package/src/worker-thread/entry.ts +454 -0
  93. package/src/worker-thread/execution-context.ts +53 -0
  94. package/src/worker-thread/executor.ts +595 -0
  95. package/src/worker-thread/protocol.ts +552 -0
  96. package/src/worker-thread/remote-trace-store.ts +90 -0
  97. package/src/worker-thread/request-clone-fix.ts +69 -0
  98. package/src/worker-thread/rpc-client.ts +5 -0
  99. package/src/worker-thread/rpc-shared.ts +503 -0
  100. package/src/worker-thread/serialize.ts +37 -0
  101. package/src/worker-thread/stream-shared.ts +414 -0
  102. package/src/worker-thread/thread-env.ts +350 -0
  103. package/src/worker-thread/wire-handlers.ts +80 -0
  104. package/src/worker-thread/ws-bridge-shared.ts +482 -0
  105. package/dist/types/bindings/do-websocket-bridge.d.ts +0 -60
  106. package/dist/types/module-cache.d.ts +0 -23
  107. package/src/bindings/do-websocket-bridge.ts +0 -79
  108. package/src/module-cache.ts +0 -58
  109. 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
+ }