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
package/src/cli/dev.ts
CHANGED
|
@@ -6,6 +6,13 @@ process.on('unhandledRejection', (reason) => {
|
|
|
6
6
|
console.error('[lopata] Unhandled promise rejection:', reason)
|
|
7
7
|
})
|
|
8
8
|
|
|
9
|
+
// Last-resort guard so a synchronous throw on main (e.g. a worker message
|
|
10
|
+
// handler choking on a non-serializable user value) logs instead of killing the
|
|
11
|
+
// whole dev server — every worker, the dashboard and the file watcher with it.
|
|
12
|
+
process.on('uncaughtException', (err) => {
|
|
13
|
+
console.error('[lopata] Uncaught exception (dev server kept alive):', err)
|
|
14
|
+
})
|
|
15
|
+
|
|
9
16
|
import '../plugin'
|
|
10
17
|
import path from 'node:path'
|
|
11
18
|
import {
|
|
@@ -17,14 +24,16 @@ import {
|
|
|
17
24
|
setRouteDispatcher,
|
|
18
25
|
setWorkerRegistry,
|
|
19
26
|
} from '../api'
|
|
27
|
+
import { reapOrphanContainers } from '../bindings/container-cleanup'
|
|
20
28
|
import { QueuePullConsumer } from '../bindings/queue'
|
|
21
29
|
import type { AckRequest, PullRequest } from '../bindings/queue'
|
|
22
30
|
import { CFWebSocket } from '../bindings/websocket-pair'
|
|
23
|
-
import { autoLoadConfig, loadConfig } from '../config'
|
|
31
|
+
import { autoLoadConfig, findConfigPath, loadConfig } from '../config'
|
|
24
32
|
import { handleDashboardRequest } from '../dashboard-serve'
|
|
25
33
|
import { getDatabase } from '../db'
|
|
26
34
|
import { FileWatcher } from '../file-watcher'
|
|
27
35
|
import { GenerationManager } from '../generation-manager'
|
|
36
|
+
import { ImportGraphWatcher } from '../import-graph'
|
|
28
37
|
import { loadLopataConfig } from '../lopata-config'
|
|
29
38
|
import { addCfProperty } from '../request-cf'
|
|
30
39
|
import { extractHostname, RouteDispatcher } from '../route-matcher'
|
|
@@ -55,7 +64,14 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
55
64
|
const portFlag = values.port
|
|
56
65
|
|
|
57
66
|
const baseDir = process.cwd()
|
|
58
|
-
const watchers:
|
|
67
|
+
const watchers: { stop(): void }[] = []
|
|
68
|
+
|
|
69
|
+
// Reap orphan containers left behind by previous lopata runs that crashed
|
|
70
|
+
// or were `kill -9`'d before the exit handler fired. Silently does nothing
|
|
71
|
+
// if docker isn't installed or nothing matches the label filter.
|
|
72
|
+
reapOrphanContainers().then(count => {
|
|
73
|
+
if (count > 0) console.log(`[lopata] Reaped ${count} orphan container(s) from previous run(s)`)
|
|
74
|
+
}).catch(() => {})
|
|
59
75
|
|
|
60
76
|
// Try to load lopata.config.ts for multi-worker mode
|
|
61
77
|
const lopataConfig = await loadLopataConfig(baseDir)
|
|
@@ -69,16 +85,22 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
69
85
|
console.log('[lopata] Multi-worker mode (lopata.config.ts found)')
|
|
70
86
|
setLopataConfig(lopataConfig)
|
|
71
87
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
if (lopataConfig.isolation) {
|
|
89
|
+
if (lopataConfig.isolation === 'dev' || lopataConfig.isolation === 'isolated') {
|
|
90
|
+
console.warn(
|
|
91
|
+
`[lopata] "isolation" is deprecated and has no effect — workers always run in their own Bun Worker thread. Remove it from lopata.config.ts.`,
|
|
92
|
+
)
|
|
93
|
+
} else {
|
|
94
|
+
console.warn(`[lopata] Unknown isolation mode "${lopataConfig.isolation}" — has no effect`)
|
|
95
|
+
}
|
|
80
96
|
}
|
|
81
|
-
|
|
97
|
+
// DOs always run in worker threads now — main thread can't host the user
|
|
98
|
+
// classes since the worker entry imports user code in its own thread.
|
|
99
|
+
// Each manager gets its OWN factory instance: the factory carries the
|
|
100
|
+
// owning worker's module/config path (set via configure() on every
|
|
101
|
+
// reload). A single shared instance would be clobbered by the
|
|
102
|
+
// last-reloaded worker, so aux-worker DOs would spawn with the wrong
|
|
103
|
+
// worker's module — see generation-manager._doReload().
|
|
82
104
|
registry = new WorkerRegistry()
|
|
83
105
|
|
|
84
106
|
// Load main worker config
|
|
@@ -92,7 +114,7 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
92
114
|
workerRegistry: registry,
|
|
93
115
|
isMain: true,
|
|
94
116
|
cron: lopataConfig.cron,
|
|
95
|
-
executorFactory,
|
|
117
|
+
executorFactory: await makeExecutorFactory(),
|
|
96
118
|
configPath: lopataConfig.main,
|
|
97
119
|
browserConfig: lopataConfig.browser,
|
|
98
120
|
})
|
|
@@ -111,7 +133,7 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
111
133
|
workerRegistry: registry,
|
|
112
134
|
isMain: false,
|
|
113
135
|
cron: lopataConfig.cron,
|
|
114
|
-
executorFactory,
|
|
136
|
+
executorFactory: await makeExecutorFactory(),
|
|
115
137
|
configPath: workerDef.config,
|
|
116
138
|
})
|
|
117
139
|
registry.register(workerDef.name, auxManager)
|
|
@@ -124,11 +146,15 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
124
146
|
console.error(`[lopata] Failed to load auxiliary worker "${workerDef.name}":`, err)
|
|
125
147
|
}
|
|
126
148
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
149
|
+
// Import-graph watcher for the aux worker — follows its transitive
|
|
150
|
+
// imports (matching single-worker mode), not just the entry's directory.
|
|
151
|
+
// `watchExtra` remains the answer for non-import assets / shared dirs.
|
|
152
|
+
const auxEntry = path.resolve(auxBaseDir, auxConfig.main)
|
|
153
|
+
const auxWatcher: ImportGraphWatcher = new ImportGraphWatcher(auxEntry, auxBaseDir, () => {
|
|
154
|
+
const onSettled = () => auxWatcher.rescan()
|
|
130
155
|
auxManager.reload().then(async gen => {
|
|
131
|
-
|
|
156
|
+
onSettled()
|
|
157
|
+
console.log(`[lopata] Auxiliary worker "${workerDef.name}" reloaded → generation ${gen.id} (watching ${auxWatcher.size} files)`)
|
|
132
158
|
// Re-read config and update routes in case routes changed
|
|
133
159
|
if (routeDispatcher) {
|
|
134
160
|
try {
|
|
@@ -138,13 +164,14 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
138
164
|
console.warn(`[lopata] Failed to re-read config for "${workerDef.name}" routes:`, err)
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
|
-
}
|
|
167
|
+
}, err => {
|
|
168
|
+
onSettled()
|
|
142
169
|
console.error(`[lopata] Reload failed for "${workerDef.name}":`, err)
|
|
143
170
|
})
|
|
144
171
|
})
|
|
145
172
|
auxWatcher.start()
|
|
146
173
|
watchers.push(auxWatcher)
|
|
147
|
-
console.log(`[lopata] Watching ${
|
|
174
|
+
console.log(`[lopata] Watching ${auxWatcher.size} imported files for changes (${workerDef.name})`)
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
// Load main worker after aux workers
|
|
@@ -205,15 +232,24 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
205
232
|
setWorkerRegistry(registry)
|
|
206
233
|
setRouteDispatcher(routeDispatcher)
|
|
207
234
|
|
|
208
|
-
//
|
|
209
|
-
const
|
|
235
|
+
// Import-graph watcher for the main worker (follows its transitive imports).
|
|
236
|
+
const mainEntry = path.resolve(mainBaseDir, mainConfig.main)
|
|
210
237
|
const reloadMain = makeReloadCallback(mainManager, 'Main worker reloaded')
|
|
211
|
-
const mainWatcher = new
|
|
238
|
+
const mainWatcher: ImportGraphWatcher = new ImportGraphWatcher(mainEntry, mainBaseDir, () => {
|
|
239
|
+
mainManager.reload().then(gen => {
|
|
240
|
+
mainWatcher.rescan()
|
|
241
|
+
console.log(`[lopata] Main worker reloaded → generation ${gen.id} (watching ${mainWatcher.size} files)`)
|
|
242
|
+
}, err => {
|
|
243
|
+
mainWatcher.rescan()
|
|
244
|
+
console.error('[lopata] Reload failed:', err)
|
|
245
|
+
})
|
|
246
|
+
})
|
|
212
247
|
mainWatcher.start()
|
|
213
248
|
watchers.push(mainWatcher)
|
|
214
|
-
console.log(`[lopata] Watching ${
|
|
249
|
+
console.log(`[lopata] Watching ${mainWatcher.size} imported files for changes (main)`)
|
|
215
250
|
|
|
216
|
-
// Extra directories that should also trigger a main reload (e.g. shared
|
|
251
|
+
// Extra directories that should also trigger a main reload (e.g. shared
|
|
252
|
+
// monorepo packages / non-import assets the static scan won't follow).
|
|
217
253
|
for (const extraDir of lopataConfig.watchExtra ?? []) {
|
|
218
254
|
const extraWatcher = new FileWatcher(extraDir, reloadMain)
|
|
219
255
|
extraWatcher.start()
|
|
@@ -221,22 +257,52 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
221
257
|
console.log(`[lopata] Watching ${extraDir} for changes (extra)`)
|
|
222
258
|
}
|
|
223
259
|
} else {
|
|
224
|
-
// ─── Single-worker mode
|
|
260
|
+
// ─── Single-worker mode ────────────────────────────────────────
|
|
225
261
|
const config = await autoLoadConfig(baseDir, envFlag)
|
|
226
262
|
console.log(`[lopata] Loaded config: ${config.name}${envFlag ? ` (env: ${envFlag})` : ''}`)
|
|
227
263
|
setDashboardConfig(config)
|
|
228
264
|
|
|
229
|
-
|
|
265
|
+
const executorFactory = await makeExecutorFactory()
|
|
266
|
+
// Even in single-worker mode we need a registry so service bindings that
|
|
267
|
+
// reference the worker by name (self-bindings) resolve to its own
|
|
268
|
+
// thread executor instead of an empty `workerModule`.
|
|
269
|
+
registry = new WorkerRegistry()
|
|
270
|
+
manager = new GenerationManager(config, baseDir, {
|
|
271
|
+
workerName: config.name,
|
|
272
|
+
workerRegistry: registry,
|
|
273
|
+
isMain: true,
|
|
274
|
+
executorFactory,
|
|
275
|
+
configPath: findConfigPath(baseDir),
|
|
276
|
+
})
|
|
277
|
+
registry.register(config.name, manager, true)
|
|
230
278
|
const firstGen = await manager.reload()
|
|
231
279
|
console.log(`[lopata] Generation ${firstGen.id} loaded`)
|
|
232
280
|
setGenerationManager(manager)
|
|
281
|
+
setWorkerRegistry(registry)
|
|
233
282
|
|
|
234
|
-
// File watcher —
|
|
235
|
-
|
|
236
|
-
|
|
283
|
+
// File watcher — follow the worker's actual import graph, not just the
|
|
284
|
+
// entry's directory. This catches edits to code imported from elsewhere in
|
|
285
|
+
// the project (e.g. an entry at `workers/app.ts` importing `../app/**`),
|
|
286
|
+
// which a dir-only watcher would miss. A reload re-imports the whole graph,
|
|
287
|
+
// so after each one we rescan to pick up newly-added imports.
|
|
288
|
+
const entry = path.resolve(baseDir, config.main)
|
|
289
|
+
const watcher = new ImportGraphWatcher(entry, baseDir, () => {
|
|
290
|
+
manager.reload().then(gen => {
|
|
291
|
+
watcher.rescan()
|
|
292
|
+
console.log(`[lopata] Reloaded → generation ${gen.id} (watching ${watcher.size} files)`)
|
|
293
|
+
}, err => {
|
|
294
|
+
// Rescan even on a FAILED reload: a newly-added import whose first
|
|
295
|
+
// build failed (it referenced a file with a syntax error), or a
|
|
296
|
+
// delete-then-recreate, must still enter the watch set — otherwise
|
|
297
|
+
// fixing that file produces no event and the dev server stays broken
|
|
298
|
+
// until an already-watched file is re-touched.
|
|
299
|
+
watcher.rescan()
|
|
300
|
+
console.error('[lopata] Reload failed:', err)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
237
303
|
watcher.start()
|
|
238
304
|
watchers.push(watcher)
|
|
239
|
-
console.log(`[lopata] Watching ${
|
|
305
|
+
console.log(`[lopata] Watching ${watcher.size} imported files for changes`)
|
|
240
306
|
}
|
|
241
307
|
|
|
242
308
|
// Start server — one Bun.serve(), delegates to active generation
|
|
@@ -451,11 +517,10 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
451
517
|
}
|
|
452
518
|
|
|
453
519
|
const cfSocket = (data as { cfSocket: CFWebSocket }).cfSocket
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
520
|
+
cfSocket._peer?.dispatchOrQueue({
|
|
521
|
+
type: 'message',
|
|
522
|
+
data: typeof message === 'string' ? message : message.buffer as ArrayBuffer,
|
|
523
|
+
})
|
|
459
524
|
},
|
|
460
525
|
close(ws, code, reason) {
|
|
461
526
|
const data = ws.data as unknown as Record<string, unknown>
|
|
@@ -470,12 +535,7 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
470
535
|
|
|
471
536
|
const cfSocket = (data as { cfSocket: CFWebSocket }).cfSocket
|
|
472
537
|
if (cfSocket._peer && cfSocket._peer.readyState !== 3 /* CLOSED */) {
|
|
473
|
-
|
|
474
|
-
if (cfSocket._peer._accepted) {
|
|
475
|
-
cfSocket._peer._dispatchWSEvent(evt)
|
|
476
|
-
} else {
|
|
477
|
-
cfSocket._peer._eventQueue.push(evt)
|
|
478
|
-
}
|
|
538
|
+
cfSocket._peer.dispatchOrQueue({ type: 'close', code: code ?? 1000, reason: reason ?? '', wasClean: true })
|
|
479
539
|
cfSocket._peer.readyState = 3
|
|
480
540
|
}
|
|
481
541
|
cfSocket.readyState = 3
|
|
@@ -521,6 +581,11 @@ function resolveWorkerParam(url: URL, registry: WorkerRegistry | undefined, fall
|
|
|
521
581
|
return target
|
|
522
582
|
}
|
|
523
583
|
|
|
584
|
+
async function makeExecutorFactory(): Promise<import('../bindings/do-executor').DOExecutorFactory> {
|
|
585
|
+
const { WorkerExecutorFactory } = await import('../bindings/do-executor-worker')
|
|
586
|
+
return new WorkerExecutorFactory()
|
|
587
|
+
}
|
|
588
|
+
|
|
524
589
|
function matchGlob(text: string, pattern: string): boolean {
|
|
525
590
|
// Placeholder approach: protect ** before escaping special chars
|
|
526
591
|
const regex = pattern
|
package/src/config.ts
CHANGED
|
@@ -93,12 +93,15 @@ export async function loadConfig(path: string, envName?: string): Promise<Wrangl
|
|
|
93
93
|
* Auto-detect config file in a directory. Tries wrangler.jsonc, wrangler.json, wrangler.toml.
|
|
94
94
|
*/
|
|
95
95
|
export async function autoLoadConfig(baseDir: string, envName?: string): Promise<WranglerConfig> {
|
|
96
|
+
return loadConfig(findConfigPath(baseDir), envName)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Resolve the wrangler config path under `baseDir` (jsonc | json | toml). */
|
|
100
|
+
export function findConfigPath(baseDir: string): string {
|
|
96
101
|
const candidates = ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']
|
|
97
102
|
for (const name of candidates) {
|
|
98
103
|
const fullPath = join(baseDir, name)
|
|
99
|
-
if (existsSync(fullPath))
|
|
100
|
-
return loadConfig(fullPath, envName)
|
|
101
|
-
}
|
|
104
|
+
if (existsSync(fullPath)) return fullPath
|
|
102
105
|
}
|
|
103
106
|
throw new Error(`No wrangler config found in ${baseDir} (tried: ${candidates.join(', ')})`)
|
|
104
107
|
}
|
package/src/db.ts
CHANGED
package/src/env.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { AiBinding } from './bindings/ai'
|
|
|
4
4
|
import { SqliteAnalyticsEngine } from './bindings/analytics-engine'
|
|
5
5
|
import { BrowserBinding } from './bindings/browser'
|
|
6
6
|
import { ContainerBase } from './bindings/container'
|
|
7
|
+
import { containerLabels, registerContainer, unregisterContainer } from './bindings/container-cleanup'
|
|
7
8
|
import { DockerManager } from './bindings/container-docker'
|
|
8
9
|
import { openD1Database } from './bindings/d1'
|
|
9
10
|
import type { DOExecutorFactory } from './bindings/do-executor'
|
|
@@ -17,11 +18,11 @@ import { QueueConsumer, SqliteQueueProducer } from './bindings/queue'
|
|
|
17
18
|
import { FileR2Bucket } from './bindings/r2'
|
|
18
19
|
import { createServiceBinding } from './bindings/service-binding'
|
|
19
20
|
import { StaticAssets } from './bindings/static-assets'
|
|
20
|
-
import { SqliteWorkflowBinding } from './bindings/workflow'
|
|
21
|
+
import { SqliteWorkflowBinding, wireWorkflowClass } from './bindings/workflow'
|
|
21
22
|
import type { WranglerConfig } from './config'
|
|
22
23
|
import { getDatabase, getDataDir } from './db'
|
|
23
24
|
import { instrumentBinding, instrumentD1, instrumentDONamespace, instrumentServiceBinding } from './tracing/instrument'
|
|
24
|
-
import type { WorkerRegistry } from './worker-registry'
|
|
25
|
+
import type { ResolvedTarget, WorkerRegistry } from './worker-registry'
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Global reference to the built env object. Used by cloudflare:workers `env` export.
|
|
@@ -313,10 +314,14 @@ export function buildEnv(
|
|
|
313
314
|
console.log(`[lopata] Container: ${container.class_name} (reusing DO binding, image: ${container.image})`)
|
|
314
315
|
}
|
|
315
316
|
} else {
|
|
316
|
-
//
|
|
317
|
+
// Reuse the existing namespace across reloads (like the DO-binding branch
|
|
318
|
+
// above) — constructing a fresh one would orphan the old namespace while a
|
|
319
|
+
// new executor opens the same do-sql file for the same id, breaking DO
|
|
320
|
+
// single-threading.
|
|
317
321
|
const bindingName = container.name ?? container.class_name
|
|
318
322
|
console.log(`[lopata] Container: ${bindingName} -> ${container.class_name} (image: ${container.image})`)
|
|
319
|
-
const
|
|
323
|
+
const existingNs = existingNamespaces?.get(container.class_name)
|
|
324
|
+
const namespace = existingNs ?? new DurableObjectNamespaceImpl(db, container.class_name, getDataDir(), undefined, executorFactory)
|
|
320
325
|
env[bindingName] = instrumentDONamespace(namespace, container.class_name)
|
|
321
326
|
registry.durableObjects.push({
|
|
322
327
|
bindingName,
|
|
@@ -386,15 +391,17 @@ export function wireClassRefs(
|
|
|
386
391
|
}
|
|
387
392
|
|
|
388
393
|
for (const entry of registry.workflows) {
|
|
389
|
-
|
|
390
|
-
if (!cls) throw new Error(`Workflow class "${entry.className}" not exported from worker module`)
|
|
391
|
-
entry.binding._setClass(cls as any, env)
|
|
392
|
-
entry.binding.resumeInterrupted()
|
|
394
|
+
wireWorkflowClass(entry.binding, entry.className, workerModule, env)
|
|
393
395
|
console.log(`[lopata] Wired Workflow class: ${entry.className}`)
|
|
394
396
|
}
|
|
395
397
|
|
|
396
|
-
// Wire container configs onto namespaces
|
|
397
|
-
|
|
398
|
+
// Wire container configs onto namespaces. Main-side path goes through the
|
|
399
|
+
// cleanup registry directly (no postMessage needed — this *is* main).
|
|
400
|
+
const dockerManager = new DockerManager({
|
|
401
|
+
onRegister: registerContainer,
|
|
402
|
+
onRemove: unregisterContainer,
|
|
403
|
+
labels: containerLabels(),
|
|
404
|
+
})
|
|
398
405
|
for (const entry of registry.containers) {
|
|
399
406
|
entry.namespace._setContainerConfig({
|
|
400
407
|
className: entry.className,
|
|
@@ -405,18 +412,30 @@ export function wireClassRefs(
|
|
|
405
412
|
console.log(`[lopata] Wired container config: ${entry.className} (image: ${entry.image})`)
|
|
406
413
|
}
|
|
407
414
|
|
|
408
|
-
|
|
415
|
+
wireServiceBindings(registry, workerModule, env, workerRegistry)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Service-binding wiring extracted so thread-mode generations (which skip
|
|
420
|
+
* `wireClassRefs` entirely — DO/Workflow classes live in the worker) can
|
|
421
|
+
* still resolve cross-worker fetches through the registry.
|
|
422
|
+
*/
|
|
423
|
+
export function wireServiceBindings(
|
|
424
|
+
registry: ClassRegistry,
|
|
425
|
+
workerModule: Record<string, unknown>,
|
|
426
|
+
env: Record<string, unknown>,
|
|
427
|
+
workerRegistry?: WorkerRegistry,
|
|
428
|
+
) {
|
|
409
429
|
for (const entry of registry.serviceBindings) {
|
|
410
|
-
const wire = entry.proxy._wire as ((resolver: () =>
|
|
411
|
-
if (wire)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
console.log(`[lopata] Wired service binding: ${entry.bindingName} -> ${entry.serviceName}${entry.entrypoint ? ` (${entry.entrypoint})` : ''}`)
|
|
430
|
+
const wire = entry.proxy._wire as ((resolver: () => ResolvedTarget) => void) | undefined
|
|
431
|
+
if (!wire) continue
|
|
432
|
+
if (workerRegistry) {
|
|
433
|
+
// Resolve through registry (handles both self-ref and cross-worker)
|
|
434
|
+
wire(() => workerRegistry.resolveTarget(entry.serviceName))
|
|
435
|
+
} else {
|
|
436
|
+
// Backward compat: self-reference, in-process
|
|
437
|
+
wire(() => ({ kind: 'in-process', workerModule, env }))
|
|
420
438
|
}
|
|
439
|
+
console.log(`[lopata] Wired service binding: ${entry.bindingName} -> ${entry.serviceName}${entry.entrypoint ? ` (${entry.entrypoint})` : ''}`)
|
|
421
440
|
}
|
|
422
441
|
}
|
package/src/error-page-render.ts
CHANGED
|
@@ -150,6 +150,27 @@ function extractBindings(config: WranglerConfig): { name: string; type: string }
|
|
|
150
150
|
|
|
151
151
|
// ─── Public API ──────────────────────────────────────────────────────────
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Stitch a captured caller stack onto a short async error so dev-time stacks
|
|
155
|
+
* show where the failing call originated. Mutates `err.stack` in place; no-op
|
|
156
|
+
* if either side is missing, already stitched, or `err.stack` looks deep.
|
|
157
|
+
*/
|
|
158
|
+
export function stitchAsyncStack(err: Error, callerError: Error | null): void {
|
|
159
|
+
if (!callerError) return
|
|
160
|
+
if (!err.stack || !callerError.stack) return
|
|
161
|
+
if (err.stack.includes('--- async ---')) return
|
|
162
|
+
|
|
163
|
+
const errFrames = err.stack.split('\n').filter(l => l.trim().startsWith('at '))
|
|
164
|
+
const looksShort = errFrames.length <= 5 || err.stack.includes('processTicksAndRejections')
|
|
165
|
+
if (!looksShort) return
|
|
166
|
+
|
|
167
|
+
const callerLines = callerError.stack.split('\n').slice(1)
|
|
168
|
+
const filtered = callerLines.filter(l => !l.includes('/lopata/src/'))
|
|
169
|
+
if (filtered.length === 0) return
|
|
170
|
+
|
|
171
|
+
err.stack += '\n --- async ---\n' + filtered.join('\n')
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
export async function renderErrorPage(
|
|
154
175
|
error: unknown,
|
|
155
176
|
request: Request,
|
package/src/execution-context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { tracing } from './tracing/span'
|
|
2
3
|
|
|
3
4
|
const storage = new AsyncLocalStorage<ExecutionContext>()
|
|
4
5
|
|
|
@@ -10,18 +11,27 @@ export function runWithExecutionContext<T>(ctx: ExecutionContext, fn: () => T):
|
|
|
10
11
|
return storage.run(ctx, fn)
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/** Swallow + log a `waitUntil` rejection. Single source of truth for the log string.
|
|
15
|
+
* Coerces non-thenables (CF tolerates `ctx.waitUntil(undefined)`) — a synchronous
|
|
16
|
+
* throw here would strand the wait-until-settle accounting on the worker side. */
|
|
17
|
+
export function logIfRejected(promise: Promise<unknown>): Promise<unknown> {
|
|
18
|
+
return Promise.resolve(promise).catch(err => {
|
|
19
|
+
console.error('[lopata] waitUntil promise rejected:', err)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export class ExecutionContext {
|
|
14
24
|
private _promises: Promise<unknown>[] = []
|
|
15
25
|
readonly props: Record<string, unknown>
|
|
26
|
+
/** Cloudflare-compatible custom span API: `ctx.tracing.enterSpan(...)`. */
|
|
27
|
+
readonly tracing = tracing
|
|
16
28
|
|
|
17
29
|
constructor(props?: Record<string, unknown>) {
|
|
18
30
|
this.props = props ?? {}
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
waitUntil(promise: Promise<unknown>): void {
|
|
22
|
-
this._promises.push(promise
|
|
23
|
-
console.error('[lopata] waitUntil promise rejected:', err)
|
|
24
|
-
}))
|
|
34
|
+
this._promises.push(logIfRejected(promise))
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
passThroughOnException(): void {
|