lopata 0.7.0 → 0.8.2
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/dashboard/{chunk-pqnphvm2.css → chunk-csyd2tq2.css} +27 -0
- package/dist/dashboard/{chunk-5nxa3jfc.js → chunk-yxzrcvyh.js} +364 -3
- package/dist/dashboard/index.html +1 -1
- package/package.json +5 -3
- package/src/api/handlers/generations.ts +19 -5
- package/src/api/types.ts +14 -0
- package/src/bindings/cache.ts +14 -8
- package/src/bindings/durable-object.ts +80 -21
- package/src/bindings/kv.ts +12 -8
- package/src/bindings/queue.ts +22 -12
- package/src/bindings/workflow.ts +332 -25
- package/src/env.ts +3 -2
- package/src/file-watcher.ts +59 -32
- package/src/generation-manager.ts +6 -1
- package/src/generation.ts +15 -3
- package/src/plugin.ts +2 -90
- package/src/setup-globals.ts +23 -21
- package/src/testing/clock.ts +26 -0
- package/src/testing/durable-object.ts +325 -0
- package/src/testing/env-builder.ts +126 -0
- package/src/testing/fetch-mock.ts +145 -0
- package/src/testing/index.ts +288 -0
- package/src/testing/setup.ts +68 -0
- package/src/testing/types.ts +68 -0
- package/src/testing/workflow.ts +323 -0
- package/src/tracing/store.ts +6 -0
- package/src/tracing/types.ts +1 -0
- package/src/virtual-modules.ts +99 -0
- package/src/vite-plugin/config-plugin.ts +2 -0
- package/src/vite-plugin/dev-server-plugin.ts +159 -56
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
import type { Plugin, ViteDevServer } from 'vite'
|
|
4
|
+
import { FileWatcher } from '../file-watcher.ts'
|
|
4
5
|
|
|
5
6
|
interface DevServerPluginOptions {
|
|
6
7
|
configPath?: string
|
|
@@ -52,6 +53,11 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
52
53
|
let currentModule: Record<string, unknown> | null = null
|
|
53
54
|
// Serializes module reload — prevents concurrent wireClassRefs calls
|
|
54
55
|
let reloadLock: Promise<void> | null = null
|
|
56
|
+
// Generation counter — increments on each module reload for tracing
|
|
57
|
+
let currentGenerationId = 0
|
|
58
|
+
// Track generation records for dashboard visibility
|
|
59
|
+
const viteGenerations = new Map<number, { id: number; createdAt: number; state: 'active' | 'stopped' }>()
|
|
60
|
+
const genActiveRequests = new Map<number, number>()
|
|
55
61
|
|
|
56
62
|
/**
|
|
57
63
|
* Import the worker module through Vite's SSR runner and re-wire
|
|
@@ -81,15 +87,39 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
81
87
|
reloadLock = new Promise(r => {
|
|
82
88
|
resolveReload = r
|
|
83
89
|
})
|
|
90
|
+
const previousModule = currentModule
|
|
91
|
+
const previousGenId = currentGenerationId
|
|
84
92
|
try {
|
|
85
93
|
currentModule = workerModule
|
|
86
|
-
|
|
94
|
+
// Track generation lifecycle
|
|
95
|
+
if (viteGenerations.has(previousGenId)) {
|
|
96
|
+
viteGenerations.get(previousGenId)!.state = 'stopped'
|
|
97
|
+
}
|
|
98
|
+
currentGenerationId++
|
|
99
|
+
viteGenerations.set(currentGenerationId, { id: currentGenerationId, createdAt: Date.now(), state: 'active' })
|
|
100
|
+
wireClassRefs(registry, workerModule, env, workerRegistry, currentGenerationId)
|
|
87
101
|
setGlobalEnv(env)
|
|
88
|
-
console.log(
|
|
102
|
+
console.log(`[lopata:vite] Worker module (re)loaded, classes wired (generation ${currentGenerationId})`)
|
|
103
|
+
// Schedule cleanup of old generation after successful reload
|
|
104
|
+
if (viteGenerations.has(previousGenId)) {
|
|
105
|
+
setTimeout(() => viteGenerations.delete(previousGenId), 60_000)
|
|
106
|
+
}
|
|
89
107
|
} catch (err) {
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
// Revert generation tracking
|
|
109
|
+
viteGenerations.delete(currentGenerationId)
|
|
110
|
+
currentGenerationId = previousGenId
|
|
111
|
+
if (viteGenerations.has(previousGenId)) {
|
|
112
|
+
viteGenerations.get(previousGenId)!.state = 'active'
|
|
113
|
+
}
|
|
114
|
+
if (previousModule) {
|
|
115
|
+
// Serve old module while Vite module graph settles (e.g. DO class not yet re-exported)
|
|
116
|
+
currentModule = previousModule
|
|
117
|
+
console.warn('[lopata:vite] Module reload failed, serving previous version:', err instanceof Error ? err.message : err)
|
|
118
|
+
} else {
|
|
119
|
+
// First load — no fallback
|
|
120
|
+
currentModule = null
|
|
121
|
+
throw err
|
|
122
|
+
}
|
|
93
123
|
} finally {
|
|
94
124
|
reloadLock = null
|
|
95
125
|
resolveReload()
|
|
@@ -103,6 +133,14 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
103
133
|
return {
|
|
104
134
|
name: 'lopata:dev-server',
|
|
105
135
|
|
|
136
|
+
transform(code, id) {
|
|
137
|
+
if (!config) return
|
|
138
|
+
if (this.environment?.name !== options.envName) return
|
|
139
|
+
const entrypoint = resolve(server.config.root, config.main)
|
|
140
|
+
if (id !== entrypoint) return
|
|
141
|
+
return code + '\nif (import.meta.hot) { import.meta.hot.accept() }\n'
|
|
142
|
+
},
|
|
143
|
+
|
|
106
144
|
async configureServer(viteServer: ViteDevServer) {
|
|
107
145
|
server = viteServer
|
|
108
146
|
const projectRoot = server.config.root
|
|
@@ -167,6 +205,62 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
167
205
|
// 3. Set up API context
|
|
168
206
|
apiMod.setDashboardConfig(config)
|
|
169
207
|
|
|
208
|
+
// 3b. Create generation tracking adapter for dashboard
|
|
209
|
+
const mainAdapter = {
|
|
210
|
+
config,
|
|
211
|
+
gracePeriodMs: 0,
|
|
212
|
+
get active() {
|
|
213
|
+
return currentModule ? { workerModule: currentModule, env, registry } : null
|
|
214
|
+
},
|
|
215
|
+
list() {
|
|
216
|
+
return Array.from(viteGenerations.values()).map(g => ({
|
|
217
|
+
id: g.id,
|
|
218
|
+
state: g.state,
|
|
219
|
+
createdAt: g.createdAt,
|
|
220
|
+
activeRequests: genActiveRequests.get(g.id) ?? 0,
|
|
221
|
+
workerName: config.name,
|
|
222
|
+
durableObjects: g.state === 'active'
|
|
223
|
+
? registry.durableObjects.map((entry: any) => {
|
|
224
|
+
const executors = entry.namespace._listActiveExecutors()
|
|
225
|
+
return {
|
|
226
|
+
namespace: entry.className,
|
|
227
|
+
activeInstances: executors.length,
|
|
228
|
+
totalWebSockets: executors.reduce((sum: number, e: any) => sum + e.wsCount, 0),
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
: undefined,
|
|
232
|
+
}))
|
|
233
|
+
},
|
|
234
|
+
get(id: number) {
|
|
235
|
+
const record = viteGenerations.get(id)
|
|
236
|
+
if (!record) return null
|
|
237
|
+
return {
|
|
238
|
+
getInfo() {
|
|
239
|
+
return {
|
|
240
|
+
id: record.id,
|
|
241
|
+
state: record.state,
|
|
242
|
+
createdAt: record.createdAt,
|
|
243
|
+
activeRequests: genActiveRequests.get(record.id) ?? 0,
|
|
244
|
+
workerName: config.name,
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
registry,
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
reload() {
|
|
251
|
+
return Promise.reject(new Error('Main worker uses Vite HMR — save a file to trigger reload'))
|
|
252
|
+
},
|
|
253
|
+
stop(id: number) {
|
|
254
|
+
const record = viteGenerations.get(id)
|
|
255
|
+
if (record) {
|
|
256
|
+
record.state = 'stopped'
|
|
257
|
+
setTimeout(() => viteGenerations.delete(id), 60_000)
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
setGracePeriod() {},
|
|
261
|
+
}
|
|
262
|
+
apiMod.setGenerationManager(mainAdapter as any)
|
|
263
|
+
|
|
170
264
|
// 4. Set up auxiliary workers (if configured)
|
|
171
265
|
if (options.auxiliaryWorkers && options.auxiliaryWorkers.length > 0) {
|
|
172
266
|
await import('../plugin.ts')
|
|
@@ -175,17 +269,6 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
175
269
|
const { GenerationManager } = await import('../generation-manager.ts')
|
|
176
270
|
|
|
177
271
|
workerRegistry = new WorkerRegistry()
|
|
178
|
-
|
|
179
|
-
const mainAdapter = {
|
|
180
|
-
config,
|
|
181
|
-
gracePeriodMs: 0,
|
|
182
|
-
get active() {
|
|
183
|
-
return currentModule ? { workerModule: currentModule, env, registry } : null
|
|
184
|
-
},
|
|
185
|
-
list() {
|
|
186
|
-
return []
|
|
187
|
-
},
|
|
188
|
-
}
|
|
189
272
|
workerRegistry.register(config.name, mainAdapter as any, true)
|
|
190
273
|
|
|
191
274
|
for (const workerDef of options.auxiliaryWorkers) {
|
|
@@ -207,6 +290,18 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
207
290
|
} catch (err) {
|
|
208
291
|
console.error(`[lopata:vite] Failed to load auxiliary worker "${auxConfig.name}":`, err)
|
|
209
292
|
}
|
|
293
|
+
|
|
294
|
+
// File watcher for aux worker reload
|
|
295
|
+
const auxSrcDir = dirname(resolve(auxBaseDir, auxConfig.main))
|
|
296
|
+
const auxWatcher = new FileWatcher(auxSrcDir, () => {
|
|
297
|
+
auxManager.reload().then(gen => {
|
|
298
|
+
console.log(`[lopata:vite] Auxiliary worker "${auxConfig.name}" reloaded → generation ${gen.id}`)
|
|
299
|
+
}).catch(err => {
|
|
300
|
+
console.error(`[lopata:vite] Reload failed for "${auxConfig.name}":`, err)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
auxWatcher.start()
|
|
304
|
+
console.log(`[lopata:vite] Watching ${auxSrcDir} for changes (${auxConfig.name})`)
|
|
210
305
|
}
|
|
211
306
|
|
|
212
307
|
apiMod.setWorkerRegistry(workerRegistry)
|
|
@@ -262,53 +357,61 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
262
357
|
|
|
263
358
|
try {
|
|
264
359
|
const activeModule = await ensureWorkerModule()
|
|
360
|
+
const genId = currentGenerationId
|
|
361
|
+
genActiveRequests.set(genId, (genActiveRequests.get(genId) ?? 0) + 1)
|
|
265
362
|
|
|
266
|
-
|
|
267
|
-
|
|
363
|
+
try {
|
|
364
|
+
const request = nodeReqToRequest(req)
|
|
365
|
+
const parsedUrl = new URL(request.url)
|
|
268
366
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
367
|
+
const handler = activeModule.default as Record<string, unknown>
|
|
368
|
+
if (!handler || typeof handler.fetch !== 'function') {
|
|
369
|
+
console.error('[lopata:vite] Worker module default export has no fetch() method')
|
|
370
|
+
return next()
|
|
371
|
+
}
|
|
274
372
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
373
|
+
// Capture caller stack before entering the worker (for async stack stitching)
|
|
374
|
+
const callerStack = new Error()
|
|
375
|
+
|
|
376
|
+
const ctx = new ExecutionContext()
|
|
377
|
+
const response = await (startSpan as Function)({
|
|
378
|
+
name: `${request.method} ${parsedUrl.pathname}`,
|
|
379
|
+
kind: 'server',
|
|
380
|
+
attributes: { 'http.method': request.method, 'http.url': request.url, 'lopata.generation_id': genId },
|
|
381
|
+
}, () =>
|
|
382
|
+
runWithExecutionContext(ctx, async () => {
|
|
383
|
+
try {
|
|
384
|
+
const resp = await (handler.fetch as Function).call(handler, request, env, ctx) as Response
|
|
385
|
+
;(setSpanAttribute as Function)('http.status_code', resp.status)
|
|
386
|
+
|
|
387
|
+
// Intercept React Router error boundary responses with lopata error page
|
|
388
|
+
const routeError = (globalThis as any).__lopata_routeError
|
|
389
|
+
delete (globalThis as any).__lopata_routeError
|
|
390
|
+
if (routeError) {
|
|
391
|
+
if (routeError instanceof Error) {
|
|
392
|
+
stitchAsyncStack(routeError, callerStack)
|
|
393
|
+
}
|
|
394
|
+
console.error('[lopata:vite] Route error:\n' + (routeError instanceof Error ? routeError.stack : String(routeError)))
|
|
395
|
+
return (renderErrorPage as Function)(routeError, request, env, config)
|
|
295
396
|
}
|
|
296
|
-
console.error('[lopata:vite] Route error:\n' + (routeError instanceof Error ? routeError.stack : String(routeError)))
|
|
297
|
-
return (renderErrorPage as Function)(routeError, request, env, config)
|
|
298
|
-
}
|
|
299
397
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
398
|
+
ctx._awaitAll().catch(() => {})
|
|
399
|
+
return resp
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (err instanceof Error) {
|
|
402
|
+
stitchAsyncStack(err, callerStack)
|
|
403
|
+
}
|
|
404
|
+
console.error('[lopata:vite] Request error:\n' + (err instanceof Error ? err.stack : String(err)))
|
|
405
|
+
return (renderErrorPage as Function)(err, request, env, config)
|
|
305
406
|
}
|
|
306
|
-
|
|
307
|
-
return (renderErrorPage as Function)(err, request, env, config)
|
|
308
|
-
}
|
|
309
|
-
})) as Response
|
|
407
|
+
})) as Response
|
|
310
408
|
|
|
311
|
-
|
|
409
|
+
writeResponse(response, res)
|
|
410
|
+
} finally {
|
|
411
|
+
const count = genActiveRequests.get(genId) ?? 1
|
|
412
|
+
if (count <= 1) genActiveRequests.delete(genId)
|
|
413
|
+
else genActiveRequests.set(genId, count - 1)
|
|
414
|
+
}
|
|
312
415
|
} catch (err) {
|
|
313
416
|
console.error('[lopata:vite] Request error:', err)
|
|
314
417
|
if (!res.headersSent) {
|