lopata 0.5.2 → 0.7.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/dashboard/{chunk-rae638a4.js → chunk-5nxa3jfc.js} +296 -93
- package/dist/dashboard/{chunk-a68x1m5f.css → chunk-pqnphvm2.css} +16 -0
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/src/bindings/do-websocket-bridge.ts +9 -0
- package/src/bindings/durable-object.ts +6 -0
- package/src/bindings/websocket-pair.ts +13 -0
- package/src/cli/dev.ts +3 -0
- package/src/plugin.ts +3 -112
- package/src/setup-globals.ts +150 -0
- package/src/vite-plugin/config-plugin.ts +6 -2
- package/src/vite-plugin/dev-server-plugin.ts +217 -196
- package/src/vite-plugin/globals-plugin.ts +5 -79
|
@@ -52,8 +52,53 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
52
52
|
let currentModule: Record<string, unknown> | null = null
|
|
53
53
|
// Serializes module reload — prevents concurrent wireClassRefs calls
|
|
54
54
|
let reloadLock: Promise<void> | null = null
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Import the worker module through Vite's SSR runner and re-wire
|
|
58
|
+
* class refs when the module identity changes (HMR invalidation).
|
|
59
|
+
* Serialized via reloadLock to prevent concurrent wireClassRefs calls.
|
|
60
|
+
*/
|
|
61
|
+
async function ensureWorkerModule(): Promise<Record<string, unknown>> {
|
|
62
|
+
const ssrEnv = server.environments[options.envName]
|
|
63
|
+
if (!ssrEnv || !('runner' in ssrEnv)) {
|
|
64
|
+
throw new Error(`SSR environment "${options.envName}" not found or has no runner`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entrypoint = resolve(server.config.root, config.main)
|
|
68
|
+
|
|
69
|
+
// Wait for any in-progress reload before importing
|
|
70
|
+
if (reloadLock) await reloadLock
|
|
71
|
+
|
|
72
|
+
const workerModule = await (ssrEnv as any).runner.import(entrypoint) as Record<string, unknown>
|
|
73
|
+
|
|
74
|
+
// Re-wire class refs when module changes (HMR invalidation)
|
|
75
|
+
if (workerModule !== currentModule) {
|
|
76
|
+
if (reloadLock) {
|
|
77
|
+
// Another request started reloading while we were importing — wait for it
|
|
78
|
+
await reloadLock
|
|
79
|
+
} else {
|
|
80
|
+
let resolveReload!: () => void
|
|
81
|
+
reloadLock = new Promise(r => {
|
|
82
|
+
resolveReload = r
|
|
83
|
+
})
|
|
84
|
+
try {
|
|
85
|
+
currentModule = workerModule
|
|
86
|
+
wireClassRefs(registry, workerModule, env, workerRegistry)
|
|
87
|
+
setGlobalEnv(env)
|
|
88
|
+
console.log('[lopata:vite] Worker module (re)loaded, classes wired')
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Reset so next request retries
|
|
91
|
+
currentModule = null
|
|
92
|
+
throw err
|
|
93
|
+
} finally {
|
|
94
|
+
reloadLock = null
|
|
95
|
+
resolveReload()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return currentModule ?? workerModule
|
|
101
|
+
}
|
|
57
102
|
|
|
58
103
|
return {
|
|
59
104
|
name: 'lopata:dev-server',
|
|
@@ -167,23 +212,7 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
167
212
|
apiMod.setWorkerRegistry(workerRegistry)
|
|
168
213
|
}
|
|
169
214
|
|
|
170
|
-
// 5.
|
|
171
|
-
// Collect changed file paths; the next request will invalidate only
|
|
172
|
-
// the affected modules (and their transitive importers) instead of
|
|
173
|
-
// clearing the entire runner cache. HMR on the runner is disabled
|
|
174
|
-
// (hmr: false in config-plugin) so there's no async race.
|
|
175
|
-
server.watcher.on('change', (file) => {
|
|
176
|
-
const ssrEnv = server.environments[options.envName]
|
|
177
|
-
if (!ssrEnv) return
|
|
178
|
-
const normalizedFile = file.replace(/\\/g, '/')
|
|
179
|
-
const mods = ssrEnv.moduleGraph.getModulesByFile(normalizedFile)
|
|
180
|
-
if (mods && mods.size > 0) {
|
|
181
|
-
changedFiles.add(normalizedFile)
|
|
182
|
-
currentModule = null
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
// 6. Set up WebSocket trace streaming on httpServer
|
|
215
|
+
// 5. Set up WebSocket trace streaming on httpServer
|
|
187
216
|
setupTraceWebSocket(server)
|
|
188
217
|
|
|
189
218
|
// 6. Return middleware callback (post-middleware — runs after framework plugins)
|
|
@@ -232,63 +261,11 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
232
261
|
}
|
|
233
262
|
|
|
234
263
|
try {
|
|
235
|
-
const
|
|
236
|
-
if (!ssrEnv || !('runner' in ssrEnv)) {
|
|
237
|
-
console.error(`[lopata:vite] SSR environment "${options.envName}" not found or has no runner`)
|
|
238
|
-
return next()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const entrypoint = resolve(server.config.root, config.main)
|
|
242
|
-
|
|
243
|
-
// Wait for any in-progress reload before importing
|
|
244
|
-
if (reloadLock) await reloadLock
|
|
245
|
-
|
|
246
|
-
// Granular invalidation: only invalidate modules for changed
|
|
247
|
-
// files and their transitive importers, instead of wiping the
|
|
248
|
-
// entire runner cache. This preserves cached evaluations of
|
|
249
|
-
// unchanged modules for faster re-evaluation.
|
|
250
|
-
if (changedFiles.size > 0) {
|
|
251
|
-
const files = changedFiles
|
|
252
|
-
changedFiles = new Set()
|
|
253
|
-
const runner = (ssrEnv as any).runner
|
|
254
|
-
const count = invalidateChangedModules(runner.evaluatedModules, files)
|
|
255
|
-
if (count > 0) {
|
|
256
|
-
console.log(`[lopata:vite] Invalidated ${count} module(s) (${files.size} file(s) changed)`)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const workerModule = await (ssrEnv as any).runner.import(entrypoint) as Record<string, unknown>
|
|
261
|
-
|
|
262
|
-
// Re-wire class refs when module changes (HMR invalidation)
|
|
263
|
-
if (workerModule !== currentModule) {
|
|
264
|
-
if (reloadLock) {
|
|
265
|
-
// Another request started reloading while we were importing — wait for it
|
|
266
|
-
await reloadLock
|
|
267
|
-
} else {
|
|
268
|
-
let resolveReload!: () => void
|
|
269
|
-
reloadLock = new Promise(r => {
|
|
270
|
-
resolveReload = r
|
|
271
|
-
})
|
|
272
|
-
try {
|
|
273
|
-
currentModule = workerModule
|
|
274
|
-
wireClassRefs(registry, workerModule, env, workerRegistry)
|
|
275
|
-
setGlobalEnv(env)
|
|
276
|
-
console.log('[lopata:vite] Worker module (re)loaded, classes wired')
|
|
277
|
-
} catch (err) {
|
|
278
|
-
// Reset so next request retries
|
|
279
|
-
currentModule = null
|
|
280
|
-
throw err
|
|
281
|
-
} finally {
|
|
282
|
-
reloadLock = null
|
|
283
|
-
resolveReload()
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
264
|
+
const activeModule = await ensureWorkerModule()
|
|
287
265
|
|
|
288
266
|
const request = nodeReqToRequest(req)
|
|
289
267
|
const parsedUrl = new URL(request.url)
|
|
290
268
|
|
|
291
|
-
const activeModule = currentModule ?? workerModule
|
|
292
269
|
const handler = activeModule.default as Record<string, unknown>
|
|
293
270
|
if (!handler || typeof handler.fetch !== 'function') {
|
|
294
271
|
console.error('[lopata:vite] Worker module default export has no fetch() method')
|
|
@@ -350,98 +327,28 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
350
327
|
|
|
351
328
|
// Dynamically import ws (available as Vite dependency)
|
|
352
329
|
import('ws').then(({ WebSocketServer }) => {
|
|
353
|
-
const
|
|
330
|
+
const traceWss = new WebSocketServer({ noServer: true })
|
|
331
|
+
const workerWss = new WebSocketServer({ noServer: true })
|
|
354
332
|
|
|
355
333
|
httpServer.on('upgrade', (req: IncomingMessage, socket: any, head: Buffer) => {
|
|
356
334
|
const url = req.url ?? ''
|
|
357
|
-
if (!url.startsWith('/__api/traces/ws')) return
|
|
358
|
-
|
|
359
|
-
wss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
360
|
-
const store = getTraceStore()
|
|
361
|
-
let filter: { path?: string; status?: string; attributeFilters?: Array<{ key: string; value: string; type: 'include' | 'exclude' }> } = {}
|
|
362
|
-
let buffer: any[] = []
|
|
363
|
-
const MAX_BUFFER = 1000
|
|
364
|
-
const allowedTraces = new Set<string>()
|
|
365
|
-
const excludedTraces = new Set<string>()
|
|
366
|
-
|
|
367
|
-
function isRootSpanFiltered(span: { name: string; status: string; parentSpanId: string | null; attributes: Record<string, unknown> }): boolean {
|
|
368
|
-
if (filter.status && filter.status !== 'all') {
|
|
369
|
-
if (span.status !== 'unset' && span.status !== filter.status) return true
|
|
370
|
-
}
|
|
371
|
-
if (filter.path) {
|
|
372
|
-
if (!matchGlob(span.name, filter.path)) return true
|
|
373
|
-
}
|
|
374
|
-
if (filter.attributeFilters && filter.attributeFilters.length > 0) {
|
|
375
|
-
const attrs = span.attributes
|
|
376
|
-
for (const af of filter.attributeFilters) {
|
|
377
|
-
const val = attrs[af.key]
|
|
378
|
-
const matches = val !== undefined && String(val).toLowerCase().includes(af.value.toLowerCase())
|
|
379
|
-
if (af.type === 'include' && !matches) return true
|
|
380
|
-
if (af.type === 'exclude' && matches) return true
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return false
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const unsubscribe = store.subscribe((event: any) => {
|
|
387
|
-
const traceId = event.type === 'span.event' ? event.event.traceId : event.span.traceId
|
|
388
|
-
if ((event.type === 'span.start' || event.type === 'span.end') && event.span.parentSpanId === null) {
|
|
389
|
-
if (isRootSpanFiltered(event.span)) {
|
|
390
|
-
excludedTraces.add(traceId)
|
|
391
|
-
allowedTraces.delete(traceId)
|
|
392
|
-
return
|
|
393
|
-
}
|
|
394
|
-
excludedTraces.delete(traceId)
|
|
395
|
-
allowedTraces.add(traceId)
|
|
396
|
-
} else {
|
|
397
|
-
if (excludedTraces.has(traceId)) return
|
|
398
|
-
}
|
|
399
|
-
if (buffer.length < MAX_BUFFER) {
|
|
400
|
-
buffer.push(event)
|
|
401
|
-
}
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
const interval = setInterval(() => {
|
|
405
|
-
if (buffer.length > 0) {
|
|
406
|
-
ws.send(JSON.stringify({ type: 'batch', events: buffer }))
|
|
407
|
-
buffer = []
|
|
408
|
-
}
|
|
409
|
-
}, 500)
|
|
410
335
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const pathParam = reqUrl.searchParams.get('path')
|
|
416
|
-
if (statusParam) filter.status = statusParam
|
|
417
|
-
if (pathParam) filter.path = pathParam
|
|
418
|
-
} catch {}
|
|
419
|
-
|
|
420
|
-
let sinceMs = 15 * 60 * 1000
|
|
421
|
-
const since = Date.now() - sinceMs
|
|
422
|
-
const recent = store.getRecentTraces(since, 200, filter)
|
|
423
|
-
ws.send(JSON.stringify({ type: 'initial', traces: recent }))
|
|
336
|
+
// Skip Vite HMR WebSocket — Vite uses sec-websocket-protocol
|
|
337
|
+
// "vite-hmr" / "vite-ping" to identify its connections
|
|
338
|
+
const wsProtocol = req.headers['sec-websocket-protocol']
|
|
339
|
+
if (wsProtocol === 'vite-hmr' || wsProtocol === 'vite-ping') return
|
|
424
340
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (msg.type === 'filter') {
|
|
429
|
-
filter = { path: msg.path, status: msg.status, attributeFilters: msg.attributeFilters }
|
|
430
|
-
if (msg.sinceMs !== undefined) sinceMs = msg.sinceMs
|
|
431
|
-
allowedTraces.clear()
|
|
432
|
-
excludedTraces.clear()
|
|
433
|
-
const freshSince = sinceMs > 0 ? Date.now() - sinceMs : 0
|
|
434
|
-
const freshTraces = store.getRecentTraces(freshSince, 200, filter)
|
|
435
|
-
ws.send(JSON.stringify({ type: 'initial', traces: freshTraces }))
|
|
436
|
-
}
|
|
437
|
-
} catch {}
|
|
341
|
+
if (url.startsWith('/__api/traces/ws')) {
|
|
342
|
+
traceWss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
343
|
+
handleTraceWebSocket(ws, req)
|
|
438
344
|
})
|
|
345
|
+
return
|
|
346
|
+
}
|
|
439
347
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
})
|
|
348
|
+
// Worker WebSocket upgrade — bridge to CF WebSocketPair
|
|
349
|
+
if (req.headers.upgrade?.toLowerCase() === 'websocket') {
|
|
350
|
+
handleWorkerWebSocketUpgrade(workerWss, req, socket, head)
|
|
351
|
+
}
|
|
445
352
|
})
|
|
446
353
|
|
|
447
354
|
console.log('[lopata:vite] Dashboard: http://localhost:5173/__dashboard')
|
|
@@ -450,51 +357,165 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
450
357
|
console.log('[lopata:vite] Dashboard available (trace streaming disabled — ws package not found)')
|
|
451
358
|
})
|
|
452
359
|
}
|
|
453
|
-
}
|
|
454
360
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
361
|
+
function handleTraceWebSocket(ws: any, req: IncomingMessage) {
|
|
362
|
+
const store = getTraceStore()
|
|
363
|
+
let filter: { path?: string; status?: string; attributeFilters?: Array<{ key: string; value: string; type: 'include' | 'exclude' }> } = {}
|
|
364
|
+
let buffer: any[] = []
|
|
365
|
+
const MAX_BUFFER = 1000
|
|
366
|
+
const allowedTraces = new Set<string>()
|
|
367
|
+
const excludedTraces = new Set<string>()
|
|
368
|
+
|
|
369
|
+
function isRootSpanFiltered(span: { name: string; status: string; parentSpanId: string | null; attributes: Record<string, unknown> }): boolean {
|
|
370
|
+
if (filter.status && filter.status !== 'all') {
|
|
371
|
+
if (span.status !== 'unset' && span.status !== filter.status) return true
|
|
372
|
+
}
|
|
373
|
+
if (filter.path) {
|
|
374
|
+
if (!matchGlob(span.name, filter.path)) return true
|
|
375
|
+
}
|
|
376
|
+
if (filter.attributeFilters && filter.attributeFilters.length > 0) {
|
|
377
|
+
const attrs = span.attributes
|
|
378
|
+
for (const af of filter.attributeFilters) {
|
|
379
|
+
const val = attrs[af.key]
|
|
380
|
+
const matches = val !== undefined && String(val).toLowerCase().includes(af.value.toLowerCase())
|
|
381
|
+
if (af.type === 'include' && !matches) return true
|
|
382
|
+
if (af.type === 'exclude' && matches) return true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return false
|
|
473
386
|
}
|
|
474
|
-
}
|
|
475
387
|
|
|
476
|
-
|
|
477
|
-
|
|
388
|
+
const unsubscribe = store.subscribe((event: any) => {
|
|
389
|
+
const traceId = event.type === 'span.event' ? event.event.traceId : event.span.traceId
|
|
390
|
+
if ((event.type === 'span.start' || event.type === 'span.end') && event.span.parentSpanId === null) {
|
|
391
|
+
if (isRootSpanFiltered(event.span)) {
|
|
392
|
+
excludedTraces.add(traceId)
|
|
393
|
+
allowedTraces.delete(traceId)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
excludedTraces.delete(traceId)
|
|
397
|
+
allowedTraces.add(traceId)
|
|
398
|
+
} else {
|
|
399
|
+
if (excludedTraces.has(traceId)) return
|
|
400
|
+
}
|
|
401
|
+
if (buffer.length < MAX_BUFFER) {
|
|
402
|
+
buffer.push(event)
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const interval = setInterval(() => {
|
|
407
|
+
if (buffer.length > 0) {
|
|
408
|
+
ws.send(JSON.stringify({ type: 'batch', events: buffer }))
|
|
409
|
+
buffer = []
|
|
410
|
+
}
|
|
411
|
+
}, 500)
|
|
412
|
+
|
|
413
|
+
// Parse filter from query params
|
|
414
|
+
try {
|
|
415
|
+
const reqUrl = new URL(req.url ?? '', `http://${req.headers.host ?? 'localhost'}`)
|
|
416
|
+
const statusParam = reqUrl.searchParams.get('status')
|
|
417
|
+
const pathParam = reqUrl.searchParams.get('path')
|
|
418
|
+
if (statusParam) filter.status = statusParam
|
|
419
|
+
if (pathParam) filter.path = pathParam
|
|
420
|
+
} catch {}
|
|
421
|
+
|
|
422
|
+
let sinceMs = 15 * 60 * 1000
|
|
423
|
+
const since = Date.now() - sinceMs
|
|
424
|
+
const recent = store.getRecentTraces(since, 200, filter)
|
|
425
|
+
ws.send(JSON.stringify({ type: 'initial', traces: recent }))
|
|
426
|
+
|
|
427
|
+
ws.on('message', (data: any) => {
|
|
428
|
+
try {
|
|
429
|
+
const msg = JSON.parse(typeof data === 'string' ? data : data.toString())
|
|
430
|
+
if (msg.type === 'filter') {
|
|
431
|
+
filter = { path: msg.path, status: msg.status, attributeFilters: msg.attributeFilters }
|
|
432
|
+
if (msg.sinceMs !== undefined) sinceMs = msg.sinceMs
|
|
433
|
+
allowedTraces.clear()
|
|
434
|
+
excludedTraces.clear()
|
|
435
|
+
const freshSince = sinceMs > 0 ? Date.now() - sinceMs : 0
|
|
436
|
+
const freshTraces = store.getRecentTraces(freshSince, 200, filter)
|
|
437
|
+
ws.send(JSON.stringify({ type: 'initial', traces: freshTraces }))
|
|
438
|
+
}
|
|
439
|
+
} catch {}
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
ws.on('close', () => {
|
|
443
|
+
unsubscribe()
|
|
444
|
+
clearInterval(interval)
|
|
445
|
+
})
|
|
478
446
|
}
|
|
479
447
|
|
|
480
|
-
|
|
481
|
-
|
|
448
|
+
async function handleWorkerWebSocketUpgrade(wss: any, req: IncomingMessage, socket: any, head: Buffer) {
|
|
449
|
+
try {
|
|
450
|
+
const { CFWebSocket } = await import('../bindings/websocket-pair.ts')
|
|
482
451
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
452
|
+
const activeModule = await ensureWorkerModule()
|
|
453
|
+
const handler = activeModule.default as Record<string, unknown>
|
|
454
|
+
if (!handler || typeof handler.fetch !== 'function') {
|
|
455
|
+
socket.destroy()
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const request = nodeReqToRequest(req)
|
|
460
|
+
const ctx = new ExecutionContext()
|
|
461
|
+
const response = await runWithExecutionContext(ctx, async () => {
|
|
462
|
+
return (handler.fetch as Function).call(handler, request, env, ctx) as Response
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const cfSocket = (response as Response & { webSocket?: InstanceType<typeof CFWebSocket> }).webSocket
|
|
466
|
+
if (response.status !== 101 || !cfSocket || !(cfSocket instanceof CFWebSocket)) {
|
|
467
|
+
socket.destroy()
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Complete the upgrade and bridge
|
|
472
|
+
wss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
473
|
+
// CF → real WS
|
|
474
|
+
cfSocket.addEventListener('message', (ev: Event) => {
|
|
475
|
+
const msgData = (ev as MessageEvent).data
|
|
476
|
+
try {
|
|
477
|
+
ws.send(msgData)
|
|
478
|
+
} catch {}
|
|
479
|
+
})
|
|
480
|
+
cfSocket.addEventListener('close', (ev: Event) => {
|
|
481
|
+
const ce = ev as CloseEvent
|
|
482
|
+
try {
|
|
483
|
+
ws.close(ce.code, ce.reason)
|
|
484
|
+
} catch {}
|
|
485
|
+
})
|
|
486
|
+
// Accept the client side so events from server.send() are dispatched
|
|
487
|
+
cfSocket.accept()
|
|
488
|
+
|
|
489
|
+
// Real WS → CF
|
|
490
|
+
ws.on('message', (data: Buffer, isBinary: boolean) => {
|
|
491
|
+
const msgData = isBinary
|
|
492
|
+
? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer
|
|
493
|
+
: data.toString('utf-8')
|
|
494
|
+
const evt = { type: 'message' as const, data: msgData }
|
|
495
|
+
if (cfSocket._peer?._accepted) {
|
|
496
|
+
cfSocket._peer._dispatchWSEvent(evt)
|
|
497
|
+
} else if (cfSocket._peer) {
|
|
498
|
+
cfSocket._peer._eventQueue.push(evt)
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
ws.on('close', (code: number, reason: Buffer) => {
|
|
503
|
+
if (cfSocket._peer && cfSocket._peer.readyState !== 3) {
|
|
504
|
+
const evt = { type: 'close' as const, code: code ?? 1000, reason: reason?.toString('utf-8') ?? '', wasClean: true }
|
|
505
|
+
if (cfSocket._peer._accepted) {
|
|
506
|
+
cfSocket._peer._dispatchWSEvent(evt)
|
|
507
|
+
} else {
|
|
508
|
+
cfSocket._peer._eventQueue.push(evt)
|
|
509
|
+
}
|
|
510
|
+
cfSocket._peer.readyState = 3
|
|
511
|
+
}
|
|
512
|
+
cfSocket.readyState = 3
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.error('[lopata:vite] Worker WebSocket upgrade failed:', err)
|
|
517
|
+
socket.destroy()
|
|
518
|
+
}
|
|
498
519
|
}
|
|
499
520
|
}
|
|
500
521
|
|
|
@@ -3,13 +3,11 @@ import type { Plugin } from 'vite'
|
|
|
3
3
|
let initialized = false
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Sets up global Cloudflare-compatible APIs in the Bun process
|
|
7
|
-
* caches, HTMLRewriter, WebSocketPair, IdentityTransformStream, FixedLengthStream,
|
|
8
|
-
* navigator.userAgent, scheduler.wait(), crypto extensions.
|
|
6
|
+
* Sets up global Cloudflare-compatible APIs in the Bun process.
|
|
9
7
|
*
|
|
10
8
|
* Runs once on configureServer (before middleware), idempotent.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Uses dynamic import because the plugin file is externalized by Vite's config bundler —
|
|
10
|
+
* the import runs at dev server startup time through Bun's native loader.
|
|
13
11
|
*/
|
|
14
12
|
export function globalsPlugin(): Plugin {
|
|
15
13
|
return {
|
|
@@ -19,80 +17,8 @@ export function globalsPlugin(): Plugin {
|
|
|
19
17
|
if (initialized) return
|
|
20
18
|
initialized = true
|
|
21
19
|
|
|
22
|
-
const {
|
|
23
|
-
|
|
24
|
-
const { WebSocketPair } = await import('../bindings/websocket-pair.ts')
|
|
25
|
-
const { IdentityTransformStream, FixedLengthStream } = await import('../bindings/cf-streams.ts')
|
|
26
|
-
const { patchGlobalCrypto } = await import('../bindings/crypto-extras.ts')
|
|
27
|
-
const { getDatabase } = await import('../db.ts')
|
|
28
|
-
const { instrumentBinding } = await import('../tracing/instrument.ts')
|
|
29
|
-
|
|
30
|
-
// Global caches (CacheStorage)
|
|
31
|
-
const cacheMethods = ['match', 'put', 'delete']
|
|
32
|
-
const rawCacheStorage = new SqliteCacheStorage(getDatabase())
|
|
33
|
-
rawCacheStorage.default = instrumentBinding(rawCacheStorage.default, {
|
|
34
|
-
type: 'cache',
|
|
35
|
-
name: 'default',
|
|
36
|
-
methods: cacheMethods,
|
|
37
|
-
}) as typeof rawCacheStorage.default
|
|
38
|
-
|
|
39
|
-
const originalOpen = rawCacheStorage.open.bind(rawCacheStorage)
|
|
40
|
-
rawCacheStorage.open = async (cacheName: string) => {
|
|
41
|
-
const cache = await originalOpen(cacheName)
|
|
42
|
-
return instrumentBinding(cache, {
|
|
43
|
-
type: 'cache',
|
|
44
|
-
name: cacheName,
|
|
45
|
-
methods: cacheMethods,
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
Object.defineProperty(globalThis, 'caches', {
|
|
50
|
-
value: rawCacheStorage,
|
|
51
|
-
writable: false,
|
|
52
|
-
configurable: true,
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
Object.defineProperty(globalThis, 'HTMLRewriter', {
|
|
56
|
-
value: HTMLRewriter,
|
|
57
|
-
writable: false,
|
|
58
|
-
configurable: true,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
Object.defineProperty(globalThis, 'WebSocketPair', {
|
|
62
|
-
value: WebSocketPair,
|
|
63
|
-
writable: false,
|
|
64
|
-
configurable: true,
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
Object.defineProperty(globalThis, 'IdentityTransformStream', {
|
|
68
|
-
value: IdentityTransformStream,
|
|
69
|
-
writable: false,
|
|
70
|
-
configurable: true,
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
Object.defineProperty(globalThis, 'FixedLengthStream', {
|
|
74
|
-
value: FixedLengthStream,
|
|
75
|
-
writable: false,
|
|
76
|
-
configurable: true,
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
patchGlobalCrypto()
|
|
80
|
-
|
|
81
|
-
Object.defineProperty(globalThis.navigator, 'userAgent', {
|
|
82
|
-
value: 'Cloudflare-Workers',
|
|
83
|
-
writable: false,
|
|
84
|
-
configurable: true,
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
Object.defineProperty(globalThis, 'scheduler', {
|
|
88
|
-
value: {
|
|
89
|
-
wait(ms: number): Promise<void> {
|
|
90
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
writable: false,
|
|
94
|
-
configurable: true,
|
|
95
|
-
})
|
|
20
|
+
const { setupCloudflareGlobals } = await import('../setup-globals.ts')
|
|
21
|
+
setupCloudflareGlobals()
|
|
96
22
|
},
|
|
97
23
|
}
|
|
98
24
|
}
|