@korajs/devtools 0.1.3 → 0.3.1

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/index.js CHANGED
@@ -232,6 +232,7 @@ var EVENT_TYPE_CATEGORIES = {
232
232
  "constraint:violated": "merge",
233
233
  "sync:connected": "sync",
234
234
  "sync:disconnected": "sync",
235
+ "sync:auth-failed": "sync",
235
236
  "sync:sent": "sync",
236
237
  "sync:received": "sync",
237
238
  "sync:acknowledged": "sync",
@@ -373,7 +374,7 @@ function buildPanelModel(events) {
373
374
  dependsOn: extractCausalDependencies(entry.event)
374
375
  }));
375
376
  const conflicts = events.flatMap((entry) => {
376
- if (entry.event.type !== "merge:completed" && entry.event.type !== "merge:conflict") {
377
+ if (entry.event.type !== "merge:completed" && entry.event.type !== "merge:conflict" && entry.event.type !== "constraint:violated") {
377
378
  return [];
378
379
  }
379
380
  const trace = entry.event.trace;
@@ -484,6 +485,8 @@ function timelineLabel(event) {
484
485
  return `sync connected ${event.nodeId}`;
485
486
  case "sync:disconnected":
486
487
  return `sync disconnected`;
488
+ case "sync:auth-failed":
489
+ return `sync auth failed`;
487
490
  case "sync:sent":
488
491
  return `sync sent ${event.batchSize}`;
489
492
  case "sync:received":
@@ -524,40 +527,462 @@ function extractOperation(event) {
524
527
  }
525
528
 
526
529
  // src/ui/panel.ts
527
- function renderDevtoolsPanel(target, events) {
528
- const model = buildPanelModel(events);
529
- target.innerHTML = [
530
- '<section data-panel="timeline"><h2>Sync Timeline</h2>',
531
- `<p>Total events: ${model.timeline.length}</p>`,
532
- "<ul>",
533
- ...model.timeline.slice(-20).map(
534
- (item) => `<li><span style="color:${item.color}">${item.type}</span> \xB7 ${escapeHtml(item.label)}</li>`
535
- ),
536
- "</ul></section>",
537
- '<section data-panel="conflicts"><h2>Conflict Inspector</h2>',
538
- `<p>Conflicts: ${model.conflicts.length}</p>`,
539
- "<ul>",
540
- ...model.conflicts.slice(-20).map(
541
- (item) => `<li>${escapeHtml(item.collection)}.${escapeHtml(item.field)} \xB7 ${escapeHtml(item.strategy)} \xB7 tier ${item.tier}</li>`
542
- ),
543
- "</ul></section>",
544
- '<section data-panel="operations"><h2>Operation Log</h2>',
545
- `<p>Operations: ${model.operations.length}</p>`,
546
- "<ul>",
547
- ...model.operations.slice(-20).map(
548
- (item) => `<li>${escapeHtml(item.opType)} ${escapeHtml(item.collection)}/${escapeHtml(item.recordId)} (${escapeHtml(item.operationId)})</li>`
549
- ),
550
- "</ul></section>",
551
- '<section data-panel="network"><h2>Network Status</h2>',
552
- `<p>Connected: ${model.network.connected ? "yes" : "no"}</p>`,
553
- `<p>Pending ACKs: ${model.network.pendingAcks}</p>`,
554
- `<p>Sent ops: ${model.network.sentOps}</p>`,
555
- `<p>Received ops: ${model.network.receivedOps}</p>`,
556
- "</section>"
557
- ].join("");
530
+ import { h, render } from "preact";
531
+ import { useState, useCallback, useMemo } from "preact/hooks";
532
+ import htm from "htm";
533
+
534
+ // src/ui/components.ts
535
+ function formatTime(timestamp) {
536
+ const d = new Date(timestamp);
537
+ const h2 = String(d.getHours()).padStart(2, "0");
538
+ const m = String(d.getMinutes()).padStart(2, "0");
539
+ const s = String(d.getSeconds()).padStart(2, "0");
540
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
541
+ return `${h2}:${m}:${s}.${ms}`;
542
+ }
543
+ function truncate(str, max) {
544
+ if (str.length <= max) return str;
545
+ return str.slice(0, max - 1) + "\u2026";
546
+ }
547
+ function formatValue(value) {
548
+ if (value === null || value === void 0) return "null";
549
+ if (typeof value === "string") return `"${value}"`;
550
+ if (typeof value === "object") return JSON.stringify(value, null, 2);
551
+ return String(value);
552
+ }
553
+
554
+ // src/ui/panel.ts
555
+ var html = htm.bind(h);
556
+ var ALL_CATEGORIES = ["operation", "merge", "sync", "query", "connection"];
557
+ function DevToolsPanel({ events }) {
558
+ const [activeTab, setActiveTab] = useState("timeline");
559
+ const [search, setSearch] = useState("");
560
+ const [categories, setCategories] = useState(
561
+ () => new Set(ALL_CATEGORIES)
562
+ );
563
+ const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
564
+ const [paused, setPaused] = useState(false);
565
+ const toggleCategory = useCallback(
566
+ (cat) => {
567
+ setCategories((prev) => {
568
+ const next = new Set(prev);
569
+ if (next.has(cat)) next.delete(cat);
570
+ else next.add(cat);
571
+ return next;
572
+ });
573
+ },
574
+ []
575
+ );
576
+ const toggleExpand = useCallback(
577
+ (id) => {
578
+ setExpanded((prev) => {
579
+ const next = new Set(prev);
580
+ if (next.has(id)) next.delete(id);
581
+ else next.add(id);
582
+ return next;
583
+ });
584
+ },
585
+ []
586
+ );
587
+ const filtered = useMemo(
588
+ () => filterEvents(events, { categories: [...categories] }),
589
+ [events, categories]
590
+ );
591
+ const model = useMemo(() => buildPanelModel(filtered), [filtered]);
592
+ const stats = useMemo(() => computeStatistics(events), [events]);
593
+ return html`
594
+ <div class="kora-devtools">
595
+ <${Toolbar}
596
+ activeTab=${activeTab}
597
+ onTabChange=${setActiveTab}
598
+ totalEvents=${events.length}
599
+ conflicts=${stats.mergeConflicts}
600
+ search=${search}
601
+ onSearchChange=${setSearch}
602
+ paused=${paused}
603
+ onPauseToggle=${() => setPaused((p) => !p)}
604
+ onClear=${() => setExpanded(/* @__PURE__ */ new Set())}
605
+ categories=${categories}
606
+ onToggleCategory=${toggleCategory}
607
+ />
608
+ <div class="kora-content">
609
+ ${activeTab === "timeline" && html`<${TimelinePanel}
610
+ items=${model.timeline}
611
+ search=${search}
612
+ expanded=${expanded}
613
+ onToggle=${toggleExpand}
614
+ />`}
615
+ ${activeTab === "conflicts" && html`<${ConflictsPanel}
616
+ items=${model.conflicts}
617
+ search=${search}
618
+ expanded=${expanded}
619
+ onToggle=${toggleExpand}
620
+ />`}
621
+ ${activeTab === "operations" && html`<${OperationsPanel}
622
+ items=${model.operations}
623
+ search=${search}
624
+ expanded=${expanded}
625
+ onToggle=${toggleExpand}
626
+ />`}
627
+ ${activeTab === "network" && html`<${NetworkPanel} network=${model.network} events=${events} />`}
628
+ </div>
629
+ </div>
630
+ `;
631
+ }
632
+ function Toolbar({
633
+ activeTab,
634
+ onTabChange,
635
+ totalEvents,
636
+ conflicts,
637
+ search,
638
+ onSearchChange,
639
+ paused,
640
+ onPauseToggle,
641
+ onClear,
642
+ categories,
643
+ onToggleCategory
644
+ }) {
645
+ const tabs = [
646
+ { id: "timeline", label: "Timeline", badge: totalEvents },
647
+ { id: "conflicts", label: "Conflicts", badge: conflicts },
648
+ { id: "operations", label: "Operations" },
649
+ { id: "network", label: "Network" }
650
+ ];
651
+ return html`
652
+ <div class="kora-toolbar">
653
+ <div class="kora-tabs">
654
+ ${tabs.map(
655
+ (tab) => html`
656
+ <button
657
+ class="kora-tab${activeTab === tab.id ? " active" : ""}"
658
+ onClick=${() => onTabChange(tab.id)}
659
+ >
660
+ ${tab.label}
661
+ ${tab.badge != null && tab.badge > 0 ? html`<span class="kora-badge">${tab.badge}</span>` : null}
662
+ </button>
663
+ `
664
+ )}
665
+ </div>
666
+ <div class="kora-controls">
667
+ <input
668
+ type="text"
669
+ class="kora-search"
670
+ placeholder="Filter..."
671
+ value=${search}
672
+ onInput=${(e) => onSearchChange(e.target.value)}
673
+ />
674
+ <button class="kora-btn" onClick=${onPauseToggle}>
675
+ ${paused ? "Resume" : "Pause"}
676
+ </button>
677
+ <button class="kora-btn" onClick=${onClear}>Clear</button>
678
+ ${ALL_CATEGORIES.map(
679
+ (cat) => html`
680
+ <button
681
+ class="kora-cat-toggle${categories.has(cat) ? " active" : ""}"
682
+ onClick=${() => onToggleCategory(cat)}
683
+ >
684
+ ${cat}
685
+ </button>
686
+ `
687
+ )}
688
+ </div>
689
+ </div>
690
+ `;
691
+ }
692
+ function TimelinePanel({
693
+ items,
694
+ search,
695
+ expanded,
696
+ onToggle
697
+ }) {
698
+ const lowerSearch = search.toLowerCase();
699
+ const filtered = lowerSearch ? items.filter(
700
+ (i) => i.label.toLowerCase().includes(lowerSearch) || i.type.toLowerCase().includes(lowerSearch)
701
+ ) : items;
702
+ const visible = filtered.slice(-200).reverse();
703
+ if (visible.length === 0) {
704
+ return html`<div class="kora-panel"><div class="kora-empty">No events recorded yet.</div></div>`;
705
+ }
706
+ return html`
707
+ <div class="kora-panel kora-timeline">
708
+ <div class="kora-summary">Showing ${visible.length} of ${items.length} events</div>
709
+ <div class="kora-event-list">
710
+ ${visible.map(
711
+ (item) => html`
712
+ <div
713
+ key=${item.id}
714
+ class="kora-row${expanded.has(item.id) ? " expanded" : ""}"
715
+ onClick=${() => onToggle(item.id)}
716
+ >
717
+ <span class="kora-time">${formatTime(item.receivedAt)}</span>
718
+ <span class="kora-dot" style="background:${item.color}"></span>
719
+ <span class="kora-type">${item.type}</span>
720
+ <span class="kora-label">${item.label}</span>
721
+ ${item.dependsOn.length > 0 ? html`<span class="kora-deps">deps: ${item.dependsOn.length}</span>` : null}
722
+ </div>
723
+ ${expanded.has(item.id) ? html`
724
+ <div class="kora-detail">
725
+ <div>
726
+ <span class="kora-detail-label">Event ID:</span> ${item.id}
727
+ </div>
728
+ <div>
729
+ <span class="kora-detail-label">Received:</span>
730
+ ${new Date(item.receivedAt).toISOString()}
731
+ </div>
732
+ ${item.dependsOn.length > 0 ? html`<div>
733
+ <span class="kora-detail-label">Causal deps:</span>
734
+ ${item.dependsOn.join(", ")}
735
+ </div>` : null}
736
+ </div>
737
+ ` : null}
738
+ `
739
+ )}
740
+ </div>
741
+ </div>
742
+ `;
743
+ }
744
+ function ConflictsPanel({
745
+ items,
746
+ search,
747
+ expanded,
748
+ onToggle
749
+ }) {
750
+ const lowerSearch = search.toLowerCase();
751
+ const filtered = lowerSearch ? items.filter(
752
+ (i) => i.collection.toLowerCase().includes(lowerSearch) || i.field.toLowerCase().includes(lowerSearch) || i.strategy.toLowerCase().includes(lowerSearch)
753
+ ) : items;
754
+ const visible = filtered.slice(-100).reverse();
755
+ if (visible.length === 0) {
756
+ return html`<div class="kora-panel"><div class="kora-empty">No merge conflicts detected.</div></div>`;
757
+ }
758
+ return html`
759
+ <div class="kora-panel kora-conflicts">
760
+ <div class="kora-summary">
761
+ ${items.length} conflict${items.length !== 1 ? "s" : ""}
762
+ </div>
763
+ <table class="kora-table">
764
+ <thead>
765
+ <tr>
766
+ <th>Time</th>
767
+ <th>Collection</th>
768
+ <th>Field</th>
769
+ <th>Strategy</th>
770
+ <th>Tier</th>
771
+ <th>Result</th>
772
+ </tr>
773
+ </thead>
774
+ <tbody>
775
+ ${visible.map(
776
+ (item) => html`
777
+ <tr
778
+ key=${item.id}
779
+ class="kora-conflict-row${item.constraintViolated ? " violated" : ""}${expanded.has(item.id) ? " expanded" : ""}"
780
+ onClick=${() => onToggle(item.id)}
781
+ >
782
+ <td>${formatTime(item.timestamp)}</td>
783
+ <td>${item.collection}</td>
784
+ <td class="kora-mono">${item.field}</td>
785
+ <td>
786
+ <span class="kora-strategy tier-${item.tier}">${item.strategy}</span>
787
+ </td>
788
+ <td>${item.tier}</td>
789
+ <td class="kora-mono">${truncate(String(item.output), 30)}</td>
790
+ </tr>
791
+ ${expanded.has(item.id) ? html`
792
+ <tr class="kora-detail-row">
793
+ <td colspan="6">
794
+ <div class="kora-conflict-detail">
795
+ <div class="kora-comparison">
796
+ <div class="kora-value-box">
797
+ <div class="kora-value-label">Input A (local)</div>
798
+ <pre class="kora-value-code">
799
+ ${formatValue(item.inputA)}</pre
800
+ >
801
+ </div>
802
+ <div class="kora-value-box">
803
+ <div class="kora-value-label">Input B (remote)</div>
804
+ <pre class="kora-value-code">
805
+ ${formatValue(item.inputB)}</pre
806
+ >
807
+ </div>
808
+ <div class="kora-value-box result">
809
+ <div class="kora-value-label">Result</div>
810
+ <pre class="kora-value-code">
811
+ ${formatValue(item.output)}</pre
812
+ >
813
+ </div>
814
+ </div>
815
+ ${item.constraintViolated ? html`<div class="kora-constraint-warning">
816
+ Constraint violated: ${item.constraintViolated}
817
+ </div>` : null}
818
+ </div>
819
+ </td>
820
+ </tr>
821
+ ` : null}
822
+ `
823
+ )}
824
+ </tbody>
825
+ </table>
826
+ </div>
827
+ `;
558
828
  }
559
- function escapeHtml(value) {
560
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
829
+ function OperationsPanel({
830
+ items,
831
+ search,
832
+ expanded,
833
+ onToggle
834
+ }) {
835
+ const lowerSearch = search.toLowerCase();
836
+ const filtered = lowerSearch ? items.filter(
837
+ (i) => i.collection.toLowerCase().includes(lowerSearch) || i.recordId.toLowerCase().includes(lowerSearch) || i.operationId.toLowerCase().includes(lowerSearch) || i.opType.toLowerCase().includes(lowerSearch)
838
+ ) : items;
839
+ const visible = filtered.slice(-200).reverse();
840
+ if (visible.length === 0) {
841
+ return html`<div class="kora-panel"><div class="kora-empty">No operations recorded yet.</div></div>`;
842
+ }
843
+ return html`
844
+ <div class="kora-panel kora-operations">
845
+ <div class="kora-summary">
846
+ Showing ${visible.length} of ${items.length} operations
847
+ </div>
848
+ <div class="kora-op-list">
849
+ ${visible.map(
850
+ (item) => html`
851
+ <div
852
+ key=${item.id}
853
+ class="kora-op-row${expanded.has(item.id) ? " expanded" : ""}"
854
+ onClick=${() => onToggle(item.id)}
855
+ >
856
+ <span class="kora-time">${formatTime(item.timestamp)}</span>
857
+ <span class="kora-op-type op-${item.opType}">${item.opType}</span>
858
+ <span class="kora-op-collection">${item.collection}</span>
859
+ <span class="kora-op-record kora-mono"
860
+ >${truncate(item.recordId, 12)}</span
861
+ >
862
+ <span class="kora-op-node">node:${truncate(item.nodeId, 8)}</span>
863
+ <span class="kora-op-seq">#${item.sequenceNumber}</span>
864
+ </div>
865
+ ${expanded.has(item.id) ? html`
866
+ <div class="kora-op-detail">
867
+ <div>
868
+ <span class="kora-detail-label">Operation ID:</span>
869
+ <span class="kora-mono">${item.operationId}</span>
870
+ </div>
871
+ <div>
872
+ <span class="kora-detail-label">Node:</span>
873
+ <span class="kora-mono">${item.nodeId}</span>
874
+ </div>
875
+ <div>
876
+ <span class="kora-detail-label">Sequence:</span>
877
+ ${item.sequenceNumber}
878
+ </div>
879
+ ${item.causalDeps.length > 0 ? html`<div>
880
+ <span class="kora-detail-label">Causal deps:</span>
881
+ <span class="kora-mono"
882
+ >${item.causalDeps.join(", ")}</span
883
+ >
884
+ </div>` : null}
885
+ ${item.data ? html`<div>
886
+ <div class="kora-detail-label">Data:</div>
887
+ <pre class="kora-value-code">
888
+ ${JSON.stringify(item.data, null, 2)}</pre
889
+ >
890
+ </div>` : null}
891
+ </div>
892
+ ` : null}
893
+ `
894
+ )}
895
+ </div>
896
+ </div>
897
+ `;
898
+ }
899
+ function NetworkPanel({
900
+ network,
901
+ events
902
+ }) {
903
+ const statusClass = network.connected ? "connected" : "disconnected";
904
+ const statusText = network.connected ? network.quality ? `Connected (${network.quality})` : "Connected" : "Disconnected";
905
+ const syncEvents = events.filter((e) => {
906
+ const cat = eventTypeToCategory(e.event.type);
907
+ return cat === "sync" || cat === "connection";
908
+ }).slice(-50).reverse();
909
+ return html`
910
+ <div class="kora-panel kora-network">
911
+ <div class="kora-network-status">
912
+ <div class="kora-status-indicator ${statusClass}">
913
+ <span class="kora-status-dot"></span>
914
+ <span class="kora-status-text">${statusText}</span>
915
+ </div>
916
+ </div>
917
+
918
+ <div class="kora-stats-grid">
919
+ <${StatCard} label="Sent" value=${String(network.sentOps)} unit="ops" />
920
+ <${StatCard} label="Received" value=${String(network.receivedOps)} unit="ops" />
921
+ <${StatCard} label="Pending" value=${String(network.pendingAcks)} unit="acks" />
922
+ <${StatCard}
923
+ label="Last Sync"
924
+ value=${network.lastSyncAt ? formatTime(network.lastSyncAt) : "never"}
925
+ />
926
+ </div>
927
+
928
+ ${network.versionVector.length > 0 ? html`
929
+ <div class="kora-vv-section">
930
+ <h3>Version Vector</h3>
931
+ <table class="kora-table kora-vv-table">
932
+ <thead>
933
+ <tr>
934
+ <th>Node</th>
935
+ <th>Sequence</th>
936
+ </tr>
937
+ </thead>
938
+ <tbody>
939
+ ${network.versionVector.map(
940
+ (entry) => html`
941
+ <tr key=${entry.nodeId}>
942
+ <td class="kora-mono">${truncate(entry.nodeId, 16)}</td>
943
+ <td>${entry.sequenceNumber}</td>
944
+ </tr>
945
+ `
946
+ )}
947
+ </tbody>
948
+ </table>
949
+ </div>
950
+ ` : null}
951
+
952
+ ${syncEvents.length > 0 ? html`
953
+ <div class="kora-sync-log">
954
+ <h3>Recent Sync Activity</h3>
955
+ <div class="kora-event-list">
956
+ ${syncEvents.map(
957
+ (entry) => html`
958
+ <div key=${entry.id} class="kora-row compact">
959
+ <span class="kora-time"
960
+ >${formatTime(entry.receivedAt)}</span
961
+ >
962
+ <span class="kora-type">${entry.event.type}</span>
963
+ </div>
964
+ `
965
+ )}
966
+ </div>
967
+ </div>
968
+ ` : null}
969
+ </div>
970
+ `;
971
+ }
972
+ function StatCard({
973
+ label,
974
+ value,
975
+ unit
976
+ }) {
977
+ return html`
978
+ <div class="kora-stat-card">
979
+ <div class="kora-stat-value">${value}</div>
980
+ <div class="kora-stat-label">${label}${unit ? ` (${unit})` : ""}</div>
981
+ </div>
982
+ `;
983
+ }
984
+ function renderDevtoolsPanel(target, events) {
985
+ render(html`<${DevToolsPanel} events=${events} />`, target);
561
986
  }
562
987
 
563
988
  // src/extension/port-router.ts