lopata 0.4.3 → 0.5.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-paesqsyf.css → chunk-a68x1m5f.css} +6 -0
- package/dist/dashboard/{chunk-ymq225fp.js → chunk-rae638a4.js} +42 -11
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/src/api/handlers/do.ts +14 -0
- package/src/bindings/durable-object.ts +49 -1
- package/src/vite-plugin/config-plugin.ts +1 -1
- package/src/vite-plugin/dev-server-plugin.ts +65 -19
|
@@ -2408,6 +2408,12 @@
|
|
|
2408
2408
|
}
|
|
2409
2409
|
}
|
|
2410
2410
|
|
|
2411
|
+
@media (hover: hover) {
|
|
2412
|
+
.hover\:bg-red-700:hover {
|
|
2413
|
+
background-color: var(--color-red-700);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2411
2417
|
@media (hover: hover) {
|
|
2412
2418
|
.hover\:bg-yellow-500\/25:hover {
|
|
2413
2419
|
background-color: #edb20040;
|
|
@@ -4538,6 +4538,8 @@ function DoInstanceDetail({ ns, id, basePath, routeTab, routeTable, routeQuery }
|
|
|
4538
4538
|
const { data, refetch } = useQuery("do.getInstance", { ns, id });
|
|
4539
4539
|
const deleteEntry = useMutation("do.deleteEntry");
|
|
4540
4540
|
const triggerAlarm = useMutation("do.triggerAlarm");
|
|
4541
|
+
const cancelAlarm = useMutation("do.cancelAlarm");
|
|
4542
|
+
const deleteInstance = useMutation("do.deleteInstance");
|
|
4541
4543
|
const { data: sqlTables } = useQuery("do.listSqlTables", { ns, id });
|
|
4542
4544
|
const handleDelete = async (key) => {
|
|
4543
4545
|
if (!confirm(`Delete storage key "${key}"?`))
|
|
@@ -4549,6 +4551,16 @@ function DoInstanceDetail({ ns, id, basePath, routeTab, routeTable, routeQuery }
|
|
|
4549
4551
|
await triggerAlarm.mutate({ ns, id });
|
|
4550
4552
|
refetch();
|
|
4551
4553
|
};
|
|
4554
|
+
const handleCancelAlarm = async () => {
|
|
4555
|
+
await cancelAlarm.mutate({ ns, id });
|
|
4556
|
+
refetch();
|
|
4557
|
+
};
|
|
4558
|
+
const handleDeleteInstance = async () => {
|
|
4559
|
+
if (!confirm(`Delete this Durable Object instance? All storage data will be permanently removed.`))
|
|
4560
|
+
return;
|
|
4561
|
+
await deleteInstance.mutate({ ns, id });
|
|
4562
|
+
location.hash = `#/do/${encodeURIComponent(ns)}`;
|
|
4563
|
+
};
|
|
4552
4564
|
if (!data)
|
|
4553
4565
|
return /* @__PURE__ */ u3("div", {
|
|
4554
4566
|
class: "p-4 sm:p-8 text-text-muted font-medium",
|
|
@@ -4565,23 +4577,42 @@ function DoInstanceDetail({ ns, id, basePath, routeTab, routeTable, routeQuery }
|
|
|
4565
4577
|
]
|
|
4566
4578
|
}, undefined, false, undefined, this),
|
|
4567
4579
|
/* @__PURE__ */ u3("div", {
|
|
4568
|
-
class: "mb-6 flex justify-end",
|
|
4569
|
-
children:
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4580
|
+
class: "mb-6 flex justify-end gap-2",
|
|
4581
|
+
children: [
|
|
4582
|
+
/* @__PURE__ */ u3("button", {
|
|
4583
|
+
onClick: handleDeleteInstance,
|
|
4584
|
+
disabled: deleteInstance.isLoading,
|
|
4585
|
+
class: "rounded-md px-3 py-1.5 text-xs font-medium bg-red-600 text-white hover:bg-red-700 disabled:opacity-40 disabled:cursor-not-allowed transition-all",
|
|
4586
|
+
children: deleteInstance.isLoading ? "Deleting..." : "Delete instance"
|
|
4587
|
+
}, undefined, false, undefined, this),
|
|
4588
|
+
/* @__PURE__ */ u3(RefreshButton, {
|
|
4589
|
+
onClick: refetch
|
|
4590
|
+
}, undefined, false, undefined, this)
|
|
4591
|
+
]
|
|
4592
|
+
}, undefined, true, undefined, this),
|
|
4573
4593
|
(data.alarm || data.hasAlarmHandler) && /* @__PURE__ */ u3("div", {
|
|
4574
4594
|
class: "mb-6 px-4 py-3 bg-panel-secondary border border-border rounded-lg text-sm font-medium text-ink flex items-center justify-between",
|
|
4575
4595
|
children: [
|
|
4576
4596
|
/* @__PURE__ */ u3("span", {
|
|
4577
4597
|
children: data.alarm ? `Alarm set for: ${formatTime(data.alarm)}` : "No alarm scheduled"
|
|
4578
4598
|
}, undefined, false, undefined, this),
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4599
|
+
/* @__PURE__ */ u3("div", {
|
|
4600
|
+
class: "flex gap-2",
|
|
4601
|
+
children: [
|
|
4602
|
+
data.alarm && /* @__PURE__ */ u3("button", {
|
|
4603
|
+
onClick: handleCancelAlarm,
|
|
4604
|
+
disabled: cancelAlarm.isLoading,
|
|
4605
|
+
class: "rounded-md px-3 py-1.5 text-xs font-medium bg-red-600 text-white hover:bg-red-700 disabled:opacity-40 disabled:cursor-not-allowed transition-all",
|
|
4606
|
+
children: cancelAlarm.isLoading ? "Cancelling..." : "Cancel"
|
|
4607
|
+
}, undefined, false, undefined, this),
|
|
4608
|
+
data.hasAlarmHandler && /* @__PURE__ */ u3("button", {
|
|
4609
|
+
onClick: handleTriggerAlarm,
|
|
4610
|
+
disabled: triggerAlarm.isLoading,
|
|
4611
|
+
class: "rounded-md px-3 py-1.5 text-xs font-medium bg-ink text-surface hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed transition-all",
|
|
4612
|
+
children: triggerAlarm.isLoading ? "Triggering..." : "Trigger now"
|
|
4613
|
+
}, undefined, false, undefined, this)
|
|
4614
|
+
]
|
|
4615
|
+
}, undefined, true, undefined, this)
|
|
4585
4616
|
]
|
|
4586
4617
|
}, undefined, true, undefined, this),
|
|
4587
4618
|
data.entries.length === 0 ? /* @__PURE__ */ u3(EmptyState, {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
10
10
|
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-a68x1m5f.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-rae638a4.js"></script></head>
|
|
12
12
|
<body class="h-full bg-surface text-ink" style="font-family: system-ui, -apple-system, sans-serif;">
|
|
13
13
|
<script>
|
|
14
14
|
// Apply saved theme before first paint to prevent flash
|
package/package.json
CHANGED
package/src/api/handlers/do.ts
CHANGED
|
@@ -94,6 +94,20 @@ export const handlers = {
|
|
|
94
94
|
return { ok: true }
|
|
95
95
|
},
|
|
96
96
|
|
|
97
|
+
'do.cancelAlarm'({ ns, id }: { ns: string; id: string }, ctx: HandlerContext): OkResponse {
|
|
98
|
+
const namespace = getDoNamespace(ctx, ns)
|
|
99
|
+
if (!namespace) throw new Error(`Durable Object namespace "${ns}" not found (worker not loaded?)`)
|
|
100
|
+
namespace.cancelAlarm(id)
|
|
101
|
+
return { ok: true }
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
'do.deleteInstance'({ ns, id }: { ns: string; id: string }, ctx: HandlerContext): OkResponse {
|
|
105
|
+
const namespace = getDoNamespace(ctx, ns)
|
|
106
|
+
if (!namespace) throw new Error(`Durable Object namespace "${ns}" not found (worker not loaded?)`)
|
|
107
|
+
namespace.deleteInstance(id)
|
|
108
|
+
return { ok: true }
|
|
109
|
+
},
|
|
110
|
+
|
|
97
111
|
async 'do.generateSql'({ ns, id, prompt }: { ns: string; id: string; prompt: string }, ctx: HandlerContext): Promise<{ sql: string }> {
|
|
98
112
|
const dbPath = join(getDataDir(), 'do-sql', ns, `${id}.sqlite`)
|
|
99
113
|
if (!existsSync(dbPath)) throw new Error('SQL database not found for this instance')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Database, type SQLQueryBindings } from 'bun:sqlite'
|
|
2
|
-
import { mkdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, statSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { persistError, startSpan } from '../tracing/span'
|
|
5
5
|
import type { ContainerContext } from './container'
|
|
@@ -952,6 +952,54 @@ export class DurableObjectNamespaceImpl {
|
|
|
952
952
|
return this._fireAlarm(idStr, 0)
|
|
953
953
|
}
|
|
954
954
|
|
|
955
|
+
/** @internal Cancel a scheduled alarm without firing it */
|
|
956
|
+
cancelAlarm(idStr: string): void {
|
|
957
|
+
const timer = this.alarmTimers.get(idStr)
|
|
958
|
+
if (timer) clearTimeout(timer)
|
|
959
|
+
this.alarmTimers.delete(idStr)
|
|
960
|
+
this.db
|
|
961
|
+
.query('DELETE FROM do_alarms WHERE namespace = ? AND id = ?')
|
|
962
|
+
.run(this.namespaceName, idStr)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** @internal Delete a DO instance and all its data */
|
|
966
|
+
deleteInstance(idStr: string): void {
|
|
967
|
+
this.cancelAlarm(idStr)
|
|
968
|
+
|
|
969
|
+
const executor = this._executors.get(idStr)
|
|
970
|
+
if (executor) {
|
|
971
|
+
executor.dispose().catch(() => {})
|
|
972
|
+
this._executors.delete(idStr)
|
|
973
|
+
}
|
|
974
|
+
this._stubs.delete(idStr)
|
|
975
|
+
this._knownIds.delete(idStr)
|
|
976
|
+
this._lastActivity.delete(idStr)
|
|
977
|
+
|
|
978
|
+
this.db.run('BEGIN')
|
|
979
|
+
try {
|
|
980
|
+
this.db.query('DELETE FROM do_instances WHERE namespace = ? AND id = ?').run(this.namespaceName, idStr)
|
|
981
|
+
this.db.query('DELETE FROM do_storage WHERE namespace = ? AND id = ?').run(this.namespaceName, idStr)
|
|
982
|
+
this.db.query('DELETE FROM do_alarms WHERE namespace = ? AND id = ?').run(this.namespaceName, idStr)
|
|
983
|
+
this.db.run('COMMIT')
|
|
984
|
+
} catch (e) {
|
|
985
|
+
this.db.run('ROLLBACK')
|
|
986
|
+
throw e
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Delete SQL storage database files
|
|
990
|
+
if (this.dataDir) {
|
|
991
|
+
const sqlDbPath = join(this.dataDir, 'do-sql', this.namespaceName, `${idStr}.sqlite`)
|
|
992
|
+
if (existsSync(sqlDbPath)) {
|
|
993
|
+
rmSync(sqlDbPath, { force: true })
|
|
994
|
+
// Clean up WAL/SHM files
|
|
995
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
996
|
+
const walPath = sqlDbPath + suffix
|
|
997
|
+
if (existsSync(walPath)) rmSync(walPath, { force: true })
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
955
1003
|
newUniqueId(_options?: { jurisdiction?: string }): DurableObjectIdImpl {
|
|
956
1004
|
return new DurableObjectIdImpl(crypto.randomUUID().replace(/-/g, ''))
|
|
957
1005
|
}
|
|
@@ -52,8 +52,8 @@ 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
|
-
let
|
|
55
|
+
// Files changed since last request — coalesced into a single invalidation batch
|
|
56
|
+
let changedFiles: Set<string> = new Set()
|
|
57
57
|
|
|
58
58
|
return {
|
|
59
59
|
name: 'lopata:dev-server',
|
|
@@ -168,18 +168,17 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// 5. Track SSR-relevant file changes.
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// ensuring a clean re-evaluation from the freshly-invalidated module graph.
|
|
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.
|
|
176
175
|
server.watcher.on('change', (file) => {
|
|
177
176
|
const ssrEnv = server.environments[options.envName]
|
|
178
177
|
if (!ssrEnv) return
|
|
179
178
|
const normalizedFile = file.replace(/\\/g, '/')
|
|
180
179
|
const mods = ssrEnv.moduleGraph.getModulesByFile(normalizedFile)
|
|
181
180
|
if (mods && mods.size > 0) {
|
|
182
|
-
|
|
181
|
+
changedFiles.add(normalizedFile)
|
|
183
182
|
currentModule = null
|
|
184
183
|
}
|
|
185
184
|
})
|
|
@@ -244,17 +243,18 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
244
243
|
// Wait for any in-progress reload before importing
|
|
245
244
|
if (reloadLock) await reloadLock
|
|
246
245
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
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
258
|
}
|
|
259
259
|
|
|
260
260
|
const workerModule = await (ssrEnv as any).runner.import(entrypoint) as Record<string, unknown>
|
|
@@ -452,6 +452,52 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Invalidate runner-evaluated modules for the given changed files and all
|
|
457
|
+
* their transitive importers. Virtual modules (IDs starting with `\0`) are
|
|
458
|
+
* skipped to preserve shimmed CF modules (see commit 75b1736).
|
|
459
|
+
*
|
|
460
|
+
* Returns the number of modules invalidated.
|
|
461
|
+
*/
|
|
462
|
+
function invalidateChangedModules(
|
|
463
|
+
evaluatedModules: { getModulesByFile(file: string): Iterable<{ id: string; importers: Set<any> }> | undefined; invalidateModule(node: any): void },
|
|
464
|
+
changedFiles: Set<string>,
|
|
465
|
+
): number {
|
|
466
|
+
const toInvalidate = new Set<{ id: string; importers: Set<any> }>()
|
|
467
|
+
|
|
468
|
+
for (const file of changedFiles) {
|
|
469
|
+
const nodes = evaluatedModules.getModulesByFile(file)
|
|
470
|
+
if (!nodes) continue
|
|
471
|
+
for (const node of nodes) {
|
|
472
|
+
collectTransitiveImporters(node, toInvalidate)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const node of toInvalidate) {
|
|
477
|
+
evaluatedModules.invalidateModule(node)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return toInvalidate.size
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Collect `node` and all its transitive importers into `result`.
|
|
485
|
+
* Skips virtual modules (IDs starting with `\0`) — these are CF module shims
|
|
486
|
+
* that must not be invalidated or traversed further.
|
|
487
|
+
*/
|
|
488
|
+
function collectTransitiveImporters(
|
|
489
|
+
node: { id: string; importers: Set<any> },
|
|
490
|
+
result: Set<{ id: string; importers: Set<any> }>,
|
|
491
|
+
): void {
|
|
492
|
+
if (result.has(node)) return
|
|
493
|
+
// Skip virtual modules — CF shims must stay cached
|
|
494
|
+
if (node.id.startsWith('\0')) return
|
|
495
|
+
result.add(node)
|
|
496
|
+
for (const importer of node.importers) {
|
|
497
|
+
collectTransitiveImporters(importer, result)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
455
501
|
function stitchAsyncStack(err: Error, callerError: Error | null): void {
|
|
456
502
|
if (!callerError) return
|
|
457
503
|
if (!err.stack || !callerError.stack) return
|