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
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
--color-rose-400: oklch(71.2% .194 13.428);
|
|
43
43
|
--color-rose-500: oklch(64.5% .246 16.439);
|
|
44
44
|
--color-gray-400: oklch(70.7% .022 261.325);
|
|
45
|
+
--color-gray-500: oklch(55.1% .027 264.364);
|
|
45
46
|
--color-gray-900: oklch(21% .034 264.665);
|
|
46
47
|
--color-neutral-400: oklch(70.8% 0 0);
|
|
47
48
|
--color-neutral-500: oklch(55.6% 0 0);
|
|
@@ -1438,6 +1439,16 @@
|
|
|
1438
1439
|
background-color: var(--color-gray-400);
|
|
1439
1440
|
}
|
|
1440
1441
|
|
|
1442
|
+
.bg-gray-500\/15 {
|
|
1443
|
+
background-color: #6a728226;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1447
|
+
.bg-gray-500\/15 {
|
|
1448
|
+
background-color: color-mix(in oklab, var(--color-gray-500) 15%, transparent);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1441
1452
|
.bg-gray-900 {
|
|
1442
1453
|
background-color: var(--color-gray-900);
|
|
1443
1454
|
}
|
|
@@ -1964,6 +1975,10 @@
|
|
|
1964
1975
|
color: var(--color-emerald-600);
|
|
1965
1976
|
}
|
|
1966
1977
|
|
|
1978
|
+
.text-gray-400 {
|
|
1979
|
+
color: var(--color-gray-400);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1967
1982
|
.text-indigo-400 {
|
|
1968
1983
|
color: var(--color-indigo-400);
|
|
1969
1984
|
}
|
|
@@ -2304,6 +2319,18 @@
|
|
|
2304
2319
|
}
|
|
2305
2320
|
}
|
|
2306
2321
|
|
|
2322
|
+
@media (hover: hover) {
|
|
2323
|
+
.hover\:bg-accent-lime\/90:hover {
|
|
2324
|
+
background-color: #f0a030e6;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
2328
|
+
.hover\:bg-accent-lime\/90:hover {
|
|
2329
|
+
background-color: color-mix(in oklab, var(--color-accent-lime) 90%, transparent);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2307
2334
|
@media (hover: hover) {
|
|
2308
2335
|
.hover\:bg-amber-500\/10:hover {
|
|
2309
2336
|
background-color: #f99c001a;
|
|
@@ -6264,6 +6264,325 @@ function truncateUrl(url) {
|
|
|
6264
6264
|
}
|
|
6265
6265
|
}
|
|
6266
6266
|
|
|
6267
|
+
// src/dashboard/views/generations.tsx
|
|
6268
|
+
var STATE_COLORS = {
|
|
6269
|
+
active: "bg-emerald-500/15 text-emerald-500",
|
|
6270
|
+
draining: "bg-yellow-500/15 text-yellow-500",
|
|
6271
|
+
stopped: "bg-gray-500/15 text-gray-400"
|
|
6272
|
+
};
|
|
6273
|
+
var STATE_DESCRIPTIONS = {
|
|
6274
|
+
active: "Receiving new requests",
|
|
6275
|
+
draining: "Finishing in-flight requests, no new requests accepted",
|
|
6276
|
+
stopped: "All work finished, will be removed shortly"
|
|
6277
|
+
};
|
|
6278
|
+
function formatRelativeTime(timestamp) {
|
|
6279
|
+
const diff = Date.now() - timestamp;
|
|
6280
|
+
if (diff < 1000)
|
|
6281
|
+
return "just now";
|
|
6282
|
+
if (diff < 60000)
|
|
6283
|
+
return `${Math.floor(diff / 1000)}s ago`;
|
|
6284
|
+
if (diff < 3600000)
|
|
6285
|
+
return `${Math.floor(diff / 60000)}m ago`;
|
|
6286
|
+
return `${Math.floor(diff / 3600000)}h ago`;
|
|
6287
|
+
}
|
|
6288
|
+
function GenerationCard({ gen, onReload, onStop }) {
|
|
6289
|
+
const [detail, setDetail] = d2(null);
|
|
6290
|
+
const [expanded, setExpanded] = d2(false);
|
|
6291
|
+
y2(() => {
|
|
6292
|
+
if (!expanded)
|
|
6293
|
+
return;
|
|
6294
|
+
rpc("generations.detail", { id: gen.id, workerName: gen.workerName }).then(setDetail).catch(() => {});
|
|
6295
|
+
}, [expanded, gen.id]);
|
|
6296
|
+
return /* @__PURE__ */ u3("div", {
|
|
6297
|
+
class: "bg-panel rounded-lg border border-border p-4",
|
|
6298
|
+
children: [
|
|
6299
|
+
/* @__PURE__ */ u3("div", {
|
|
6300
|
+
class: "flex items-center justify-between",
|
|
6301
|
+
children: [
|
|
6302
|
+
/* @__PURE__ */ u3("div", {
|
|
6303
|
+
class: "flex items-center gap-3",
|
|
6304
|
+
children: [
|
|
6305
|
+
/* @__PURE__ */ u3("span", {
|
|
6306
|
+
class: "text-lg font-bold font-mono text-ink",
|
|
6307
|
+
children: [
|
|
6308
|
+
"#",
|
|
6309
|
+
gen.id
|
|
6310
|
+
]
|
|
6311
|
+
}, undefined, true, undefined, this),
|
|
6312
|
+
/* @__PURE__ */ u3("span", {
|
|
6313
|
+
title: STATE_DESCRIPTIONS[gen.state],
|
|
6314
|
+
children: /* @__PURE__ */ u3(StatusBadge, {
|
|
6315
|
+
status: gen.state,
|
|
6316
|
+
colorMap: STATE_COLORS
|
|
6317
|
+
}, undefined, false, undefined, this)
|
|
6318
|
+
}, undefined, false, undefined, this),
|
|
6319
|
+
gen.workerName && /* @__PURE__ */ u3("span", {
|
|
6320
|
+
class: "inline-flex px-2 py-0.5 rounded-md text-xs font-medium bg-panel-hover text-text-secondary",
|
|
6321
|
+
children: gen.workerName
|
|
6322
|
+
}, undefined, false, undefined, this)
|
|
6323
|
+
]
|
|
6324
|
+
}, undefined, true, undefined, this),
|
|
6325
|
+
/* @__PURE__ */ u3("div", {
|
|
6326
|
+
class: "flex items-center gap-2",
|
|
6327
|
+
children: [
|
|
6328
|
+
gen.state === "active" && /* @__PURE__ */ u3("button", {
|
|
6329
|
+
onClick: onReload,
|
|
6330
|
+
class: "rounded-md px-3 py-1.5 text-xs font-medium bg-panel border border-border text-text-secondary hover:bg-panel-hover transition-all",
|
|
6331
|
+
children: "Reload"
|
|
6332
|
+
}, undefined, false, undefined, this),
|
|
6333
|
+
gen.state === "draining" && /* @__PURE__ */ u3("button", {
|
|
6334
|
+
onClick: () => onStop(gen.id),
|
|
6335
|
+
class: "rounded-md px-3 py-1.5 text-xs font-medium bg-panel border border-border text-text-secondary btn-danger transition-all",
|
|
6336
|
+
children: "Force Stop"
|
|
6337
|
+
}, undefined, false, undefined, this)
|
|
6338
|
+
]
|
|
6339
|
+
}, undefined, true, undefined, this)
|
|
6340
|
+
]
|
|
6341
|
+
}, undefined, true, undefined, this),
|
|
6342
|
+
/* @__PURE__ */ u3("div", {
|
|
6343
|
+
class: "mt-3 flex items-center gap-4 text-xs text-text-muted",
|
|
6344
|
+
children: [
|
|
6345
|
+
/* @__PURE__ */ u3("span", {
|
|
6346
|
+
title: new Date(gen.createdAt).toLocaleString(),
|
|
6347
|
+
children: [
|
|
6348
|
+
"Loaded ",
|
|
6349
|
+
formatRelativeTime(gen.createdAt)
|
|
6350
|
+
]
|
|
6351
|
+
}, undefined, true, undefined, this),
|
|
6352
|
+
/* @__PURE__ */ u3("span", {
|
|
6353
|
+
title: "Number of HTTP requests currently being processed by this generation",
|
|
6354
|
+
children: [
|
|
6355
|
+
gen.activeRequests,
|
|
6356
|
+
" in-flight request",
|
|
6357
|
+
gen.activeRequests !== 1 ? "s" : ""
|
|
6358
|
+
]
|
|
6359
|
+
}, undefined, true, undefined, this)
|
|
6360
|
+
]
|
|
6361
|
+
}, undefined, true, undefined, this),
|
|
6362
|
+
gen.durableObjects && gen.durableObjects.length > 0 && /* @__PURE__ */ u3("div", {
|
|
6363
|
+
class: "mt-3 flex flex-wrap gap-2",
|
|
6364
|
+
children: gen.durableObjects.map((d3) => /* @__PURE__ */ u3("span", {
|
|
6365
|
+
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs bg-panel-hover text-text-secondary",
|
|
6366
|
+
title: `Durable Object class "${d3.namespace}": ${d3.activeInstances} active instance(s), ${d3.totalWebSockets} WebSocket connection(s)`,
|
|
6367
|
+
children: [
|
|
6368
|
+
d3.namespace,
|
|
6369
|
+
":",
|
|
6370
|
+
/* @__PURE__ */ u3("span", {
|
|
6371
|
+
class: "font-medium text-ink",
|
|
6372
|
+
children: d3.activeInstances
|
|
6373
|
+
}, undefined, false, undefined, this),
|
|
6374
|
+
" instance",
|
|
6375
|
+
d3.activeInstances !== 1 ? "s" : "",
|
|
6376
|
+
d3.totalWebSockets > 0 && /* @__PURE__ */ u3("span", {
|
|
6377
|
+
class: "text-text-muted",
|
|
6378
|
+
children: [
|
|
6379
|
+
"(",
|
|
6380
|
+
d3.totalWebSockets,
|
|
6381
|
+
" ws)"
|
|
6382
|
+
]
|
|
6383
|
+
}, undefined, true, undefined, this)
|
|
6384
|
+
]
|
|
6385
|
+
}, d3.namespace, true, undefined, this))
|
|
6386
|
+
}, undefined, false, undefined, this),
|
|
6387
|
+
/* @__PURE__ */ u3("button", {
|
|
6388
|
+
onClick: () => setExpanded(!expanded),
|
|
6389
|
+
class: "mt-2 text-xs text-text-muted hover:text-ink transition-colors",
|
|
6390
|
+
children: expanded ? "Hide DO instances" : "Show DO instances"
|
|
6391
|
+
}, undefined, false, undefined, this),
|
|
6392
|
+
expanded && detail && /* @__PURE__ */ u3("div", {
|
|
6393
|
+
class: "mt-3 border-t border-border pt-3",
|
|
6394
|
+
children: detail.doNamespaces.length === 0 ? /* @__PURE__ */ u3("div", {
|
|
6395
|
+
class: "text-xs text-text-muted",
|
|
6396
|
+
children: "No active Durable Object instances in this generation"
|
|
6397
|
+
}, undefined, false, undefined, this) : detail.doNamespaces.map((ns) => /* @__PURE__ */ u3("div", {
|
|
6398
|
+
class: "mb-3",
|
|
6399
|
+
children: [
|
|
6400
|
+
/* @__PURE__ */ u3("div", {
|
|
6401
|
+
class: "text-xs font-medium text-text-secondary mb-1",
|
|
6402
|
+
children: ns.namespace
|
|
6403
|
+
}, undefined, false, undefined, this),
|
|
6404
|
+
ns.instances.length === 0 ? /* @__PURE__ */ u3("div", {
|
|
6405
|
+
class: "text-xs text-text-muted ml-2",
|
|
6406
|
+
children: "No active instances"
|
|
6407
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ u3("div", {
|
|
6408
|
+
class: "ml-2 space-y-0.5",
|
|
6409
|
+
children: ns.instances.map((inst) => /* @__PURE__ */ u3("div", {
|
|
6410
|
+
class: "text-xs font-mono text-text-data flex items-center gap-2",
|
|
6411
|
+
children: [
|
|
6412
|
+
/* @__PURE__ */ u3("span", {
|
|
6413
|
+
class: "truncate max-w-[200px]",
|
|
6414
|
+
title: inst.id,
|
|
6415
|
+
children: inst.id
|
|
6416
|
+
}, undefined, false, undefined, this),
|
|
6417
|
+
inst.wsCount > 0 && /* @__PURE__ */ u3("span", {
|
|
6418
|
+
class: "text-text-muted",
|
|
6419
|
+
title: "Active WebSocket connections",
|
|
6420
|
+
children: [
|
|
6421
|
+
inst.wsCount,
|
|
6422
|
+
" ws"
|
|
6423
|
+
]
|
|
6424
|
+
}, undefined, true, undefined, this)
|
|
6425
|
+
]
|
|
6426
|
+
}, inst.id, true, undefined, this))
|
|
6427
|
+
}, undefined, false, undefined, this)
|
|
6428
|
+
]
|
|
6429
|
+
}, ns.namespace, true, undefined, this))
|
|
6430
|
+
}, undefined, false, undefined, this)
|
|
6431
|
+
]
|
|
6432
|
+
}, undefined, true, undefined, this);
|
|
6433
|
+
}
|
|
6434
|
+
function GenerationsView() {
|
|
6435
|
+
const { data, refetch } = useQuery("generations.list");
|
|
6436
|
+
const reload = useMutation("generations.reload");
|
|
6437
|
+
const drain = useMutation("generations.drain");
|
|
6438
|
+
const configMutation = useMutation("generations.config");
|
|
6439
|
+
const [gracePeriod, setGracePeriod] = d2("");
|
|
6440
|
+
y2(() => {
|
|
6441
|
+
if (data?.gracePeriodMs != null && gracePeriod === "") {
|
|
6442
|
+
setGracePeriod(String(data.gracePeriodMs));
|
|
6443
|
+
}
|
|
6444
|
+
}, [data?.gracePeriodMs]);
|
|
6445
|
+
y2(() => {
|
|
6446
|
+
const timer = setInterval(refetch, 2000);
|
|
6447
|
+
return () => clearInterval(timer);
|
|
6448
|
+
}, [refetch]);
|
|
6449
|
+
const handleReload = q2(async (workerName) => {
|
|
6450
|
+
await reload.mutate(workerName ? { workerName } : undefined);
|
|
6451
|
+
refetch();
|
|
6452
|
+
}, [reload, refetch]);
|
|
6453
|
+
const handleStop = q2(async (workerName) => {
|
|
6454
|
+
await drain.mutate(workerName ? { workerName } : undefined);
|
|
6455
|
+
refetch();
|
|
6456
|
+
}, [drain, refetch]);
|
|
6457
|
+
const handleGracePeriodSave = q2(() => {
|
|
6458
|
+
const ms = parseInt(gracePeriod, 10);
|
|
6459
|
+
if (Number.isNaN(ms) || ms < 0)
|
|
6460
|
+
return;
|
|
6461
|
+
configMutation.mutate({ gracePeriodMs: ms });
|
|
6462
|
+
}, [gracePeriod, configMutation]);
|
|
6463
|
+
if (!data) {
|
|
6464
|
+
return /* @__PURE__ */ u3("div", {
|
|
6465
|
+
class: "p-4 sm:p-8",
|
|
6466
|
+
children: [
|
|
6467
|
+
/* @__PURE__ */ u3(PageHeader, {
|
|
6468
|
+
title: "Generations"
|
|
6469
|
+
}, undefined, false, undefined, this),
|
|
6470
|
+
/* @__PURE__ */ u3("div", {
|
|
6471
|
+
class: "text-text-muted text-sm text-center py-12",
|
|
6472
|
+
children: "Loading..."
|
|
6473
|
+
}, undefined, false, undefined, this)
|
|
6474
|
+
]
|
|
6475
|
+
}, undefined, true, undefined, this);
|
|
6476
|
+
}
|
|
6477
|
+
const generations = data.generations;
|
|
6478
|
+
return /* @__PURE__ */ u3("div", {
|
|
6479
|
+
class: "p-4 sm:p-8",
|
|
6480
|
+
children: [
|
|
6481
|
+
/* @__PURE__ */ u3(PageHeader, {
|
|
6482
|
+
title: "Generations",
|
|
6483
|
+
subtitle: `${generations.length} generation(s)`,
|
|
6484
|
+
actions: /* @__PURE__ */ u3("div", {
|
|
6485
|
+
class: "flex items-center gap-2",
|
|
6486
|
+
children: [
|
|
6487
|
+
/* @__PURE__ */ u3("div", {
|
|
6488
|
+
class: "flex items-center gap-1",
|
|
6489
|
+
title: "How long to wait for in-flight requests to finish before force-stopping an old generation",
|
|
6490
|
+
children: [
|
|
6491
|
+
/* @__PURE__ */ u3("label", {
|
|
6492
|
+
class: "text-xs text-text-muted",
|
|
6493
|
+
children: "Grace period (ms):"
|
|
6494
|
+
}, undefined, false, undefined, this),
|
|
6495
|
+
/* @__PURE__ */ u3("input", {
|
|
6496
|
+
type: "number",
|
|
6497
|
+
value: gracePeriod,
|
|
6498
|
+
onInput: (e3) => setGracePeriod(e3.target.value),
|
|
6499
|
+
onBlur: handleGracePeriodSave,
|
|
6500
|
+
class: "bg-panel border border-border rounded-md px-2 py-1 text-xs w-24 outline-none focus:border-border focus:ring-1 focus:ring-border"
|
|
6501
|
+
}, undefined, false, undefined, this)
|
|
6502
|
+
]
|
|
6503
|
+
}, undefined, true, undefined, this),
|
|
6504
|
+
/* @__PURE__ */ u3("button", {
|
|
6505
|
+
onClick: () => handleReload(),
|
|
6506
|
+
disabled: reload.isLoading,
|
|
6507
|
+
class: "rounded-md px-3 py-1.5 text-sm font-medium bg-accent-lime text-surface hover:bg-accent-lime/90 disabled:opacity-50 transition-all",
|
|
6508
|
+
children: reload.isLoading ? "Reloading..." : "Reload"
|
|
6509
|
+
}, undefined, false, undefined, this)
|
|
6510
|
+
]
|
|
6511
|
+
}, undefined, true, undefined, this)
|
|
6512
|
+
}, undefined, false, undefined, this),
|
|
6513
|
+
/* @__PURE__ */ u3("p", {
|
|
6514
|
+
class: "text-xs text-text-muted mb-6 max-w-xl leading-relaxed",
|
|
6515
|
+
children: [
|
|
6516
|
+
"A generation is a snapshot of your worker code at a point in time. When code changes, a new generation is created and the old one is drained (finishes in-flight work, then stops). Each request and Durable Object is tied to the generation that was active when it started. The",
|
|
6517
|
+
" ",
|
|
6518
|
+
/* @__PURE__ */ u3("span", {
|
|
6519
|
+
class: "font-mono",
|
|
6520
|
+
children: "Gen"
|
|
6521
|
+
}, undefined, false, undefined, this),
|
|
6522
|
+
" column in Traces shows which generation handled each request."
|
|
6523
|
+
]
|
|
6524
|
+
}, undefined, true, undefined, this),
|
|
6525
|
+
generations.length === 0 ? /* @__PURE__ */ u3(EmptyState, {
|
|
6526
|
+
message: "No generations yet. Start your dev server to see generations here."
|
|
6527
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ u3("div", {
|
|
6528
|
+
class: "space-y-4",
|
|
6529
|
+
children: generations.map((gen) => /* @__PURE__ */ u3(GenerationCard, {
|
|
6530
|
+
gen,
|
|
6531
|
+
onReload: () => handleReload(),
|
|
6532
|
+
onStop: (id) => handleStop()
|
|
6533
|
+
}, gen.id, false, undefined, this))
|
|
6534
|
+
}, undefined, false, undefined, this),
|
|
6535
|
+
data.workers && data.workers.length > 0 && /* @__PURE__ */ u3("div", {
|
|
6536
|
+
class: "mt-8",
|
|
6537
|
+
children: [
|
|
6538
|
+
/* @__PURE__ */ u3("h2", {
|
|
6539
|
+
class: "text-lg font-bold text-ink mb-1",
|
|
6540
|
+
children: "Per-Worker Generations"
|
|
6541
|
+
}, undefined, false, undefined, this),
|
|
6542
|
+
/* @__PURE__ */ u3("p", {
|
|
6543
|
+
class: "text-xs text-text-muted mb-4",
|
|
6544
|
+
children: "In multi-worker mode, each worker has its own independent generation lifecycle."
|
|
6545
|
+
}, undefined, false, undefined, this),
|
|
6546
|
+
data.workers.map((w3) => /* @__PURE__ */ u3("div", {
|
|
6547
|
+
class: "mb-6",
|
|
6548
|
+
children: [
|
|
6549
|
+
/* @__PURE__ */ u3("div", {
|
|
6550
|
+
class: "flex items-center gap-3 mb-3",
|
|
6551
|
+
children: [
|
|
6552
|
+
/* @__PURE__ */ u3("h3", {
|
|
6553
|
+
class: "text-sm font-bold text-ink",
|
|
6554
|
+
children: w3.workerName
|
|
6555
|
+
}, undefined, false, undefined, this),
|
|
6556
|
+
/* @__PURE__ */ u3("span", {
|
|
6557
|
+
class: "text-xs text-text-muted",
|
|
6558
|
+
children: [
|
|
6559
|
+
w3.generations.length,
|
|
6560
|
+
" generation(s)"
|
|
6561
|
+
]
|
|
6562
|
+
}, undefined, true, undefined, this),
|
|
6563
|
+
/* @__PURE__ */ u3("button", {
|
|
6564
|
+
onClick: () => handleReload(w3.workerName),
|
|
6565
|
+
class: "rounded-md px-2 py-1 text-xs font-medium bg-panel border border-border text-text-secondary hover:bg-panel-hover transition-all",
|
|
6566
|
+
children: "Reload"
|
|
6567
|
+
}, undefined, false, undefined, this)
|
|
6568
|
+
]
|
|
6569
|
+
}, undefined, true, undefined, this),
|
|
6570
|
+
/* @__PURE__ */ u3("div", {
|
|
6571
|
+
class: "space-y-3",
|
|
6572
|
+
children: w3.generations.map((gen) => /* @__PURE__ */ u3(GenerationCard, {
|
|
6573
|
+
gen,
|
|
6574
|
+
onReload: () => handleReload(w3.workerName),
|
|
6575
|
+
onStop: () => handleStop(w3.workerName)
|
|
6576
|
+
}, gen.id, false, undefined, this))
|
|
6577
|
+
}, undefined, false, undefined, this)
|
|
6578
|
+
]
|
|
6579
|
+
}, w3.workerName, true, undefined, this))
|
|
6580
|
+
]
|
|
6581
|
+
}, undefined, true, undefined, this)
|
|
6582
|
+
]
|
|
6583
|
+
}, undefined, true, undefined, this);
|
|
6584
|
+
}
|
|
6585
|
+
|
|
6267
6586
|
// src/dashboard/views/home.tsx
|
|
6268
6587
|
var INVENTORY = [
|
|
6269
6588
|
{ key: "kv", label: "KV", path: "/kv", icon: "kv" },
|
|
@@ -7688,7 +8007,8 @@ function useTraceStream() {
|
|
|
7688
8007
|
startTime: s3.startTime,
|
|
7689
8008
|
durationMs: s3.durationMs,
|
|
7690
8009
|
spanCount: 1,
|
|
7691
|
-
errorCount: 0
|
|
8010
|
+
errorCount: 0,
|
|
8011
|
+
generationId: typeof s3.attributes["lopata.generation_id"] === "number" ? s3.attributes["lopata.generation_id"] : null
|
|
7692
8012
|
});
|
|
7693
8013
|
} else if (event.type === "span.end" && event.span.parentSpanId === null) {
|
|
7694
8014
|
const s3 = event.span;
|
|
@@ -7970,6 +8290,10 @@ function TracesListView() {
|
|
|
7970
8290
|
class: "text-left px-4 py-2.5 text-xs text-text-muted font-medium",
|
|
7971
8291
|
children: "Worker"
|
|
7972
8292
|
}, undefined, false, undefined, this),
|
|
8293
|
+
/* @__PURE__ */ u3("th", {
|
|
8294
|
+
class: "text-left px-4 py-2.5 text-xs text-text-muted font-medium",
|
|
8295
|
+
children: "Gen"
|
|
8296
|
+
}, undefined, false, undefined, this),
|
|
7973
8297
|
/* @__PURE__ */ u3("th", {
|
|
7974
8298
|
class: "text-left px-4 py-2.5 text-xs text-text-muted font-medium",
|
|
7975
8299
|
style: { minWidth: "140px" },
|
|
@@ -8017,6 +8341,16 @@ function TracesListView() {
|
|
|
8017
8341
|
children: trace.workerName
|
|
8018
8342
|
}, undefined, false, undefined, this)
|
|
8019
8343
|
}, undefined, false, undefined, this),
|
|
8344
|
+
/* @__PURE__ */ u3("td", {
|
|
8345
|
+
class: "px-4 py-2.5",
|
|
8346
|
+
children: trace.generationId != null && /* @__PURE__ */ u3("span", {
|
|
8347
|
+
class: "inline-flex px-1.5 py-0.5 rounded text-[10px] font-mono font-medium bg-panel-hover text-text-secondary",
|
|
8348
|
+
children: [
|
|
8349
|
+
"#",
|
|
8350
|
+
trace.generationId
|
|
8351
|
+
]
|
|
8352
|
+
}, undefined, true, undefined, this)
|
|
8353
|
+
}, undefined, false, undefined, this),
|
|
8020
8354
|
/* @__PURE__ */ u3("td", {
|
|
8021
8355
|
class: "px-4 py-2.5",
|
|
8022
8356
|
children: /* @__PURE__ */ u3(DurationBar, {
|
|
@@ -8113,6 +8447,13 @@ function TraceDetailPage({ traceId }) {
|
|
|
8113
8447
|
class: "ml-2 inline-flex px-2 py-0.5 rounded-md text-xs font-medium bg-panel-hover text-text-secondary",
|
|
8114
8448
|
children: rootSpan.workerName
|
|
8115
8449
|
}, undefined, false, undefined, this),
|
|
8450
|
+
typeof rootSpan?.attributes["lopata.generation_id"] === "number" && /* @__PURE__ */ u3("span", {
|
|
8451
|
+
class: "ml-2 inline-flex px-1.5 py-0.5 rounded text-[10px] font-mono font-medium bg-panel-hover text-text-secondary",
|
|
8452
|
+
children: [
|
|
8453
|
+
"Gen #",
|
|
8454
|
+
rootSpan.attributes["lopata.generation_id"]
|
|
8455
|
+
]
|
|
8456
|
+
}, undefined, true, undefined, this),
|
|
8116
8457
|
rootSpan?.durationMs != null && /* @__PURE__ */ u3("span", {
|
|
8117
8458
|
class: "ml-2 text-text-secondary",
|
|
8118
8459
|
children: formatDuration(rootSpan.durationMs)
|
|
@@ -9539,6 +9880,8 @@ function App() {
|
|
|
9539
9880
|
}, undefined, false, undefined, this);
|
|
9540
9881
|
if (route.startsWith("/workers"))
|
|
9541
9882
|
return /* @__PURE__ */ u3(WorkersView, {}, undefined, false, undefined, this);
|
|
9883
|
+
if (route.startsWith("/generations"))
|
|
9884
|
+
return /* @__PURE__ */ u3(GenerationsView, {}, undefined, false, undefined, this);
|
|
9542
9885
|
if (route.startsWith("/kv"))
|
|
9543
9886
|
return /* @__PURE__ */ u3(KvView, {
|
|
9544
9887
|
route
|
|
@@ -9655,8 +9998,26 @@ function App() {
|
|
|
9655
9998
|
}, undefined, true, undefined, this),
|
|
9656
9999
|
/* @__PURE__ */ u3("div", {
|
|
9657
10000
|
class: "border-t border-border px-2 py-2",
|
|
9658
|
-
children:
|
|
9659
|
-
|
|
10001
|
+
children: [
|
|
10002
|
+
/* @__PURE__ */ u3("a", {
|
|
10003
|
+
href: "#/generations",
|
|
10004
|
+
class: `flex items-center gap-2 w-full px-3 py-1.5 text-xs font-mono rounded-md transition-colors ${activeSection === "/generations" ? "text-ink bg-panel-hover" : "text-text-muted hover:text-ink hover:bg-panel-hover"}`,
|
|
10005
|
+
children: [
|
|
10006
|
+
/* @__PURE__ */ u3("span", {
|
|
10007
|
+
class: "w-4 text-center opacity-60",
|
|
10008
|
+
children: /* @__PURE__ */ u3(BindingIcon, {
|
|
10009
|
+
type: "workers",
|
|
10010
|
+
class: "w-4 text-center opacity-60 flex items-center justify-center"
|
|
10011
|
+
}, undefined, false, undefined, this)
|
|
10012
|
+
}, undefined, false, undefined, this),
|
|
10013
|
+
/* @__PURE__ */ u3("span", {
|
|
10014
|
+
children: "Generations"
|
|
10015
|
+
}, undefined, false, undefined, this)
|
|
10016
|
+
]
|
|
10017
|
+
}, undefined, true, undefined, this),
|
|
10018
|
+
/* @__PURE__ */ u3(ThemeSwitcher, {}, undefined, false, undefined, this)
|
|
10019
|
+
]
|
|
10020
|
+
}, undefined, true, undefined, this)
|
|
9660
10021
|
]
|
|
9661
10022
|
}, undefined, true, undefined, this);
|
|
9662
10023
|
return /* @__PURE__ */ u3("div", {
|
|
@@ -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-csyd2tq2.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-yxzrcvyh.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lopata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"lopata": "src/cli.ts"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
"./vite-plugin": "./src/vite-plugin/index.ts"
|
|
14
|
+
"./vite-plugin": "./src/vite-plugin/index.ts",
|
|
15
|
+
"./testing": "./src/testing/index.ts",
|
|
16
|
+
"./testing/setup": "./src/testing/setup.ts"
|
|
15
17
|
},
|
|
16
18
|
"files": [
|
|
17
19
|
"src/",
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
"scripts": {
|
|
23
25
|
"dev": "cd examples/playground && bun ../../src/cli.ts dev",
|
|
24
26
|
"cli": "bun src/cli.ts",
|
|
25
|
-
"test": "bun test tests/",
|
|
27
|
+
"test": "bun test tests/ examples/playground/tests/",
|
|
26
28
|
"lint": "biome lint .",
|
|
27
29
|
"lint:fix": "biome lint . --write",
|
|
28
30
|
"format": "dprint fmt",
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import type { GenerationInfo, GenerationsData, HandlerContext, OkResponse } from '../types'
|
|
1
|
+
import type { GenerationDetail, GenerationInfo, GenerationsData, HandlerContext, OkResponse } from '../types'
|
|
2
2
|
|
|
3
3
|
export const handlers = {
|
|
4
|
-
'generations.
|
|
5
|
-
|
|
4
|
+
'generations.detail'(input: { id: number; workerName?: string }, ctx: HandlerContext): GenerationDetail {
|
|
5
|
+
let manager = ctx.manager
|
|
6
|
+
if (input.workerName && ctx.registry) {
|
|
7
|
+
manager = ctx.registry.getManager(input.workerName) ?? null
|
|
8
|
+
}
|
|
9
|
+
if (!manager) throw new Error('Generation manager not available')
|
|
10
|
+
const gen = manager.get(input.id)
|
|
11
|
+
if (!gen) throw new Error(`Generation ${input.id} not found`)
|
|
12
|
+
const info = gen.getInfo()
|
|
13
|
+
const doNamespaces = gen.registry.durableObjects.map(entry => ({
|
|
14
|
+
namespace: entry.className,
|
|
15
|
+
instances: entry.namespace._listActiveExecutors(),
|
|
16
|
+
}))
|
|
17
|
+
return { ...info, doNamespaces }
|
|
18
|
+
},
|
|
6
19
|
|
|
20
|
+
'generations.list'(_input: {}, ctx: HandlerContext): GenerationsData {
|
|
7
21
|
const result: GenerationsData = {
|
|
8
|
-
generations: ctx.manager
|
|
9
|
-
gracePeriodMs: ctx.manager
|
|
22
|
+
generations: ctx.manager?.list() ?? [],
|
|
23
|
+
gracePeriodMs: ctx.manager?.gracePeriodMs ?? 10_000,
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
// Include per-worker data when registry is available (multi-worker mode)
|
package/src/api/types.ts
CHANGED
|
@@ -240,6 +240,20 @@ export interface GenerationsData {
|
|
|
240
240
|
workers?: WorkerGenerations[]
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
export interface GenerationDoInstance {
|
|
244
|
+
id: string
|
|
245
|
+
wsCount: number
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface GenerationDoNamespace {
|
|
249
|
+
namespace: string
|
|
250
|
+
instances: GenerationDoInstance[]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface GenerationDetail extends GenerationInfo {
|
|
254
|
+
doNamespaces: GenerationDoNamespace[]
|
|
255
|
+
}
|
|
256
|
+
|
|
243
257
|
// Workers
|
|
244
258
|
export interface WorkerBinding {
|
|
245
259
|
type: string
|
package/src/bindings/cache.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import type { Clock } from '../testing/clock'
|
|
3
|
+
import { realClock } from '../testing/clock'
|
|
2
4
|
|
|
3
5
|
export interface CacheLimits {
|
|
4
6
|
maxBodySize?: number // default 512 MiB (CF limit)
|
|
@@ -42,12 +44,12 @@ function parseCacheControlMaxAge(header: string | null): number | null {
|
|
|
42
44
|
* Compute the expiration timestamp (ms since epoch) from response headers.
|
|
43
45
|
* Returns null if no expiration info is present (cache indefinitely).
|
|
44
46
|
*/
|
|
45
|
-
function computeExpiresAt(headers: Headers): number | null {
|
|
47
|
+
function computeExpiresAt(headers: Headers, now: number): number | null {
|
|
46
48
|
const cacheControl = headers.get('cache-control')
|
|
47
49
|
const maxAge = parseCacheControlMaxAge(cacheControl)
|
|
48
50
|
|
|
49
51
|
if (maxAge === -1) return -1 // signal: no-store
|
|
50
|
-
if (maxAge !== null) return
|
|
52
|
+
if (maxAge !== null) return now + maxAge * 1000
|
|
51
53
|
|
|
52
54
|
// Fallback to Expires header
|
|
53
55
|
const expires = headers.get('expires')
|
|
@@ -63,11 +65,13 @@ export class SqliteCache {
|
|
|
63
65
|
private db: Database
|
|
64
66
|
private cacheName: string
|
|
65
67
|
private limits: Required<CacheLimits>
|
|
68
|
+
private clock: Clock
|
|
66
69
|
|
|
67
|
-
constructor(db: Database, cacheName: string, limits?: CacheLimits) {
|
|
70
|
+
constructor(db: Database, cacheName: string, limits?: CacheLimits, clock?: Clock) {
|
|
68
71
|
this.db = db
|
|
69
72
|
this.cacheName = cacheName
|
|
70
73
|
this.limits = { ...CACHE_DEFAULTS, ...limits }
|
|
74
|
+
this.clock = clock ?? realClock
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
async match(request: Request | string, options?: { ignoreMethod?: boolean }): Promise<Response | undefined> {
|
|
@@ -86,7 +90,7 @@ export class SqliteCache {
|
|
|
86
90
|
if (!row) return undefined
|
|
87
91
|
|
|
88
92
|
// Check expiration — lazily delete expired entries
|
|
89
|
-
if (row.expires_at !== null && row.expires_at <=
|
|
93
|
+
if (row.expires_at !== null && row.expires_at <= this.clock.now()) {
|
|
90
94
|
this.db.query(
|
|
91
95
|
'DELETE FROM cache_entries WHERE cache_name = ? AND url = ?',
|
|
92
96
|
).run(this.cacheName, url)
|
|
@@ -123,7 +127,7 @@ export class SqliteCache {
|
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
// Parse expiration from Cache-Control / Expires
|
|
126
|
-
const expiresAt = computeExpiresAt(response.headers)
|
|
130
|
+
const expiresAt = computeExpiresAt(response.headers, this.clock.now())
|
|
127
131
|
|
|
128
132
|
// no-store — don't cache at all
|
|
129
133
|
if (expiresAt === -1) {
|
|
@@ -165,15 +169,17 @@ export class SqliteCache {
|
|
|
165
169
|
export class SqliteCacheStorage {
|
|
166
170
|
private db: Database
|
|
167
171
|
private limits?: CacheLimits
|
|
172
|
+
private clock: Clock
|
|
168
173
|
public default: SqliteCache
|
|
169
174
|
|
|
170
|
-
constructor(db: Database, limits?: CacheLimits) {
|
|
175
|
+
constructor(db: Database, limits?: CacheLimits, clock?: Clock) {
|
|
171
176
|
this.db = db
|
|
172
177
|
this.limits = limits
|
|
173
|
-
this.
|
|
178
|
+
this.clock = clock ?? realClock
|
|
179
|
+
this.default = new SqliteCache(db, 'default', limits, this.clock)
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
async open(cacheName: string): Promise<SqliteCache> {
|
|
177
|
-
return new SqliteCache(this.db, cacheName, this.limits)
|
|
183
|
+
return new SqliteCache(this.db, cacheName, this.limits, this.clock)
|
|
178
184
|
}
|
|
179
185
|
}
|