@korajs/devtools 0.1.2 → 0.3.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/extension/background.cjs +1 -1
- package/dist/extension/background.cjs.map +1 -1
- package/dist/extension/background.js +54 -4
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/content-script.cjs +1 -1
- package/dist/extension/content-script.cjs.map +1 -1
- package/dist/extension/content-script.js +1 -1
- package/dist/extension/content-script.js.map +1 -1
- package/dist/extension/devtools.cjs +2 -3
- package/dist/extension/devtools.cjs.map +1 -1
- package/dist/extension/devtools.html +599 -0
- package/dist/extension/devtools.js +2 -3
- package/dist/extension/devtools.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/panel.cjs +1013 -40
- package/dist/extension/panel.cjs.map +1 -1
- package/dist/extension/panel.js +1176 -9
- package/dist/extension/panel.js.map +1 -1
- package/dist/index.cjs +466 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +674 -8
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/dist/chunk-4ZQ2RTZM.js +0 -203
- package/dist/chunk-4ZQ2RTZM.js.map +0 -1
- package/dist/chunk-JH2X4T4Z.js +0 -58
- package/dist/chunk-JH2X4T4Z.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PortRouter
|
|
3
|
-
} from "./chunk-JH2X4T4Z.js";
|
|
4
|
-
import {
|
|
5
|
-
buildPanelModel,
|
|
6
|
-
renderDevtoolsPanel
|
|
7
|
-
} from "./chunk-4ZQ2RTZM.js";
|
|
8
|
-
|
|
9
1
|
// src/bridge/message-bridge.ts
|
|
10
2
|
var DEFAULT_CHANNEL = "kora-devtools";
|
|
11
3
|
var MessageBridge = class {
|
|
@@ -369,6 +361,680 @@ function computeStatistics(events) {
|
|
|
369
361
|
syncOperationsReceived
|
|
370
362
|
};
|
|
371
363
|
}
|
|
364
|
+
|
|
365
|
+
// src/ui/panel-state.ts
|
|
366
|
+
function buildPanelModel(events) {
|
|
367
|
+
const timeline = events.map((entry) => ({
|
|
368
|
+
id: entry.id,
|
|
369
|
+
type: entry.event.type,
|
|
370
|
+
label: timelineLabel(entry.event),
|
|
371
|
+
color: timelineColor(entry.event.type),
|
|
372
|
+
receivedAt: entry.receivedAt,
|
|
373
|
+
dependsOn: extractCausalDependencies(entry.event)
|
|
374
|
+
}));
|
|
375
|
+
const conflicts = events.flatMap((entry) => {
|
|
376
|
+
if (entry.event.type !== "merge:completed" && entry.event.type !== "merge:conflict" && entry.event.type !== "constraint:violated") {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const trace = entry.event.trace;
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
id: entry.id,
|
|
383
|
+
timestamp: entry.receivedAt,
|
|
384
|
+
collection: trace.operationA.collection,
|
|
385
|
+
field: trace.field,
|
|
386
|
+
strategy: trace.strategy,
|
|
387
|
+
tier: trace.tier,
|
|
388
|
+
inputA: trace.inputA,
|
|
389
|
+
inputB: trace.inputB,
|
|
390
|
+
output: trace.output,
|
|
391
|
+
constraintViolated: trace.constraintViolated
|
|
392
|
+
}
|
|
393
|
+
];
|
|
394
|
+
});
|
|
395
|
+
const operations = events.map((entry) => {
|
|
396
|
+
const operation = extractOperation(entry.event);
|
|
397
|
+
if (!operation) return null;
|
|
398
|
+
return {
|
|
399
|
+
id: entry.id,
|
|
400
|
+
timestamp: entry.receivedAt,
|
|
401
|
+
operationId: operation.id,
|
|
402
|
+
collection: operation.collection,
|
|
403
|
+
recordId: operation.recordId,
|
|
404
|
+
opType: operation.type,
|
|
405
|
+
data: operation.data,
|
|
406
|
+
causalDeps: operation.causalDeps,
|
|
407
|
+
nodeId: operation.nodeId,
|
|
408
|
+
sequenceNumber: operation.sequenceNumber
|
|
409
|
+
};
|
|
410
|
+
}).filter((item) => item !== null);
|
|
411
|
+
const network = buildNetworkStatus(events, operations);
|
|
412
|
+
return {
|
|
413
|
+
timeline,
|
|
414
|
+
conflicts,
|
|
415
|
+
operations,
|
|
416
|
+
network
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function buildNetworkStatus(events, operations) {
|
|
420
|
+
let connected = false;
|
|
421
|
+
let quality = null;
|
|
422
|
+
let pendingAcks = 0;
|
|
423
|
+
let lastSyncAt = null;
|
|
424
|
+
let sentOps = 0;
|
|
425
|
+
let receivedOps = 0;
|
|
426
|
+
for (const entry of events) {
|
|
427
|
+
switch (entry.event.type) {
|
|
428
|
+
case "sync:connected":
|
|
429
|
+
connected = true;
|
|
430
|
+
lastSyncAt = entry.receivedAt;
|
|
431
|
+
break;
|
|
432
|
+
case "sync:disconnected":
|
|
433
|
+
connected = false;
|
|
434
|
+
break;
|
|
435
|
+
case "connection:quality":
|
|
436
|
+
quality = entry.event.quality;
|
|
437
|
+
break;
|
|
438
|
+
case "sync:sent":
|
|
439
|
+
sentOps += entry.event.operations.length;
|
|
440
|
+
pendingAcks += entry.event.operations.length;
|
|
441
|
+
lastSyncAt = entry.receivedAt;
|
|
442
|
+
break;
|
|
443
|
+
case "sync:received":
|
|
444
|
+
receivedOps += entry.event.operations.length;
|
|
445
|
+
lastSyncAt = entry.receivedAt;
|
|
446
|
+
break;
|
|
447
|
+
case "sync:acknowledged":
|
|
448
|
+
pendingAcks = Math.max(0, pendingAcks - 1);
|
|
449
|
+
lastSyncAt = entry.receivedAt;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const vector = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const operation of operations) {
|
|
455
|
+
const current = vector.get(operation.nodeId) ?? 0;
|
|
456
|
+
if (operation.sequenceNumber > current) {
|
|
457
|
+
vector.set(operation.nodeId, operation.sequenceNumber);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
connected,
|
|
462
|
+
quality,
|
|
463
|
+
pendingAcks,
|
|
464
|
+
lastSyncAt,
|
|
465
|
+
sentOps,
|
|
466
|
+
receivedOps,
|
|
467
|
+
versionVector: [...vector.entries()].map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber })).sort((left, right) => left.nodeId.localeCompare(right.nodeId))
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function timelineLabel(event) {
|
|
471
|
+
switch (event.type) {
|
|
472
|
+
case "operation:created":
|
|
473
|
+
case "operation:applied":
|
|
474
|
+
return `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`;
|
|
475
|
+
case "merge:started":
|
|
476
|
+
return `merge start ${event.operationA.collection}`;
|
|
477
|
+
case "merge:completed":
|
|
478
|
+
return `merge complete ${event.trace.field}`;
|
|
479
|
+
case "merge:conflict":
|
|
480
|
+
return `merge conflict ${event.trace.field}`;
|
|
481
|
+
case "constraint:violated":
|
|
482
|
+
return `constraint ${event.constraint}`;
|
|
483
|
+
case "sync:connected":
|
|
484
|
+
return `sync connected ${event.nodeId}`;
|
|
485
|
+
case "sync:disconnected":
|
|
486
|
+
return `sync disconnected`;
|
|
487
|
+
case "sync:sent":
|
|
488
|
+
return `sync sent ${event.batchSize}`;
|
|
489
|
+
case "sync:received":
|
|
490
|
+
return `sync received ${event.batchSize}`;
|
|
491
|
+
case "sync:acknowledged":
|
|
492
|
+
return `sync ack ${event.sequenceNumber}`;
|
|
493
|
+
case "query:subscribed":
|
|
494
|
+
return `query subscribed ${event.collection}`;
|
|
495
|
+
case "query:invalidated":
|
|
496
|
+
return `query invalidated ${event.queryId}`;
|
|
497
|
+
case "query:executed":
|
|
498
|
+
return `query executed ${event.queryId}`;
|
|
499
|
+
case "connection:quality":
|
|
500
|
+
return `connection ${event.quality}`;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function timelineColor(type) {
|
|
504
|
+
if (type.startsWith("operation:")) return "#22c55e";
|
|
505
|
+
if (type.startsWith("sync:")) return "#a855f7";
|
|
506
|
+
if (type.startsWith("merge:") || type.startsWith("constraint:")) return "#f59e0b";
|
|
507
|
+
if (type.startsWith("query:")) return "#0ea5e9";
|
|
508
|
+
return "#64748b";
|
|
509
|
+
}
|
|
510
|
+
function extractCausalDependencies(event) {
|
|
511
|
+
const operation = extractOperation(event);
|
|
512
|
+
return operation?.causalDeps ?? [];
|
|
513
|
+
}
|
|
514
|
+
function extractOperation(event) {
|
|
515
|
+
switch (event.type) {
|
|
516
|
+
case "operation:created":
|
|
517
|
+
case "operation:applied":
|
|
518
|
+
return event.operation;
|
|
519
|
+
case "query:invalidated":
|
|
520
|
+
return event.trigger;
|
|
521
|
+
default:
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/ui/panel.ts
|
|
527
|
+
import { h, render } from "preact";
|
|
528
|
+
import { useState, useCallback, useMemo } from "preact/hooks";
|
|
529
|
+
import htm from "htm";
|
|
530
|
+
|
|
531
|
+
// src/ui/components.ts
|
|
532
|
+
function formatTime(timestamp) {
|
|
533
|
+
const d = new Date(timestamp);
|
|
534
|
+
const h2 = String(d.getHours()).padStart(2, "0");
|
|
535
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
536
|
+
const s = String(d.getSeconds()).padStart(2, "0");
|
|
537
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
538
|
+
return `${h2}:${m}:${s}.${ms}`;
|
|
539
|
+
}
|
|
540
|
+
function truncate(str, max) {
|
|
541
|
+
if (str.length <= max) return str;
|
|
542
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
543
|
+
}
|
|
544
|
+
function formatValue(value) {
|
|
545
|
+
if (value === null || value === void 0) return "null";
|
|
546
|
+
if (typeof value === "string") return `"${value}"`;
|
|
547
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
548
|
+
return String(value);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/ui/panel.ts
|
|
552
|
+
var html = htm.bind(h);
|
|
553
|
+
var ALL_CATEGORIES = ["operation", "merge", "sync", "query", "connection"];
|
|
554
|
+
function DevToolsPanel({ events }) {
|
|
555
|
+
const [activeTab, setActiveTab] = useState("timeline");
|
|
556
|
+
const [search, setSearch] = useState("");
|
|
557
|
+
const [categories, setCategories] = useState(
|
|
558
|
+
() => new Set(ALL_CATEGORIES)
|
|
559
|
+
);
|
|
560
|
+
const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
|
|
561
|
+
const [paused, setPaused] = useState(false);
|
|
562
|
+
const toggleCategory = useCallback(
|
|
563
|
+
(cat) => {
|
|
564
|
+
setCategories((prev) => {
|
|
565
|
+
const next = new Set(prev);
|
|
566
|
+
if (next.has(cat)) next.delete(cat);
|
|
567
|
+
else next.add(cat);
|
|
568
|
+
return next;
|
|
569
|
+
});
|
|
570
|
+
},
|
|
571
|
+
[]
|
|
572
|
+
);
|
|
573
|
+
const toggleExpand = useCallback(
|
|
574
|
+
(id) => {
|
|
575
|
+
setExpanded((prev) => {
|
|
576
|
+
const next = new Set(prev);
|
|
577
|
+
if (next.has(id)) next.delete(id);
|
|
578
|
+
else next.add(id);
|
|
579
|
+
return next;
|
|
580
|
+
});
|
|
581
|
+
},
|
|
582
|
+
[]
|
|
583
|
+
);
|
|
584
|
+
const filtered = useMemo(
|
|
585
|
+
() => filterEvents(events, { categories: [...categories] }),
|
|
586
|
+
[events, categories]
|
|
587
|
+
);
|
|
588
|
+
const model = useMemo(() => buildPanelModel(filtered), [filtered]);
|
|
589
|
+
const stats = useMemo(() => computeStatistics(events), [events]);
|
|
590
|
+
return html`
|
|
591
|
+
<div class="kora-devtools">
|
|
592
|
+
<${Toolbar}
|
|
593
|
+
activeTab=${activeTab}
|
|
594
|
+
onTabChange=${setActiveTab}
|
|
595
|
+
totalEvents=${events.length}
|
|
596
|
+
conflicts=${stats.mergeConflicts}
|
|
597
|
+
search=${search}
|
|
598
|
+
onSearchChange=${setSearch}
|
|
599
|
+
paused=${paused}
|
|
600
|
+
onPauseToggle=${() => setPaused((p) => !p)}
|
|
601
|
+
onClear=${() => setExpanded(/* @__PURE__ */ new Set())}
|
|
602
|
+
categories=${categories}
|
|
603
|
+
onToggleCategory=${toggleCategory}
|
|
604
|
+
/>
|
|
605
|
+
<div class="kora-content">
|
|
606
|
+
${activeTab === "timeline" && html`<${TimelinePanel}
|
|
607
|
+
items=${model.timeline}
|
|
608
|
+
search=${search}
|
|
609
|
+
expanded=${expanded}
|
|
610
|
+
onToggle=${toggleExpand}
|
|
611
|
+
/>`}
|
|
612
|
+
${activeTab === "conflicts" && html`<${ConflictsPanel}
|
|
613
|
+
items=${model.conflicts}
|
|
614
|
+
search=${search}
|
|
615
|
+
expanded=${expanded}
|
|
616
|
+
onToggle=${toggleExpand}
|
|
617
|
+
/>`}
|
|
618
|
+
${activeTab === "operations" && html`<${OperationsPanel}
|
|
619
|
+
items=${model.operations}
|
|
620
|
+
search=${search}
|
|
621
|
+
expanded=${expanded}
|
|
622
|
+
onToggle=${toggleExpand}
|
|
623
|
+
/>`}
|
|
624
|
+
${activeTab === "network" && html`<${NetworkPanel} network=${model.network} events=${events} />`}
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
function Toolbar({
|
|
630
|
+
activeTab,
|
|
631
|
+
onTabChange,
|
|
632
|
+
totalEvents,
|
|
633
|
+
conflicts,
|
|
634
|
+
search,
|
|
635
|
+
onSearchChange,
|
|
636
|
+
paused,
|
|
637
|
+
onPauseToggle,
|
|
638
|
+
onClear,
|
|
639
|
+
categories,
|
|
640
|
+
onToggleCategory
|
|
641
|
+
}) {
|
|
642
|
+
const tabs = [
|
|
643
|
+
{ id: "timeline", label: "Timeline", badge: totalEvents },
|
|
644
|
+
{ id: "conflicts", label: "Conflicts", badge: conflicts },
|
|
645
|
+
{ id: "operations", label: "Operations" },
|
|
646
|
+
{ id: "network", label: "Network" }
|
|
647
|
+
];
|
|
648
|
+
return html`
|
|
649
|
+
<div class="kora-toolbar">
|
|
650
|
+
<div class="kora-tabs">
|
|
651
|
+
${tabs.map(
|
|
652
|
+
(tab) => html`
|
|
653
|
+
<button
|
|
654
|
+
class="kora-tab${activeTab === tab.id ? " active" : ""}"
|
|
655
|
+
onClick=${() => onTabChange(tab.id)}
|
|
656
|
+
>
|
|
657
|
+
${tab.label}
|
|
658
|
+
${tab.badge != null && tab.badge > 0 ? html`<span class="kora-badge">${tab.badge}</span>` : null}
|
|
659
|
+
</button>
|
|
660
|
+
`
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
<div class="kora-controls">
|
|
664
|
+
<input
|
|
665
|
+
type="text"
|
|
666
|
+
class="kora-search"
|
|
667
|
+
placeholder="Filter..."
|
|
668
|
+
value=${search}
|
|
669
|
+
onInput=${(e) => onSearchChange(e.target.value)}
|
|
670
|
+
/>
|
|
671
|
+
<button class="kora-btn" onClick=${onPauseToggle}>
|
|
672
|
+
${paused ? "Resume" : "Pause"}
|
|
673
|
+
</button>
|
|
674
|
+
<button class="kora-btn" onClick=${onClear}>Clear</button>
|
|
675
|
+
${ALL_CATEGORIES.map(
|
|
676
|
+
(cat) => html`
|
|
677
|
+
<button
|
|
678
|
+
class="kora-cat-toggle${categories.has(cat) ? " active" : ""}"
|
|
679
|
+
onClick=${() => onToggleCategory(cat)}
|
|
680
|
+
>
|
|
681
|
+
${cat}
|
|
682
|
+
</button>
|
|
683
|
+
`
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
function TimelinePanel({
|
|
690
|
+
items,
|
|
691
|
+
search,
|
|
692
|
+
expanded,
|
|
693
|
+
onToggle
|
|
694
|
+
}) {
|
|
695
|
+
const lowerSearch = search.toLowerCase();
|
|
696
|
+
const filtered = lowerSearch ? items.filter(
|
|
697
|
+
(i) => i.label.toLowerCase().includes(lowerSearch) || i.type.toLowerCase().includes(lowerSearch)
|
|
698
|
+
) : items;
|
|
699
|
+
const visible = filtered.slice(-200).reverse();
|
|
700
|
+
if (visible.length === 0) {
|
|
701
|
+
return html`<div class="kora-panel"><div class="kora-empty">No events recorded yet.</div></div>`;
|
|
702
|
+
}
|
|
703
|
+
return html`
|
|
704
|
+
<div class="kora-panel kora-timeline">
|
|
705
|
+
<div class="kora-summary">Showing ${visible.length} of ${items.length} events</div>
|
|
706
|
+
<div class="kora-event-list">
|
|
707
|
+
${visible.map(
|
|
708
|
+
(item) => html`
|
|
709
|
+
<div
|
|
710
|
+
key=${item.id}
|
|
711
|
+
class="kora-row${expanded.has(item.id) ? " expanded" : ""}"
|
|
712
|
+
onClick=${() => onToggle(item.id)}
|
|
713
|
+
>
|
|
714
|
+
<span class="kora-time">${formatTime(item.receivedAt)}</span>
|
|
715
|
+
<span class="kora-dot" style="background:${item.color}"></span>
|
|
716
|
+
<span class="kora-type">${item.type}</span>
|
|
717
|
+
<span class="kora-label">${item.label}</span>
|
|
718
|
+
${item.dependsOn.length > 0 ? html`<span class="kora-deps">deps: ${item.dependsOn.length}</span>` : null}
|
|
719
|
+
</div>
|
|
720
|
+
${expanded.has(item.id) ? html`
|
|
721
|
+
<div class="kora-detail">
|
|
722
|
+
<div>
|
|
723
|
+
<span class="kora-detail-label">Event ID:</span> ${item.id}
|
|
724
|
+
</div>
|
|
725
|
+
<div>
|
|
726
|
+
<span class="kora-detail-label">Received:</span>
|
|
727
|
+
${new Date(item.receivedAt).toISOString()}
|
|
728
|
+
</div>
|
|
729
|
+
${item.dependsOn.length > 0 ? html`<div>
|
|
730
|
+
<span class="kora-detail-label">Causal deps:</span>
|
|
731
|
+
${item.dependsOn.join(", ")}
|
|
732
|
+
</div>` : null}
|
|
733
|
+
</div>
|
|
734
|
+
` : null}
|
|
735
|
+
`
|
|
736
|
+
)}
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
741
|
+
function ConflictsPanel({
|
|
742
|
+
items,
|
|
743
|
+
search,
|
|
744
|
+
expanded,
|
|
745
|
+
onToggle
|
|
746
|
+
}) {
|
|
747
|
+
const lowerSearch = search.toLowerCase();
|
|
748
|
+
const filtered = lowerSearch ? items.filter(
|
|
749
|
+
(i) => i.collection.toLowerCase().includes(lowerSearch) || i.field.toLowerCase().includes(lowerSearch) || i.strategy.toLowerCase().includes(lowerSearch)
|
|
750
|
+
) : items;
|
|
751
|
+
const visible = filtered.slice(-100).reverse();
|
|
752
|
+
if (visible.length === 0) {
|
|
753
|
+
return html`<div class="kora-panel"><div class="kora-empty">No merge conflicts detected.</div></div>`;
|
|
754
|
+
}
|
|
755
|
+
return html`
|
|
756
|
+
<div class="kora-panel kora-conflicts">
|
|
757
|
+
<div class="kora-summary">
|
|
758
|
+
${items.length} conflict${items.length !== 1 ? "s" : ""}
|
|
759
|
+
</div>
|
|
760
|
+
<table class="kora-table">
|
|
761
|
+
<thead>
|
|
762
|
+
<tr>
|
|
763
|
+
<th>Time</th>
|
|
764
|
+
<th>Collection</th>
|
|
765
|
+
<th>Field</th>
|
|
766
|
+
<th>Strategy</th>
|
|
767
|
+
<th>Tier</th>
|
|
768
|
+
<th>Result</th>
|
|
769
|
+
</tr>
|
|
770
|
+
</thead>
|
|
771
|
+
<tbody>
|
|
772
|
+
${visible.map(
|
|
773
|
+
(item) => html`
|
|
774
|
+
<tr
|
|
775
|
+
key=${item.id}
|
|
776
|
+
class="kora-conflict-row${item.constraintViolated ? " violated" : ""}${expanded.has(item.id) ? " expanded" : ""}"
|
|
777
|
+
onClick=${() => onToggle(item.id)}
|
|
778
|
+
>
|
|
779
|
+
<td>${formatTime(item.timestamp)}</td>
|
|
780
|
+
<td>${item.collection}</td>
|
|
781
|
+
<td class="kora-mono">${item.field}</td>
|
|
782
|
+
<td>
|
|
783
|
+
<span class="kora-strategy tier-${item.tier}">${item.strategy}</span>
|
|
784
|
+
</td>
|
|
785
|
+
<td>${item.tier}</td>
|
|
786
|
+
<td class="kora-mono">${truncate(String(item.output), 30)}</td>
|
|
787
|
+
</tr>
|
|
788
|
+
${expanded.has(item.id) ? html`
|
|
789
|
+
<tr class="kora-detail-row">
|
|
790
|
+
<td colspan="6">
|
|
791
|
+
<div class="kora-conflict-detail">
|
|
792
|
+
<div class="kora-comparison">
|
|
793
|
+
<div class="kora-value-box">
|
|
794
|
+
<div class="kora-value-label">Input A (local)</div>
|
|
795
|
+
<pre class="kora-value-code">
|
|
796
|
+
${formatValue(item.inputA)}</pre
|
|
797
|
+
>
|
|
798
|
+
</div>
|
|
799
|
+
<div class="kora-value-box">
|
|
800
|
+
<div class="kora-value-label">Input B (remote)</div>
|
|
801
|
+
<pre class="kora-value-code">
|
|
802
|
+
${formatValue(item.inputB)}</pre
|
|
803
|
+
>
|
|
804
|
+
</div>
|
|
805
|
+
<div class="kora-value-box result">
|
|
806
|
+
<div class="kora-value-label">Result</div>
|
|
807
|
+
<pre class="kora-value-code">
|
|
808
|
+
${formatValue(item.output)}</pre
|
|
809
|
+
>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
${item.constraintViolated ? html`<div class="kora-constraint-warning">
|
|
813
|
+
Constraint violated: ${item.constraintViolated}
|
|
814
|
+
</div>` : null}
|
|
815
|
+
</div>
|
|
816
|
+
</td>
|
|
817
|
+
</tr>
|
|
818
|
+
` : null}
|
|
819
|
+
`
|
|
820
|
+
)}
|
|
821
|
+
</tbody>
|
|
822
|
+
</table>
|
|
823
|
+
</div>
|
|
824
|
+
`;
|
|
825
|
+
}
|
|
826
|
+
function OperationsPanel({
|
|
827
|
+
items,
|
|
828
|
+
search,
|
|
829
|
+
expanded,
|
|
830
|
+
onToggle
|
|
831
|
+
}) {
|
|
832
|
+
const lowerSearch = search.toLowerCase();
|
|
833
|
+
const filtered = lowerSearch ? items.filter(
|
|
834
|
+
(i) => i.collection.toLowerCase().includes(lowerSearch) || i.recordId.toLowerCase().includes(lowerSearch) || i.operationId.toLowerCase().includes(lowerSearch) || i.opType.toLowerCase().includes(lowerSearch)
|
|
835
|
+
) : items;
|
|
836
|
+
const visible = filtered.slice(-200).reverse();
|
|
837
|
+
if (visible.length === 0) {
|
|
838
|
+
return html`<div class="kora-panel"><div class="kora-empty">No operations recorded yet.</div></div>`;
|
|
839
|
+
}
|
|
840
|
+
return html`
|
|
841
|
+
<div class="kora-panel kora-operations">
|
|
842
|
+
<div class="kora-summary">
|
|
843
|
+
Showing ${visible.length} of ${items.length} operations
|
|
844
|
+
</div>
|
|
845
|
+
<div class="kora-op-list">
|
|
846
|
+
${visible.map(
|
|
847
|
+
(item) => html`
|
|
848
|
+
<div
|
|
849
|
+
key=${item.id}
|
|
850
|
+
class="kora-op-row${expanded.has(item.id) ? " expanded" : ""}"
|
|
851
|
+
onClick=${() => onToggle(item.id)}
|
|
852
|
+
>
|
|
853
|
+
<span class="kora-time">${formatTime(item.timestamp)}</span>
|
|
854
|
+
<span class="kora-op-type op-${item.opType}">${item.opType}</span>
|
|
855
|
+
<span class="kora-op-collection">${item.collection}</span>
|
|
856
|
+
<span class="kora-op-record kora-mono"
|
|
857
|
+
>${truncate(item.recordId, 12)}</span
|
|
858
|
+
>
|
|
859
|
+
<span class="kora-op-node">node:${truncate(item.nodeId, 8)}</span>
|
|
860
|
+
<span class="kora-op-seq">#${item.sequenceNumber}</span>
|
|
861
|
+
</div>
|
|
862
|
+
${expanded.has(item.id) ? html`
|
|
863
|
+
<div class="kora-op-detail">
|
|
864
|
+
<div>
|
|
865
|
+
<span class="kora-detail-label">Operation ID:</span>
|
|
866
|
+
<span class="kora-mono">${item.operationId}</span>
|
|
867
|
+
</div>
|
|
868
|
+
<div>
|
|
869
|
+
<span class="kora-detail-label">Node:</span>
|
|
870
|
+
<span class="kora-mono">${item.nodeId}</span>
|
|
871
|
+
</div>
|
|
872
|
+
<div>
|
|
873
|
+
<span class="kora-detail-label">Sequence:</span>
|
|
874
|
+
${item.sequenceNumber}
|
|
875
|
+
</div>
|
|
876
|
+
${item.causalDeps.length > 0 ? html`<div>
|
|
877
|
+
<span class="kora-detail-label">Causal deps:</span>
|
|
878
|
+
<span class="kora-mono"
|
|
879
|
+
>${item.causalDeps.join(", ")}</span
|
|
880
|
+
>
|
|
881
|
+
</div>` : null}
|
|
882
|
+
${item.data ? html`<div>
|
|
883
|
+
<div class="kora-detail-label">Data:</div>
|
|
884
|
+
<pre class="kora-value-code">
|
|
885
|
+
${JSON.stringify(item.data, null, 2)}</pre
|
|
886
|
+
>
|
|
887
|
+
</div>` : null}
|
|
888
|
+
</div>
|
|
889
|
+
` : null}
|
|
890
|
+
`
|
|
891
|
+
)}
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
`;
|
|
895
|
+
}
|
|
896
|
+
function NetworkPanel({
|
|
897
|
+
network,
|
|
898
|
+
events
|
|
899
|
+
}) {
|
|
900
|
+
const statusClass = network.connected ? "connected" : "disconnected";
|
|
901
|
+
const statusText = network.connected ? network.quality ? `Connected (${network.quality})` : "Connected" : "Disconnected";
|
|
902
|
+
const syncEvents = events.filter((e) => {
|
|
903
|
+
const cat = eventTypeToCategory(e.event.type);
|
|
904
|
+
return cat === "sync" || cat === "connection";
|
|
905
|
+
}).slice(-50).reverse();
|
|
906
|
+
return html`
|
|
907
|
+
<div class="kora-panel kora-network">
|
|
908
|
+
<div class="kora-network-status">
|
|
909
|
+
<div class="kora-status-indicator ${statusClass}">
|
|
910
|
+
<span class="kora-status-dot"></span>
|
|
911
|
+
<span class="kora-status-text">${statusText}</span>
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
|
|
915
|
+
<div class="kora-stats-grid">
|
|
916
|
+
<${StatCard} label="Sent" value=${String(network.sentOps)} unit="ops" />
|
|
917
|
+
<${StatCard} label="Received" value=${String(network.receivedOps)} unit="ops" />
|
|
918
|
+
<${StatCard} label="Pending" value=${String(network.pendingAcks)} unit="acks" />
|
|
919
|
+
<${StatCard}
|
|
920
|
+
label="Last Sync"
|
|
921
|
+
value=${network.lastSyncAt ? formatTime(network.lastSyncAt) : "never"}
|
|
922
|
+
/>
|
|
923
|
+
</div>
|
|
924
|
+
|
|
925
|
+
${network.versionVector.length > 0 ? html`
|
|
926
|
+
<div class="kora-vv-section">
|
|
927
|
+
<h3>Version Vector</h3>
|
|
928
|
+
<table class="kora-table kora-vv-table">
|
|
929
|
+
<thead>
|
|
930
|
+
<tr>
|
|
931
|
+
<th>Node</th>
|
|
932
|
+
<th>Sequence</th>
|
|
933
|
+
</tr>
|
|
934
|
+
</thead>
|
|
935
|
+
<tbody>
|
|
936
|
+
${network.versionVector.map(
|
|
937
|
+
(entry) => html`
|
|
938
|
+
<tr key=${entry.nodeId}>
|
|
939
|
+
<td class="kora-mono">${truncate(entry.nodeId, 16)}</td>
|
|
940
|
+
<td>${entry.sequenceNumber}</td>
|
|
941
|
+
</tr>
|
|
942
|
+
`
|
|
943
|
+
)}
|
|
944
|
+
</tbody>
|
|
945
|
+
</table>
|
|
946
|
+
</div>
|
|
947
|
+
` : null}
|
|
948
|
+
|
|
949
|
+
${syncEvents.length > 0 ? html`
|
|
950
|
+
<div class="kora-sync-log">
|
|
951
|
+
<h3>Recent Sync Activity</h3>
|
|
952
|
+
<div class="kora-event-list">
|
|
953
|
+
${syncEvents.map(
|
|
954
|
+
(entry) => html`
|
|
955
|
+
<div key=${entry.id} class="kora-row compact">
|
|
956
|
+
<span class="kora-time"
|
|
957
|
+
>${formatTime(entry.receivedAt)}</span
|
|
958
|
+
>
|
|
959
|
+
<span class="kora-type">${entry.event.type}</span>
|
|
960
|
+
</div>
|
|
961
|
+
`
|
|
962
|
+
)}
|
|
963
|
+
</div>
|
|
964
|
+
</div>
|
|
965
|
+
` : null}
|
|
966
|
+
</div>
|
|
967
|
+
`;
|
|
968
|
+
}
|
|
969
|
+
function StatCard({
|
|
970
|
+
label,
|
|
971
|
+
value,
|
|
972
|
+
unit
|
|
973
|
+
}) {
|
|
974
|
+
return html`
|
|
975
|
+
<div class="kora-stat-card">
|
|
976
|
+
<div class="kora-stat-value">${value}</div>
|
|
977
|
+
<div class="kora-stat-label">${label}${unit ? ` (${unit})` : ""}</div>
|
|
978
|
+
</div>
|
|
979
|
+
`;
|
|
980
|
+
}
|
|
981
|
+
function renderDevtoolsPanel(target, events) {
|
|
982
|
+
render(html`<${DevToolsPanel} events=${events} />`, target);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/extension/port-router.ts
|
|
986
|
+
var PortRouter = class {
|
|
987
|
+
panelClients = /* @__PURE__ */ new Map();
|
|
988
|
+
contentClients = /* @__PURE__ */ new Map();
|
|
989
|
+
handleConnection(port) {
|
|
990
|
+
if (port.name === "kora-panel") {
|
|
991
|
+
this.attachPanel(port);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (port.name === "kora-content") {
|
|
995
|
+
this.attachContent(port);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
attachPanel(port) {
|
|
999
|
+
port.onMessage.addListener((message) => {
|
|
1000
|
+
if (!isPanelInitMessage(message)) return;
|
|
1001
|
+
this.panelClients.set(message.tabId, { tabId: message.tabId, port });
|
|
1002
|
+
});
|
|
1003
|
+
port.onDisconnect.addListener(() => {
|
|
1004
|
+
for (const [tabId, client] of this.panelClients) {
|
|
1005
|
+
if (client.port === port) {
|
|
1006
|
+
this.panelClients.delete(tabId);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
attachContent(port) {
|
|
1012
|
+
const tabId = port.sender?.tab?.id;
|
|
1013
|
+
if (typeof tabId !== "number") {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
this.contentClients.set(tabId, port);
|
|
1017
|
+
port.onMessage.addListener((message) => {
|
|
1018
|
+
if (!isContentEventMessage(message)) return;
|
|
1019
|
+
const panel = this.panelClients.get(tabId);
|
|
1020
|
+
if (!panel) return;
|
|
1021
|
+
panel.port.postMessage({ type: "kora-event", payload: message.payload });
|
|
1022
|
+
});
|
|
1023
|
+
port.onDisconnect.addListener(() => {
|
|
1024
|
+
this.contentClients.delete(tabId);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
function isPanelInitMessage(value) {
|
|
1029
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1030
|
+
const record = value;
|
|
1031
|
+
return record.type === "panel-init" && typeof record.tabId === "number";
|
|
1032
|
+
}
|
|
1033
|
+
function isContentEventMessage(value) {
|
|
1034
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1035
|
+
const record = value;
|
|
1036
|
+
return record.type === "kora-event" && "payload" in record;
|
|
1037
|
+
}
|
|
372
1038
|
export {
|
|
373
1039
|
EventBuffer,
|
|
374
1040
|
Instrumenter,
|