@korajs/devtools 0.1.0 → 0.1.3

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/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @korajs/devtools
2
+
3
+ Browser DevTools extension for Kora.js. Inspect operations, trace conflict resolution decisions, monitor sync status, and debug your offline-first app in real time.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @korajs/devtools
9
+ ```
10
+
11
+ ## Enable
12
+
13
+ Add `devtools: true` to your app config:
14
+
15
+ ```typescript
16
+ import { createApp } from 'korajs'
17
+
18
+ const app = createApp({
19
+ schema,
20
+ devtools: process.env.NODE_ENV === 'development',
21
+ })
22
+ ```
23
+
24
+ When enabled, the Kora DevTools panel appears in your browser's developer tools.
25
+
26
+ ## Panels
27
+
28
+ ### Sync Timeline
29
+ Horizontal timeline of operations and sync events, color-coded by type. Click any event to inspect it. Causal arrows show dependencies between operations.
30
+
31
+ ### Conflict Inspector
32
+ Table of all merge events, filterable by collection, tier, and strategy. Expand any row to see the full `MergeTrace` -- input values, base value, resolved output, and which tier handled it.
33
+
34
+ ### Operation Log
35
+ Searchable list of all operations. Click an operation to view its full payload, causal dependencies, and the database state at that point in time.
36
+
37
+ ### Network Status
38
+ Real-time connection quality indicator, pending operation count, bandwidth graph, and last sync timestamp.
39
+
40
+ ## Instrumentation Events
41
+
42
+ The DevTools extension listens for events emitted by the Kora runtime:
43
+
44
+ - `operation:created` / `operation:applied`
45
+ - `merge:started` / `merge:completed` / `merge:conflict`
46
+ - `sync:connected` / `sync:disconnected` / `sync:sent` / `sync:received`
47
+ - `query:subscribed` / `query:invalidated` / `query:executed`
48
+ - `connection:quality`
49
+
50
+ ## Keyboard Shortcut
51
+
52
+ In development mode, press `Ctrl+Shift+K` (or `Cmd+Shift+K` on macOS) to toggle the embedded DevTools overlay.
53
+
54
+ ## License
55
+
56
+ MIT
57
+
58
+ See the [full documentation](https://github.com/ehoneahobed/kora) for guides, API reference, and examples.
@@ -55,7 +55,7 @@ function isContentEventMessage(value) {
55
55
  }
56
56
 
57
57
  // src/extension/background.ts
58
- var runtime = globalThis.chrome?.runtime;
58
+ var runtime = chrome?.runtime;
59
59
  if (runtime?.onConnect) {
60
60
  const router = new PortRouter();
61
61
  runtime.onConnect.addListener((port) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/port-router.ts","../../src/extension/background.ts"],"sourcesContent":["export interface ExtensionPort {\n\tname: string\n\tonMessage: {\n\t\taddListener(callback: (message: unknown) => void): void\n\t}\n\tonDisconnect: {\n\t\taddListener(callback: () => void): void\n\t}\n\tpostMessage(message: unknown): void\n\tsender?: { tab?: { id?: number } }\n}\n\ninterface PanelClient {\n\tport: ExtensionPort\n\ttabId: number\n}\n\n/**\n * Routes content-script events to the matching DevTools panel by tab.\n */\nexport class PortRouter {\n\tprivate readonly panelClients = new Map<number, PanelClient>()\n\tprivate readonly contentClients = new Map<number, ExtensionPort>()\n\n\thandleConnection(port: ExtensionPort): void {\n\t\tif (port.name === 'kora-panel') {\n\t\t\tthis.attachPanel(port)\n\t\t\treturn\n\t\t}\n\n\t\tif (port.name === 'kora-content') {\n\t\t\tthis.attachContent(port)\n\t\t}\n\t}\n\n\tprivate attachPanel(port: ExtensionPort): void {\n\t\tport.onMessage.addListener((message) => {\n\t\t\tif (!isPanelInitMessage(message)) return\n\n\t\t\tthis.panelClients.set(message.tabId, { tabId: message.tabId, port })\n\t\t})\n\n\t\tport.onDisconnect.addListener(() => {\n\t\t\tfor (const [tabId, client] of this.panelClients) {\n\t\t\t\tif (client.port === port) {\n\t\t\t\t\tthis.panelClients.delete(tabId)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tprivate attachContent(port: ExtensionPort): void {\n\t\tconst tabId = port.sender?.tab?.id\n\t\tif (typeof tabId !== 'number') {\n\t\t\treturn\n\t\t}\n\n\t\tthis.contentClients.set(tabId, port)\n\n\t\tport.onMessage.addListener((message) => {\n\t\t\tif (!isContentEventMessage(message)) return\n\n\t\t\tconst panel = this.panelClients.get(tabId)\n\t\t\tif (!panel) return\n\n\t\t\tpanel.port.postMessage({ type: 'kora-event', payload: message.payload })\n\t\t})\n\n\t\tport.onDisconnect.addListener(() => {\n\t\t\tthis.contentClients.delete(tabId)\n\t\t})\n\t}\n}\n\nfunction isPanelInitMessage(value: unknown): value is { type: 'panel-init'; tabId: number } {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst record = value as Record<string, unknown>\n\treturn record.type === 'panel-init' && typeof record.tabId === 'number'\n}\n\nfunction isContentEventMessage(value: unknown): value is { type: 'kora-event'; payload: unknown } {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst record = value as Record<string, unknown>\n\treturn record.type === 'kora-event' && 'payload' in record\n}\n","import { PortRouter, type ExtensionPort } from './port-router'\n\ninterface RuntimeLike {\n\tonConnect?: {\n\t\taddListener(callback: (port: ExtensionPort) => void): void\n\t}\n}\n\nconst runtime = (globalThis as { chrome?: { runtime?: RuntimeLike } }).chrome?.runtime\n\nif (runtime?.onConnect) {\n\tconst router = new PortRouter()\n\truntime.onConnect.addListener((port) => {\n\t\trouter.handleConnection(port)\n\t})\n}\n"],"mappings":";;;AAoBO,IAAM,aAAN,MAAiB;AAAA,EACN,eAAe,oBAAI,IAAyB;AAAA,EAC5C,iBAAiB,oBAAI,IAA2B;AAAA,EAEjE,iBAAiB,MAA2B;AAC3C,QAAI,KAAK,SAAS,cAAc;AAC/B,WAAK,YAAY,IAAI;AACrB;AAAA,IACD;AAEA,QAAI,KAAK,SAAS,gBAAgB;AACjC,WAAK,cAAc,IAAI;AAAA,IACxB;AAAA,EACD;AAAA,EAEQ,YAAY,MAA2B;AAC9C,SAAK,UAAU,YAAY,CAAC,YAAY;AACvC,UAAI,CAAC,mBAAmB,OAAO,EAAG;AAElC,WAAK,aAAa,IAAI,QAAQ,OAAO,EAAE,OAAO,QAAQ,OAAO,KAAK,CAAC;AAAA,IACpE,CAAC;AAED,SAAK,aAAa,YAAY,MAAM;AACnC,iBAAW,CAAC,OAAO,MAAM,KAAK,KAAK,cAAc;AAChD,YAAI,OAAO,SAAS,MAAM;AACzB,eAAK,aAAa,OAAO,KAAK;AAAA,QAC/B;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEQ,cAAc,MAA2B;AAChD,UAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,QAAI,OAAO,UAAU,UAAU;AAC9B;AAAA,IACD;AAEA,SAAK,eAAe,IAAI,OAAO,IAAI;AAEnC,SAAK,UAAU,YAAY,CAAC,YAAY;AACvC,UAAI,CAAC,sBAAsB,OAAO,EAAG;AAErC,YAAM,QAAQ,KAAK,aAAa,IAAI,KAAK;AACzC,UAAI,CAAC,MAAO;AAEZ,YAAM,KAAK,YAAY,EAAE,MAAM,cAAc,SAAS,QAAQ,QAAQ,CAAC;AAAA,IACxE,CAAC;AAED,SAAK,aAAa,YAAY,MAAM;AACnC,WAAK,eAAe,OAAO,KAAK;AAAA,IACjC,CAAC;AAAA,EACF;AACD;AAEA,SAAS,mBAAmB,OAAgE;AAC3F,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,SAAS,gBAAgB,OAAO,OAAO,UAAU;AAChE;AAEA,SAAS,sBAAsB,OAAmE;AACjG,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,SAAS,gBAAgB,aAAa;AACrD;;;AC5EA,IAAM,UAAW,WAAsD,QAAQ;AAE/E,IAAI,SAAS,WAAW;AACvB,QAAM,SAAS,IAAI,WAAW;AAC9B,UAAQ,UAAU,YAAY,CAAC,SAAS;AACvC,WAAO,iBAAiB,IAAI;AAAA,EAC7B,CAAC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/port-router.ts","../../src/extension/background.ts"],"sourcesContent":["export interface ExtensionPort {\n\tname: string\n\tonMessage: {\n\t\taddListener(callback: (message: unknown) => void): void\n\t}\n\tonDisconnect: {\n\t\taddListener(callback: () => void): void\n\t}\n\tpostMessage(message: unknown): void\n\tsender?: { tab?: { id?: number } }\n}\n\ninterface PanelClient {\n\tport: ExtensionPort\n\ttabId: number\n}\n\n/**\n * Routes content-script events to the matching DevTools panel by tab.\n */\nexport class PortRouter {\n\tprivate readonly panelClients = new Map<number, PanelClient>()\n\tprivate readonly contentClients = new Map<number, ExtensionPort>()\n\n\thandleConnection(port: ExtensionPort): void {\n\t\tif (port.name === 'kora-panel') {\n\t\t\tthis.attachPanel(port)\n\t\t\treturn\n\t\t}\n\n\t\tif (port.name === 'kora-content') {\n\t\t\tthis.attachContent(port)\n\t\t}\n\t}\n\n\tprivate attachPanel(port: ExtensionPort): void {\n\t\tport.onMessage.addListener((message) => {\n\t\t\tif (!isPanelInitMessage(message)) return\n\n\t\t\tthis.panelClients.set(message.tabId, { tabId: message.tabId, port })\n\t\t})\n\n\t\tport.onDisconnect.addListener(() => {\n\t\t\tfor (const [tabId, client] of this.panelClients) {\n\t\t\t\tif (client.port === port) {\n\t\t\t\t\tthis.panelClients.delete(tabId)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tprivate attachContent(port: ExtensionPort): void {\n\t\tconst tabId = port.sender?.tab?.id\n\t\tif (typeof tabId !== 'number') {\n\t\t\treturn\n\t\t}\n\n\t\tthis.contentClients.set(tabId, port)\n\n\t\tport.onMessage.addListener((message) => {\n\t\t\tif (!isContentEventMessage(message)) return\n\n\t\t\tconst panel = this.panelClients.get(tabId)\n\t\t\tif (!panel) return\n\n\t\t\tpanel.port.postMessage({ type: 'kora-event', payload: message.payload })\n\t\t})\n\n\t\tport.onDisconnect.addListener(() => {\n\t\t\tthis.contentClients.delete(tabId)\n\t\t})\n\t}\n}\n\nfunction isPanelInitMessage(value: unknown): value is { type: 'panel-init'; tabId: number } {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst record = value as Record<string, unknown>\n\treturn record.type === 'panel-init' && typeof record.tabId === 'number'\n}\n\nfunction isContentEventMessage(value: unknown): value is { type: 'kora-event'; payload: unknown } {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst record = value as Record<string, unknown>\n\treturn record.type === 'kora-event' && 'payload' in record\n}\n","import { PortRouter, type ExtensionPort } from './port-router'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst runtime = chrome?.runtime\n\nif (runtime?.onConnect) {\n\tconst router = new PortRouter()\n\truntime.onConnect.addListener((port: ExtensionPort) => {\n\t\trouter.handleConnection(port)\n\t})\n}\n"],"mappings":";;;AAoBO,IAAM,aAAN,MAAiB;AAAA,EACN,eAAe,oBAAI,IAAyB;AAAA,EAC5C,iBAAiB,oBAAI,IAA2B;AAAA,EAEjE,iBAAiB,MAA2B;AAC3C,QAAI,KAAK,SAAS,cAAc;AAC/B,WAAK,YAAY,IAAI;AACrB;AAAA,IACD;AAEA,QAAI,KAAK,SAAS,gBAAgB;AACjC,WAAK,cAAc,IAAI;AAAA,IACxB;AAAA,EACD;AAAA,EAEQ,YAAY,MAA2B;AAC9C,SAAK,UAAU,YAAY,CAAC,YAAY;AACvC,UAAI,CAAC,mBAAmB,OAAO,EAAG;AAElC,WAAK,aAAa,IAAI,QAAQ,OAAO,EAAE,OAAO,QAAQ,OAAO,KAAK,CAAC;AAAA,IACpE,CAAC;AAED,SAAK,aAAa,YAAY,MAAM;AACnC,iBAAW,CAAC,OAAO,MAAM,KAAK,KAAK,cAAc;AAChD,YAAI,OAAO,SAAS,MAAM;AACzB,eAAK,aAAa,OAAO,KAAK;AAAA,QAC/B;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEQ,cAAc,MAA2B;AAChD,UAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,QAAI,OAAO,UAAU,UAAU;AAC9B;AAAA,IACD;AAEA,SAAK,eAAe,IAAI,OAAO,IAAI;AAEnC,SAAK,UAAU,YAAY,CAAC,YAAY;AACvC,UAAI,CAAC,sBAAsB,OAAO,EAAG;AAErC,YAAM,QAAQ,KAAK,aAAa,IAAI,KAAK;AACzC,UAAI,CAAC,MAAO;AAEZ,YAAM,KAAK,YAAY,EAAE,MAAM,cAAc,SAAS,QAAQ,QAAQ,CAAC;AAAA,IACxE,CAAC;AAED,SAAK,aAAa,YAAY,MAAM;AACnC,WAAK,eAAe,OAAO,KAAK;AAAA,IACjC,CAAC;AAAA,EACF;AACD;AAEA,SAAS,mBAAmB,OAAgE;AAC3F,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,SAAS,gBAAgB,OAAO,OAAO,UAAU;AAChE;AAEA,SAAS,sBAAsB,OAAmE;AACjG,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,SAAS,gBAAgB,aAAa;AACrD;;;AC9EA,IAAM,UAAU,QAAQ;AAExB,IAAI,SAAS,WAAW;AACvB,QAAM,SAAS,IAAI,WAAW;AAC9B,UAAQ,UAAU,YAAY,CAAC,SAAwB;AACtD,WAAO,iBAAiB,IAAI;AAAA,EAC7B,CAAC;AACF;","names":[]}
@@ -1,9 +1,59 @@
1
- import {
2
- PortRouter
3
- } from "../chunk-JH2X4T4Z.js";
1
+ // src/extension/port-router.ts
2
+ var PortRouter = class {
3
+ panelClients = /* @__PURE__ */ new Map();
4
+ contentClients = /* @__PURE__ */ new Map();
5
+ handleConnection(port) {
6
+ if (port.name === "kora-panel") {
7
+ this.attachPanel(port);
8
+ return;
9
+ }
10
+ if (port.name === "kora-content") {
11
+ this.attachContent(port);
12
+ }
13
+ }
14
+ attachPanel(port) {
15
+ port.onMessage.addListener((message) => {
16
+ if (!isPanelInitMessage(message)) return;
17
+ this.panelClients.set(message.tabId, { tabId: message.tabId, port });
18
+ });
19
+ port.onDisconnect.addListener(() => {
20
+ for (const [tabId, client] of this.panelClients) {
21
+ if (client.port === port) {
22
+ this.panelClients.delete(tabId);
23
+ }
24
+ }
25
+ });
26
+ }
27
+ attachContent(port) {
28
+ const tabId = port.sender?.tab?.id;
29
+ if (typeof tabId !== "number") {
30
+ return;
31
+ }
32
+ this.contentClients.set(tabId, port);
33
+ port.onMessage.addListener((message) => {
34
+ if (!isContentEventMessage(message)) return;
35
+ const panel = this.panelClients.get(tabId);
36
+ if (!panel) return;
37
+ panel.port.postMessage({ type: "kora-event", payload: message.payload });
38
+ });
39
+ port.onDisconnect.addListener(() => {
40
+ this.contentClients.delete(tabId);
41
+ });
42
+ }
43
+ };
44
+ function isPanelInitMessage(value) {
45
+ if (typeof value !== "object" || value === null) return false;
46
+ const record = value;
47
+ return record.type === "panel-init" && typeof record.tabId === "number";
48
+ }
49
+ function isContentEventMessage(value) {
50
+ if (typeof value !== "object" || value === null) return false;
51
+ const record = value;
52
+ return record.type === "kora-event" && "payload" in record;
53
+ }
4
54
 
5
55
  // src/extension/background.ts
6
- var runtime = globalThis.chrome?.runtime;
56
+ var runtime = chrome?.runtime;
7
57
  if (runtime?.onConnect) {
8
58
  const router = new PortRouter();
9
59
  runtime.onConnect.addListener((port) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/background.ts"],"sourcesContent":["import { PortRouter, type ExtensionPort } from './port-router'\n\ninterface RuntimeLike {\n\tonConnect?: {\n\t\taddListener(callback: (port: ExtensionPort) => void): void\n\t}\n}\n\nconst runtime = (globalThis as { chrome?: { runtime?: RuntimeLike } }).chrome?.runtime\n\nif (runtime?.onConnect) {\n\tconst router = new PortRouter()\n\truntime.onConnect.addListener((port) => {\n\t\trouter.handleConnection(port)\n\t})\n}\n"],"mappings":";;;;;AAQA,IAAM,UAAW,WAAsD,QAAQ;AAE/E,IAAI,SAAS,WAAW;AACvB,QAAM,SAAS,IAAI,WAAW;AAC9B,UAAQ,UAAU,YAAY,CAAC,SAAS;AACvC,WAAO,iBAAiB,IAAI;AAAA,EAC7B,CAAC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/port-router.ts","../../src/extension/background.ts"],"sourcesContent":["export interface ExtensionPort {\n\tname: string\n\tonMessage: {\n\t\taddListener(callback: (message: unknown) => void): void\n\t}\n\tonDisconnect: {\n\t\taddListener(callback: () => void): void\n\t}\n\tpostMessage(message: unknown): void\n\tsender?: { tab?: { id?: number } }\n}\n\ninterface PanelClient {\n\tport: ExtensionPort\n\ttabId: number\n}\n\n/**\n * Routes content-script events to the matching DevTools panel by tab.\n */\nexport class PortRouter {\n\tprivate readonly panelClients = new Map<number, PanelClient>()\n\tprivate readonly contentClients = new Map<number, ExtensionPort>()\n\n\thandleConnection(port: ExtensionPort): void {\n\t\tif (port.name === 'kora-panel') {\n\t\t\tthis.attachPanel(port)\n\t\t\treturn\n\t\t}\n\n\t\tif (port.name === 'kora-content') {\n\t\t\tthis.attachContent(port)\n\t\t}\n\t}\n\n\tprivate attachPanel(port: ExtensionPort): void {\n\t\tport.onMessage.addListener((message) => {\n\t\t\tif (!isPanelInitMessage(message)) return\n\n\t\t\tthis.panelClients.set(message.tabId, { tabId: message.tabId, port })\n\t\t})\n\n\t\tport.onDisconnect.addListener(() => {\n\t\t\tfor (const [tabId, client] of this.panelClients) {\n\t\t\t\tif (client.port === port) {\n\t\t\t\t\tthis.panelClients.delete(tabId)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tprivate attachContent(port: ExtensionPort): void {\n\t\tconst tabId = port.sender?.tab?.id\n\t\tif (typeof tabId !== 'number') {\n\t\t\treturn\n\t\t}\n\n\t\tthis.contentClients.set(tabId, port)\n\n\t\tport.onMessage.addListener((message) => {\n\t\t\tif (!isContentEventMessage(message)) return\n\n\t\t\tconst panel = this.panelClients.get(tabId)\n\t\t\tif (!panel) return\n\n\t\t\tpanel.port.postMessage({ type: 'kora-event', payload: message.payload })\n\t\t})\n\n\t\tport.onDisconnect.addListener(() => {\n\t\t\tthis.contentClients.delete(tabId)\n\t\t})\n\t}\n}\n\nfunction isPanelInitMessage(value: unknown): value is { type: 'panel-init'; tabId: number } {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst record = value as Record<string, unknown>\n\treturn record.type === 'panel-init' && typeof record.tabId === 'number'\n}\n\nfunction isContentEventMessage(value: unknown): value is { type: 'kora-event'; payload: unknown } {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst record = value as Record<string, unknown>\n\treturn record.type === 'kora-event' && 'payload' in record\n}\n","import { PortRouter, type ExtensionPort } from './port-router'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst runtime = chrome?.runtime\n\nif (runtime?.onConnect) {\n\tconst router = new PortRouter()\n\truntime.onConnect.addListener((port: ExtensionPort) => {\n\t\trouter.handleConnection(port)\n\t})\n}\n"],"mappings":";AAoBO,IAAM,aAAN,MAAiB;AAAA,EACN,eAAe,oBAAI,IAAyB;AAAA,EAC5C,iBAAiB,oBAAI,IAA2B;AAAA,EAEjE,iBAAiB,MAA2B;AAC3C,QAAI,KAAK,SAAS,cAAc;AAC/B,WAAK,YAAY,IAAI;AACrB;AAAA,IACD;AAEA,QAAI,KAAK,SAAS,gBAAgB;AACjC,WAAK,cAAc,IAAI;AAAA,IACxB;AAAA,EACD;AAAA,EAEQ,YAAY,MAA2B;AAC9C,SAAK,UAAU,YAAY,CAAC,YAAY;AACvC,UAAI,CAAC,mBAAmB,OAAO,EAAG;AAElC,WAAK,aAAa,IAAI,QAAQ,OAAO,EAAE,OAAO,QAAQ,OAAO,KAAK,CAAC;AAAA,IACpE,CAAC;AAED,SAAK,aAAa,YAAY,MAAM;AACnC,iBAAW,CAAC,OAAO,MAAM,KAAK,KAAK,cAAc;AAChD,YAAI,OAAO,SAAS,MAAM;AACzB,eAAK,aAAa,OAAO,KAAK;AAAA,QAC/B;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEQ,cAAc,MAA2B;AAChD,UAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,QAAI,OAAO,UAAU,UAAU;AAC9B;AAAA,IACD;AAEA,SAAK,eAAe,IAAI,OAAO,IAAI;AAEnC,SAAK,UAAU,YAAY,CAAC,YAAY;AACvC,UAAI,CAAC,sBAAsB,OAAO,EAAG;AAErC,YAAM,QAAQ,KAAK,aAAa,IAAI,KAAK;AACzC,UAAI,CAAC,MAAO;AAEZ,YAAM,KAAK,YAAY,EAAE,MAAM,cAAc,SAAS,QAAQ,QAAQ,CAAC;AAAA,IACxE,CAAC;AAED,SAAK,aAAa,YAAY,MAAM;AACnC,WAAK,eAAe,OAAO,KAAK;AAAA,IACjC,CAAC;AAAA,EACF;AACD;AAEA,SAAS,mBAAmB,OAAgE;AAC3F,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,SAAS,gBAAgB,OAAO,OAAO,UAAU;AAChE;AAEA,SAAS,sBAAsB,OAAmE;AACjG,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,SAAO,OAAO,SAAS,gBAAgB,aAAa;AACrD;;;AC9EA,IAAM,UAAU,QAAQ;AAExB,IAAI,SAAS,WAAW;AACvB,QAAM,SAAS,IAAI,WAAW;AAC9B,UAAQ,UAAU,YAAY,CAAC,SAAwB;AACtD,WAAO,iBAAiB,IAAI;AAAA,EAC7B,CAAC;AACF;","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  // src/extension/content-script.ts
4
- var runtime = globalThis.chrome?.runtime;
4
+ var runtime = chrome?.runtime;
5
5
  if (runtime) {
6
6
  const port = runtime.connect({ name: "kora-content" });
7
7
  window.addEventListener("message", (event) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/content-script.ts"],"sourcesContent":["import type { TimestampedEvent } from '../types'\n\ninterface RuntimePort {\n\tpostMessage(message: unknown): void\n}\n\ninterface RuntimeLike {\n\tconnect(info: { name: string }): RuntimePort\n}\n\nconst runtime = (globalThis as { chrome?: { runtime?: RuntimeLike } }).chrome?.runtime\n\nif (runtime) {\n\tconst port = runtime.connect({ name: 'kora-content' })\n\n\twindow.addEventListener('message', (event: MessageEvent) => {\n\t\tconst data = event.data as { source?: string; payload?: TimestampedEvent } | undefined\n\t\tif (!data || data.source !== 'kora-devtools' || !data.payload) {\n\t\t\treturn\n\t\t}\n\n\t\tport.postMessage({\n\t\t\ttype: 'kora-event',\n\t\t\tpayload: data.payload,\n\t\t})\n\t})\n}\n"],"mappings":";;;AAUA,IAAM,UAAW,WAAsD,QAAQ;AAE/E,IAAI,SAAS;AACZ,QAAM,OAAO,QAAQ,QAAQ,EAAE,MAAM,eAAe,CAAC;AAErD,SAAO,iBAAiB,WAAW,CAAC,UAAwB;AAC3D,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,QAAQ,KAAK,WAAW,mBAAmB,CAAC,KAAK,SAAS;AAC9D;AAAA,IACD;AAEA,SAAK,YAAY;AAAA,MAChB,MAAM;AAAA,MACN,SAAS,KAAK;AAAA,IACf,CAAC;AAAA,EACF,CAAC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/content-script.ts"],"sourcesContent":["import type { TimestampedEvent } from '../types'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst runtime = chrome?.runtime\n\nif (runtime) {\n\tconst port = runtime.connect({ name: 'kora-content' })\n\n\twindow.addEventListener('message', (event: MessageEvent) => {\n\t\tconst data = event.data as { source?: string; payload?: TimestampedEvent } | undefined\n\t\tif (!data || data.source !== 'kora-devtools' || !data.payload) {\n\t\t\treturn\n\t\t}\n\n\t\tport.postMessage({\n\t\t\ttype: 'kora-event',\n\t\t\tpayload: data.payload,\n\t\t})\n\t})\n}\n"],"mappings":";;;AAMA,IAAM,UAAU,QAAQ;AAExB,IAAI,SAAS;AACZ,QAAM,OAAO,QAAQ,QAAQ,EAAE,MAAM,eAAe,CAAC;AAErD,SAAO,iBAAiB,WAAW,CAAC,UAAwB;AAC3D,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,QAAQ,KAAK,WAAW,mBAAmB,CAAC,KAAK,SAAS;AAC9D;AAAA,IACD;AAEA,SAAK,YAAY;AAAA,MAChB,MAAM;AAAA,MACN,SAAS,KAAK;AAAA,IACf,CAAC;AAAA,EACF,CAAC;AACF;","names":[]}
@@ -1,5 +1,5 @@
1
1
  // src/extension/content-script.ts
2
- var runtime = globalThis.chrome?.runtime;
2
+ var runtime = chrome?.runtime;
3
3
  if (runtime) {
4
4
  const port = runtime.connect({ name: "kora-content" });
5
5
  window.addEventListener("message", (event) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/content-script.ts"],"sourcesContent":["import type { TimestampedEvent } from '../types'\n\ninterface RuntimePort {\n\tpostMessage(message: unknown): void\n}\n\ninterface RuntimeLike {\n\tconnect(info: { name: string }): RuntimePort\n}\n\nconst runtime = (globalThis as { chrome?: { runtime?: RuntimeLike } }).chrome?.runtime\n\nif (runtime) {\n\tconst port = runtime.connect({ name: 'kora-content' })\n\n\twindow.addEventListener('message', (event: MessageEvent) => {\n\t\tconst data = event.data as { source?: string; payload?: TimestampedEvent } | undefined\n\t\tif (!data || data.source !== 'kora-devtools' || !data.payload) {\n\t\t\treturn\n\t\t}\n\n\t\tport.postMessage({\n\t\t\ttype: 'kora-event',\n\t\t\tpayload: data.payload,\n\t\t})\n\t})\n}\n"],"mappings":";AAUA,IAAM,UAAW,WAAsD,QAAQ;AAE/E,IAAI,SAAS;AACZ,QAAM,OAAO,QAAQ,QAAQ,EAAE,MAAM,eAAe,CAAC;AAErD,SAAO,iBAAiB,WAAW,CAAC,UAAwB;AAC3D,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,QAAQ,KAAK,WAAW,mBAAmB,CAAC,KAAK,SAAS;AAC9D;AAAA,IACD;AAEA,SAAK,YAAY;AAAA,MAChB,MAAM;AAAA,MACN,SAAS,KAAK;AAAA,IACf,CAAC;AAAA,EACF,CAAC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/content-script.ts"],"sourcesContent":["import type { TimestampedEvent } from '../types'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst runtime = chrome?.runtime\n\nif (runtime) {\n\tconst port = runtime.connect({ name: 'kora-content' })\n\n\twindow.addEventListener('message', (event: MessageEvent) => {\n\t\tconst data = event.data as { source?: string; payload?: TimestampedEvent } | undefined\n\t\tif (!data || data.source !== 'kora-devtools' || !data.payload) {\n\t\t\treturn\n\t\t}\n\n\t\tport.postMessage({\n\t\t\ttype: 'kora-event',\n\t\t\tpayload: data.payload,\n\t\t})\n\t})\n}\n"],"mappings":";AAMA,IAAM,UAAU,QAAQ;AAExB,IAAI,SAAS;AACZ,QAAM,OAAO,QAAQ,QAAQ,EAAE,MAAM,eAAe,CAAC;AAErD,SAAO,iBAAiB,WAAW,CAAC,UAAwB;AAC3D,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,QAAQ,KAAK,WAAW,mBAAmB,CAAC,KAAK,SAAS;AAC9D;AAAA,IACD;AAEA,SAAK,YAAY;AAAA,MAChB,MAAM;AAAA,MACN,SAAS,KAAK;AAAA,IACf,CAAC;AAAA,EACF,CAAC;AACF;","names":[]}
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  // src/extension/devtools.ts
4
- var devtools = globalThis.chrome?.devtools;
5
- if (devtools?.panels) {
6
- devtools.panels.create("Kora", "", "devtools-page.html", () => {
4
+ if (chrome?.devtools?.panels) {
5
+ chrome.devtools.panels.create("Kora", "", "devtools-page.html", () => {
7
6
  });
8
7
  }
9
8
  //# sourceMappingURL=devtools.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/devtools.ts"],"sourcesContent":["interface DevtoolsLike {\n\tpanels: {\n\t\tcreate(\n\t\t\ttitle: string,\n\t\t\ticonPath: string,\n\t\t\tpagePath: string,\n\t\t\tcallback: () => void,\n\t\t): void\n\t}\n}\n\nconst devtools = (globalThis as { chrome?: { devtools?: DevtoolsLike } }).chrome?.devtools\n\nif (devtools?.panels) {\n\tdevtools.panels.create('Kora', '', 'devtools-page.html', () => {})\n}\n"],"mappings":";;;AAWA,IAAM,WAAY,WAAwD,QAAQ;AAElF,IAAI,UAAU,QAAQ;AACrB,WAAS,OAAO,OAAO,QAAQ,IAAI,sBAAsB,MAAM;AAAA,EAAC,CAAC;AAClE;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/devtools.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nif (chrome?.devtools?.panels) {\n\tchrome.devtools.panels.create('Kora', '', 'devtools-page.html', () => {})\n}\n"],"mappings":";;;AAIA,IAAI,QAAQ,UAAU,QAAQ;AAC7B,SAAO,SAAS,OAAO,OAAO,QAAQ,IAAI,sBAAsB,MAAM;AAAA,EAAC,CAAC;AACzE;","names":[]}
@@ -0,0 +1,7 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8" /></head>
4
+ <body>
5
+ <script type="module" src="./devtools.js"></script>
6
+ </body>
7
+ </html>
@@ -1,7 +1,6 @@
1
1
  // src/extension/devtools.ts
2
- var devtools = globalThis.chrome?.devtools;
3
- if (devtools?.panels) {
4
- devtools.panels.create("Kora", "", "devtools-page.html", () => {
2
+ if (chrome?.devtools?.panels) {
3
+ chrome.devtools.panels.create("Kora", "", "devtools-page.html", () => {
5
4
  });
6
5
  }
7
6
  //# sourceMappingURL=devtools.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/devtools.ts"],"sourcesContent":["interface DevtoolsLike {\n\tpanels: {\n\t\tcreate(\n\t\t\ttitle: string,\n\t\t\ticonPath: string,\n\t\t\tpagePath: string,\n\t\t\tcallback: () => void,\n\t\t): void\n\t}\n}\n\nconst devtools = (globalThis as { chrome?: { devtools?: DevtoolsLike } }).chrome?.devtools\n\nif (devtools?.panels) {\n\tdevtools.panels.create('Kora', '', 'devtools-page.html', () => {})\n}\n"],"mappings":";AAWA,IAAM,WAAY,WAAwD,QAAQ;AAElF,IAAI,UAAU,QAAQ;AACrB,WAAS,OAAO,OAAO,QAAQ,IAAI,sBAAsB,MAAM;AAAA,EAAC,CAAC;AAClE;","names":[]}
1
+ {"version":3,"sources":["../../src/extension/devtools.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nif (chrome?.devtools?.panels) {\n\tchrome.devtools.panels.create('Kora', '', 'devtools-page.html', () => {})\n}\n"],"mappings":";AAIA,IAAI,QAAQ,UAAU,QAAQ;AAC7B,SAAO,SAAS,OAAO,OAAO,QAAQ,IAAI,sBAAsB,MAAM;AAAA,EAAC,CAAC;AACzE;","names":[]}
@@ -9,7 +9,7 @@
9
9
  "service_worker": "background.js",
10
10
  "type": "module"
11
11
  },
12
- "devtools_page": "devtools.js",
12
+ "devtools_page": "devtools.html",
13
13
  "content_scripts": [
14
14
  {
15
15
  "matches": ["<all_urls>"],
@@ -203,16 +203,16 @@ var extensionRoot = document.getElementById("kora-devtools-root");
203
203
  if (!extensionRoot) {
204
204
  throw new Error("Missing #kora-devtools-root element");
205
205
  }
206
- var runtime = globalThis.chrome?.runtime;
207
- var devtools = globalThis.chrome?.devtools;
208
206
  var events = [];
207
+ var runtime = chrome?.runtime;
208
+ var devtools = chrome?.devtools;
209
209
  if (runtime && devtools) {
210
+ const tabId = devtools.inspectedWindow.tabId;
210
211
  const port = runtime.connect({ name: "kora-panel" });
211
- port.postMessage({ type: "panel-init", tabId: devtools.inspectedWindow.tabId });
212
+ port.postMessage({ type: "panel-init", tabId });
212
213
  port.onMessage.addListener((message) => {
213
- const typed = message;
214
- if (!typed || typed.type !== "kora-event" || !typed.payload) return;
215
- events.push(typed.payload);
214
+ if (!message || message.type !== "kora-event" || !message.payload) return;
215
+ events.push(message.payload);
216
216
  renderDevtoolsPanel(extensionRoot, events);
217
217
  });
218
218
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/ui/panel-state.ts","../../src/ui/panel.ts","../../src/extension/panel.ts"],"sourcesContent":["import type { KoraEvent, Operation } from '@korajs/core'\nimport type { TimestampedEvent } from '../types'\n\nexport interface TimelineItem {\n\tid: number\n\ttype: KoraEvent['type']\n\tlabel: string\n\tcolor: string\n\treceivedAt: number\n\tdependsOn: string[]\n}\n\nexport interface ConflictItem {\n\tid: number\n\ttimestamp: number\n\tcollection: string\n\tfield: string\n\tstrategy: string\n\ttier: 1 | 2 | 3\n\tinputA: unknown\n\tinputB: unknown\n\toutput: unknown\n\tconstraintViolated: string | null\n}\n\nexport interface OperationItem {\n\tid: number\n\ttimestamp: number\n\toperationId: string\n\tcollection: string\n\trecordId: string\n\topType: Operation['type']\n\tdata: Record<string, unknown> | null\n\tcausalDeps: string[]\n\tnodeId: string\n\tsequenceNumber: number\n}\n\nexport interface NetworkStatusModel {\n\tconnected: boolean\n\tquality: string | null\n\tpendingAcks: number\n\tlastSyncAt: number | null\n\tsentOps: number\n\treceivedOps: number\n\tversionVector: Array<{ nodeId: string; sequenceNumber: number }>\n}\n\nexport interface DevtoolsPanelModel {\n\ttimeline: TimelineItem[]\n\tconflicts: ConflictItem[]\n\toperations: OperationItem[]\n\tnetwork: NetworkStatusModel\n}\n\nexport function buildPanelModel(events: readonly TimestampedEvent[]): DevtoolsPanelModel {\n\tconst timeline = events.map((entry) => ({\n\t\tid: entry.id,\n\t\ttype: entry.event.type,\n\t\tlabel: timelineLabel(entry.event),\n\t\tcolor: timelineColor(entry.event.type),\n\t\treceivedAt: entry.receivedAt,\n\t\tdependsOn: extractCausalDependencies(entry.event),\n\t}))\n\n\tconst conflicts = events\n\t\t.flatMap((entry) => {\n\t\t\tif (entry.event.type !== 'merge:completed' && entry.event.type !== 'merge:conflict') {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\tconst trace = entry.event.trace\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\ttimestamp: entry.receivedAt,\n\t\t\t\t\tcollection: trace.operationA.collection,\n\t\t\t\t\tfield: trace.field,\n\t\t\t\t\tstrategy: trace.strategy,\n\t\t\t\t\ttier: trace.tier,\n\t\t\t\t\tinputA: trace.inputA,\n\t\t\t\t\tinputB: trace.inputB,\n\t\t\t\t\toutput: trace.output,\n\t\t\t\t\tconstraintViolated: trace.constraintViolated,\n\t\t\t\t},\n\t\t\t]\n\t\t})\n\n\tconst operations = events\n\t\t.map((entry) => {\n\t\t\tconst operation = extractOperation(entry.event)\n\t\t\tif (!operation) return null\n\n\t\t\treturn {\n\t\t\t\tid: entry.id,\n\t\t\t\ttimestamp: entry.receivedAt,\n\t\t\t\toperationId: operation.id,\n\t\t\t\tcollection: operation.collection,\n\t\t\t\trecordId: operation.recordId,\n\t\t\t\topType: operation.type,\n\t\t\t\tdata: operation.data,\n\t\t\t\tcausalDeps: operation.causalDeps,\n\t\t\t\tnodeId: operation.nodeId,\n\t\t\t\tsequenceNumber: operation.sequenceNumber,\n\t\t\t}\n\t\t})\n\t\t.filter((item): item is OperationItem => item !== null)\n\n\tconst network = buildNetworkStatus(events, operations)\n\n\treturn {\n\t\ttimeline,\n\t\tconflicts,\n\t\toperations,\n\t\tnetwork,\n\t}\n}\n\nfunction buildNetworkStatus(\n\tevents: readonly TimestampedEvent[],\n\toperations: readonly OperationItem[],\n): NetworkStatusModel {\n\tlet connected = false\n\tlet quality: string | null = null\n\tlet pendingAcks = 0\n\tlet lastSyncAt: number | null = null\n\tlet sentOps = 0\n\tlet receivedOps = 0\n\n\tfor (const entry of events) {\n\t\tswitch (entry.event.type) {\n\t\t\tcase 'sync:connected':\n\t\t\t\tconnected = true\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:disconnected':\n\t\t\t\tconnected = false\n\t\t\t\tbreak\n\t\t\tcase 'connection:quality':\n\t\t\t\tquality = entry.event.quality\n\t\t\t\tbreak\n\t\t\tcase 'sync:sent':\n\t\t\t\tsentOps += entry.event.operations.length\n\t\t\t\tpendingAcks += entry.event.operations.length\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:received':\n\t\t\t\treceivedOps += entry.event.operations.length\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:acknowledged':\n\t\t\t\tpendingAcks = Math.max(0, pendingAcks - 1)\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t}\n\t}\n\n\tconst vector = new Map<string, number>()\n\tfor (const operation of operations) {\n\t\tconst current = vector.get(operation.nodeId) ?? 0\n\t\tif (operation.sequenceNumber > current) {\n\t\t\tvector.set(operation.nodeId, operation.sequenceNumber)\n\t\t}\n\t}\n\n\treturn {\n\t\tconnected,\n\t\tquality,\n\t\tpendingAcks,\n\t\tlastSyncAt,\n\t\tsentOps,\n\t\treceivedOps,\n\t\tversionVector: [...vector.entries()]\n\t\t\t.map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber }))\n\t\t\t.sort((left, right) => left.nodeId.localeCompare(right.nodeId)),\n\t}\n}\n\nfunction timelineLabel(event: KoraEvent): string {\n\tswitch (event.type) {\n\t\tcase 'operation:created':\n\t\tcase 'operation:applied':\n\t\t\treturn `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`\n\t\tcase 'merge:started':\n\t\t\treturn `merge start ${event.operationA.collection}`\n\t\tcase 'merge:completed':\n\t\t\treturn `merge complete ${event.trace.field}`\n\t\tcase 'merge:conflict':\n\t\t\treturn `merge conflict ${event.trace.field}`\n\t\tcase 'constraint:violated':\n\t\t\treturn `constraint ${event.constraint}`\n\t\tcase 'sync:connected':\n\t\t\treturn `sync connected ${event.nodeId}`\n\t\tcase 'sync:disconnected':\n\t\t\treturn `sync disconnected`\n\t\tcase 'sync:sent':\n\t\t\treturn `sync sent ${event.batchSize}`\n\t\tcase 'sync:received':\n\t\t\treturn `sync received ${event.batchSize}`\n\t\tcase 'sync:acknowledged':\n\t\t\treturn `sync ack ${event.sequenceNumber}`\n\t\tcase 'query:subscribed':\n\t\t\treturn `query subscribed ${event.collection}`\n\t\tcase 'query:invalidated':\n\t\t\treturn `query invalidated ${event.queryId}`\n\t\tcase 'query:executed':\n\t\t\treturn `query executed ${event.queryId}`\n\t\tcase 'connection:quality':\n\t\t\treturn `connection ${event.quality}`\n\t}\n}\n\nfunction timelineColor(type: KoraEvent['type']): string {\n\tif (type.startsWith('operation:')) return '#22c55e'\n\tif (type.startsWith('sync:')) return '#a855f7'\n\tif (type.startsWith('merge:') || type.startsWith('constraint:')) return '#f59e0b'\n\tif (type.startsWith('query:')) return '#0ea5e9'\n\treturn '#64748b'\n}\n\nfunction extractCausalDependencies(event: KoraEvent): string[] {\n\tconst operation = extractOperation(event)\n\treturn operation?.causalDeps ?? []\n}\n\nfunction extractOperation(event: KoraEvent): Operation | null {\n\tswitch (event.type) {\n\t\tcase 'operation:created':\n\t\tcase 'operation:applied':\n\t\t\treturn event.operation\n\t\tcase 'query:invalidated':\n\t\t\treturn event.trigger\n\t\tdefault:\n\t\t\treturn null\n\t}\n}\n","import type { TimestampedEvent } from '../types'\nimport { buildPanelModel } from './panel-state'\n\nexport function renderDevtoolsPanel(target: HTMLElement, events: readonly TimestampedEvent[]): void {\n\tconst model = buildPanelModel(events)\n\n\ttarget.innerHTML = [\n\t\t'<section data-panel=\"timeline\"><h2>Sync Timeline</h2>',\n\t\t`<p>Total events: ${model.timeline.length}</p>`,\n\t\t'<ul>',\n\t\t...model.timeline.slice(-20).map(\n\t\t\t(item) =>\n\t\t\t\t`<li><span style=\"color:${item.color}\">${item.type}</span> · ${escapeHtml(item.label)}</li>`,\n\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"conflicts\"><h2>Conflict Inspector</h2>',\n\t\t`<p>Conflicts: ${model.conflicts.length}</p>`,\n\t\t'<ul>',\n\t\t...model.conflicts\n\t\t\t.slice(-20)\n\t\t\t.map(\n\t\t\t\t(item) =>\n\t\t\t\t\t`<li>${escapeHtml(item.collection)}.${escapeHtml(item.field)} · ${escapeHtml(item.strategy)} · tier ${item.tier}</li>`,\n\t\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"operations\"><h2>Operation Log</h2>',\n\t\t`<p>Operations: ${model.operations.length}</p>`,\n\t\t'<ul>',\n\t\t...model.operations.slice(-20).map(\n\t\t\t(item) =>\n\t\t\t\t`<li>${escapeHtml(item.opType)} ${escapeHtml(item.collection)}/${escapeHtml(item.recordId)} (${escapeHtml(item.operationId)})</li>`,\n\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"network\"><h2>Network Status</h2>',\n\t\t`<p>Connected: ${model.network.connected ? 'yes' : 'no'}</p>`,\n\t\t`<p>Pending ACKs: ${model.network.pendingAcks}</p>`,\n\t\t`<p>Sent ops: ${model.network.sentOps}</p>`,\n\t\t`<p>Received ops: ${model.network.receivedOps}</p>`,\n\t\t'</section>',\n\t].join('')\n}\n\nfunction escapeHtml(value: string): string {\n\treturn value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')\n}\n","import type { TimestampedEvent } from '../types'\nimport { renderDevtoolsPanel } from '../ui/panel'\n\ninterface RuntimePort {\n\tonMessage: {\n\t\taddListener(callback: (message: unknown) => void): void\n\t}\n\tpostMessage(message: unknown): void\n}\n\ninterface RuntimeLike {\n\tconnect(info: { name: string }): RuntimePort\n}\n\ninterface DevtoolsLike {\n\tinspectedWindow: { tabId: number }\n}\n\nconst extensionRoot = document.getElementById('kora-devtools-root')\nif (!extensionRoot) {\n\tthrow new Error('Missing #kora-devtools-root element')\n}\n\nconst runtime = (globalThis as { chrome?: { runtime?: RuntimeLike } }).chrome?.runtime\nconst devtools = (globalThis as { chrome?: { devtools?: DevtoolsLike } }).chrome?.devtools\n\nconst events: TimestampedEvent[] = []\n\nif (runtime && devtools) {\n\tconst port = runtime.connect({ name: 'kora-panel' })\n\tport.postMessage({ type: 'panel-init', tabId: devtools.inspectedWindow.tabId })\n\n\tport.onMessage.addListener((message) => {\n\t\tconst typed = message as { type?: string; payload?: TimestampedEvent } | undefined\n\t\tif (!typed || typed.type !== 'kora-event' || !typed.payload) return\n\n\t\tevents.push(typed.payload)\n\t\trenderDevtoolsPanel(extensionRoot, events)\n\t})\n}\n\nrenderDevtoolsPanel(extensionRoot, events)\n"],"mappings":"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","names":["events","events"]}
1
+ {"version":3,"sources":["../../src/ui/panel-state.ts","../../src/ui/panel.ts","../../src/extension/panel.ts"],"sourcesContent":["import type { KoraEvent, Operation } from '@korajs/core'\nimport type { TimestampedEvent } from '../types'\n\nexport interface TimelineItem {\n\tid: number\n\ttype: KoraEvent['type']\n\tlabel: string\n\tcolor: string\n\treceivedAt: number\n\tdependsOn: string[]\n}\n\nexport interface ConflictItem {\n\tid: number\n\ttimestamp: number\n\tcollection: string\n\tfield: string\n\tstrategy: string\n\ttier: 1 | 2 | 3\n\tinputA: unknown\n\tinputB: unknown\n\toutput: unknown\n\tconstraintViolated: string | null\n}\n\nexport interface OperationItem {\n\tid: number\n\ttimestamp: number\n\toperationId: string\n\tcollection: string\n\trecordId: string\n\topType: Operation['type']\n\tdata: Record<string, unknown> | null\n\tcausalDeps: string[]\n\tnodeId: string\n\tsequenceNumber: number\n}\n\nexport interface NetworkStatusModel {\n\tconnected: boolean\n\tquality: string | null\n\tpendingAcks: number\n\tlastSyncAt: number | null\n\tsentOps: number\n\treceivedOps: number\n\tversionVector: Array<{ nodeId: string; sequenceNumber: number }>\n}\n\nexport interface DevtoolsPanelModel {\n\ttimeline: TimelineItem[]\n\tconflicts: ConflictItem[]\n\toperations: OperationItem[]\n\tnetwork: NetworkStatusModel\n}\n\nexport function buildPanelModel(events: readonly TimestampedEvent[]): DevtoolsPanelModel {\n\tconst timeline = events.map((entry) => ({\n\t\tid: entry.id,\n\t\ttype: entry.event.type,\n\t\tlabel: timelineLabel(entry.event),\n\t\tcolor: timelineColor(entry.event.type),\n\t\treceivedAt: entry.receivedAt,\n\t\tdependsOn: extractCausalDependencies(entry.event),\n\t}))\n\n\tconst conflicts = events\n\t\t.flatMap((entry) => {\n\t\t\tif (entry.event.type !== 'merge:completed' && entry.event.type !== 'merge:conflict') {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\tconst trace = entry.event.trace\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\ttimestamp: entry.receivedAt,\n\t\t\t\t\tcollection: trace.operationA.collection,\n\t\t\t\t\tfield: trace.field,\n\t\t\t\t\tstrategy: trace.strategy,\n\t\t\t\t\ttier: trace.tier,\n\t\t\t\t\tinputA: trace.inputA,\n\t\t\t\t\tinputB: trace.inputB,\n\t\t\t\t\toutput: trace.output,\n\t\t\t\t\tconstraintViolated: trace.constraintViolated,\n\t\t\t\t},\n\t\t\t]\n\t\t})\n\n\tconst operations = events\n\t\t.map((entry) => {\n\t\t\tconst operation = extractOperation(entry.event)\n\t\t\tif (!operation) return null\n\n\t\t\treturn {\n\t\t\t\tid: entry.id,\n\t\t\t\ttimestamp: entry.receivedAt,\n\t\t\t\toperationId: operation.id,\n\t\t\t\tcollection: operation.collection,\n\t\t\t\trecordId: operation.recordId,\n\t\t\t\topType: operation.type,\n\t\t\t\tdata: operation.data,\n\t\t\t\tcausalDeps: operation.causalDeps,\n\t\t\t\tnodeId: operation.nodeId,\n\t\t\t\tsequenceNumber: operation.sequenceNumber,\n\t\t\t}\n\t\t})\n\t\t.filter((item): item is OperationItem => item !== null)\n\n\tconst network = buildNetworkStatus(events, operations)\n\n\treturn {\n\t\ttimeline,\n\t\tconflicts,\n\t\toperations,\n\t\tnetwork,\n\t}\n}\n\nfunction buildNetworkStatus(\n\tevents: readonly TimestampedEvent[],\n\toperations: readonly OperationItem[],\n): NetworkStatusModel {\n\tlet connected = false\n\tlet quality: string | null = null\n\tlet pendingAcks = 0\n\tlet lastSyncAt: number | null = null\n\tlet sentOps = 0\n\tlet receivedOps = 0\n\n\tfor (const entry of events) {\n\t\tswitch (entry.event.type) {\n\t\t\tcase 'sync:connected':\n\t\t\t\tconnected = true\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:disconnected':\n\t\t\t\tconnected = false\n\t\t\t\tbreak\n\t\t\tcase 'connection:quality':\n\t\t\t\tquality = entry.event.quality\n\t\t\t\tbreak\n\t\t\tcase 'sync:sent':\n\t\t\t\tsentOps += entry.event.operations.length\n\t\t\t\tpendingAcks += entry.event.operations.length\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:received':\n\t\t\t\treceivedOps += entry.event.operations.length\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:acknowledged':\n\t\t\t\tpendingAcks = Math.max(0, pendingAcks - 1)\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t}\n\t}\n\n\tconst vector = new Map<string, number>()\n\tfor (const operation of operations) {\n\t\tconst current = vector.get(operation.nodeId) ?? 0\n\t\tif (operation.sequenceNumber > current) {\n\t\t\tvector.set(operation.nodeId, operation.sequenceNumber)\n\t\t}\n\t}\n\n\treturn {\n\t\tconnected,\n\t\tquality,\n\t\tpendingAcks,\n\t\tlastSyncAt,\n\t\tsentOps,\n\t\treceivedOps,\n\t\tversionVector: [...vector.entries()]\n\t\t\t.map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber }))\n\t\t\t.sort((left, right) => left.nodeId.localeCompare(right.nodeId)),\n\t}\n}\n\nfunction timelineLabel(event: KoraEvent): string {\n\tswitch (event.type) {\n\t\tcase 'operation:created':\n\t\tcase 'operation:applied':\n\t\t\treturn `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`\n\t\tcase 'merge:started':\n\t\t\treturn `merge start ${event.operationA.collection}`\n\t\tcase 'merge:completed':\n\t\t\treturn `merge complete ${event.trace.field}`\n\t\tcase 'merge:conflict':\n\t\t\treturn `merge conflict ${event.trace.field}`\n\t\tcase 'constraint:violated':\n\t\t\treturn `constraint ${event.constraint}`\n\t\tcase 'sync:connected':\n\t\t\treturn `sync connected ${event.nodeId}`\n\t\tcase 'sync:disconnected':\n\t\t\treturn `sync disconnected`\n\t\tcase 'sync:sent':\n\t\t\treturn `sync sent ${event.batchSize}`\n\t\tcase 'sync:received':\n\t\t\treturn `sync received ${event.batchSize}`\n\t\tcase 'sync:acknowledged':\n\t\t\treturn `sync ack ${event.sequenceNumber}`\n\t\tcase 'query:subscribed':\n\t\t\treturn `query subscribed ${event.collection}`\n\t\tcase 'query:invalidated':\n\t\t\treturn `query invalidated ${event.queryId}`\n\t\tcase 'query:executed':\n\t\t\treturn `query executed ${event.queryId}`\n\t\tcase 'connection:quality':\n\t\t\treturn `connection ${event.quality}`\n\t}\n}\n\nfunction timelineColor(type: KoraEvent['type']): string {\n\tif (type.startsWith('operation:')) return '#22c55e'\n\tif (type.startsWith('sync:')) return '#a855f7'\n\tif (type.startsWith('merge:') || type.startsWith('constraint:')) return '#f59e0b'\n\tif (type.startsWith('query:')) return '#0ea5e9'\n\treturn '#64748b'\n}\n\nfunction extractCausalDependencies(event: KoraEvent): string[] {\n\tconst operation = extractOperation(event)\n\treturn operation?.causalDeps ?? []\n}\n\nfunction extractOperation(event: KoraEvent): Operation | null {\n\tswitch (event.type) {\n\t\tcase 'operation:created':\n\t\tcase 'operation:applied':\n\t\t\treturn event.operation\n\t\tcase 'query:invalidated':\n\t\t\treturn event.trigger\n\t\tdefault:\n\t\t\treturn null\n\t}\n}\n","import type { TimestampedEvent } from '../types'\nimport { buildPanelModel } from './panel-state'\n\nexport function renderDevtoolsPanel(target: HTMLElement, events: readonly TimestampedEvent[]): void {\n\tconst model = buildPanelModel(events)\n\n\ttarget.innerHTML = [\n\t\t'<section data-panel=\"timeline\"><h2>Sync Timeline</h2>',\n\t\t`<p>Total events: ${model.timeline.length}</p>`,\n\t\t'<ul>',\n\t\t...model.timeline.slice(-20).map(\n\t\t\t(item) =>\n\t\t\t\t`<li><span style=\"color:${item.color}\">${item.type}</span> · ${escapeHtml(item.label)}</li>`,\n\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"conflicts\"><h2>Conflict Inspector</h2>',\n\t\t`<p>Conflicts: ${model.conflicts.length}</p>`,\n\t\t'<ul>',\n\t\t...model.conflicts\n\t\t\t.slice(-20)\n\t\t\t.map(\n\t\t\t\t(item) =>\n\t\t\t\t\t`<li>${escapeHtml(item.collection)}.${escapeHtml(item.field)} · ${escapeHtml(item.strategy)} · tier ${item.tier}</li>`,\n\t\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"operations\"><h2>Operation Log</h2>',\n\t\t`<p>Operations: ${model.operations.length}</p>`,\n\t\t'<ul>',\n\t\t...model.operations.slice(-20).map(\n\t\t\t(item) =>\n\t\t\t\t`<li>${escapeHtml(item.opType)} ${escapeHtml(item.collection)}/${escapeHtml(item.recordId)} (${escapeHtml(item.operationId)})</li>`,\n\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"network\"><h2>Network Status</h2>',\n\t\t`<p>Connected: ${model.network.connected ? 'yes' : 'no'}</p>`,\n\t\t`<p>Pending ACKs: ${model.network.pendingAcks}</p>`,\n\t\t`<p>Sent ops: ${model.network.sentOps}</p>`,\n\t\t`<p>Received ops: ${model.network.receivedOps}</p>`,\n\t\t'</section>',\n\t].join('')\n}\n\nfunction escapeHtml(value: string): string {\n\treturn value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')\n}\n","import type { TimestampedEvent } from '../types'\nimport { renderDevtoolsPanel } from '../ui/panel'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst extensionRoot = document.getElementById('kora-devtools-root')\nif (!extensionRoot) {\n\tthrow new Error('Missing #kora-devtools-root element')\n}\n\nconst events: TimestampedEvent[] = []\n\nconst runtime = chrome?.runtime\nconst devtools = chrome?.devtools\n\nif (runtime && devtools) {\n\tconst tabId = devtools.inspectedWindow.tabId\n\tconst port = runtime.connect({ name: 'kora-panel' })\n\tport.postMessage({ type: 'panel-init', tabId })\n\n\tport.onMessage.addListener((message: { type?: string; payload?: TimestampedEvent }) => {\n\t\tif (!message || message.type !== 'kora-event' || !message.payload) return\n\n\t\tevents.push(message.payload)\n\t\trenderDevtoolsPanel(extensionRoot, events)\n\t})\n}\n\nrenderDevtoolsPanel(extensionRoot, events)\n"],"mappings":"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","names":["events","events"]}
@@ -1,22 +1,216 @@
1
- import {
2
- renderDevtoolsPanel
3
- } from "../chunk-4ZQ2RTZM.js";
1
+ // src/ui/panel-state.ts
2
+ function buildPanelModel(events2) {
3
+ const timeline = events2.map((entry) => ({
4
+ id: entry.id,
5
+ type: entry.event.type,
6
+ label: timelineLabel(entry.event),
7
+ color: timelineColor(entry.event.type),
8
+ receivedAt: entry.receivedAt,
9
+ dependsOn: extractCausalDependencies(entry.event)
10
+ }));
11
+ const conflicts = events2.flatMap((entry) => {
12
+ if (entry.event.type !== "merge:completed" && entry.event.type !== "merge:conflict") {
13
+ return [];
14
+ }
15
+ const trace = entry.event.trace;
16
+ return [
17
+ {
18
+ id: entry.id,
19
+ timestamp: entry.receivedAt,
20
+ collection: trace.operationA.collection,
21
+ field: trace.field,
22
+ strategy: trace.strategy,
23
+ tier: trace.tier,
24
+ inputA: trace.inputA,
25
+ inputB: trace.inputB,
26
+ output: trace.output,
27
+ constraintViolated: trace.constraintViolated
28
+ }
29
+ ];
30
+ });
31
+ const operations = events2.map((entry) => {
32
+ const operation = extractOperation(entry.event);
33
+ if (!operation) return null;
34
+ return {
35
+ id: entry.id,
36
+ timestamp: entry.receivedAt,
37
+ operationId: operation.id,
38
+ collection: operation.collection,
39
+ recordId: operation.recordId,
40
+ opType: operation.type,
41
+ data: operation.data,
42
+ causalDeps: operation.causalDeps,
43
+ nodeId: operation.nodeId,
44
+ sequenceNumber: operation.sequenceNumber
45
+ };
46
+ }).filter((item) => item !== null);
47
+ const network = buildNetworkStatus(events2, operations);
48
+ return {
49
+ timeline,
50
+ conflicts,
51
+ operations,
52
+ network
53
+ };
54
+ }
55
+ function buildNetworkStatus(events2, operations) {
56
+ let connected = false;
57
+ let quality = null;
58
+ let pendingAcks = 0;
59
+ let lastSyncAt = null;
60
+ let sentOps = 0;
61
+ let receivedOps = 0;
62
+ for (const entry of events2) {
63
+ switch (entry.event.type) {
64
+ case "sync:connected":
65
+ connected = true;
66
+ lastSyncAt = entry.receivedAt;
67
+ break;
68
+ case "sync:disconnected":
69
+ connected = false;
70
+ break;
71
+ case "connection:quality":
72
+ quality = entry.event.quality;
73
+ break;
74
+ case "sync:sent":
75
+ sentOps += entry.event.operations.length;
76
+ pendingAcks += entry.event.operations.length;
77
+ lastSyncAt = entry.receivedAt;
78
+ break;
79
+ case "sync:received":
80
+ receivedOps += entry.event.operations.length;
81
+ lastSyncAt = entry.receivedAt;
82
+ break;
83
+ case "sync:acknowledged":
84
+ pendingAcks = Math.max(0, pendingAcks - 1);
85
+ lastSyncAt = entry.receivedAt;
86
+ break;
87
+ }
88
+ }
89
+ const vector = /* @__PURE__ */ new Map();
90
+ for (const operation of operations) {
91
+ const current = vector.get(operation.nodeId) ?? 0;
92
+ if (operation.sequenceNumber > current) {
93
+ vector.set(operation.nodeId, operation.sequenceNumber);
94
+ }
95
+ }
96
+ return {
97
+ connected,
98
+ quality,
99
+ pendingAcks,
100
+ lastSyncAt,
101
+ sentOps,
102
+ receivedOps,
103
+ versionVector: [...vector.entries()].map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber })).sort((left, right) => left.nodeId.localeCompare(right.nodeId))
104
+ };
105
+ }
106
+ function timelineLabel(event) {
107
+ switch (event.type) {
108
+ case "operation:created":
109
+ case "operation:applied":
110
+ return `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`;
111
+ case "merge:started":
112
+ return `merge start ${event.operationA.collection}`;
113
+ case "merge:completed":
114
+ return `merge complete ${event.trace.field}`;
115
+ case "merge:conflict":
116
+ return `merge conflict ${event.trace.field}`;
117
+ case "constraint:violated":
118
+ return `constraint ${event.constraint}`;
119
+ case "sync:connected":
120
+ return `sync connected ${event.nodeId}`;
121
+ case "sync:disconnected":
122
+ return `sync disconnected`;
123
+ case "sync:sent":
124
+ return `sync sent ${event.batchSize}`;
125
+ case "sync:received":
126
+ return `sync received ${event.batchSize}`;
127
+ case "sync:acknowledged":
128
+ return `sync ack ${event.sequenceNumber}`;
129
+ case "query:subscribed":
130
+ return `query subscribed ${event.collection}`;
131
+ case "query:invalidated":
132
+ return `query invalidated ${event.queryId}`;
133
+ case "query:executed":
134
+ return `query executed ${event.queryId}`;
135
+ case "connection:quality":
136
+ return `connection ${event.quality}`;
137
+ }
138
+ }
139
+ function timelineColor(type) {
140
+ if (type.startsWith("operation:")) return "#22c55e";
141
+ if (type.startsWith("sync:")) return "#a855f7";
142
+ if (type.startsWith("merge:") || type.startsWith("constraint:")) return "#f59e0b";
143
+ if (type.startsWith("query:")) return "#0ea5e9";
144
+ return "#64748b";
145
+ }
146
+ function extractCausalDependencies(event) {
147
+ const operation = extractOperation(event);
148
+ return operation?.causalDeps ?? [];
149
+ }
150
+ function extractOperation(event) {
151
+ switch (event.type) {
152
+ case "operation:created":
153
+ case "operation:applied":
154
+ return event.operation;
155
+ case "query:invalidated":
156
+ return event.trigger;
157
+ default:
158
+ return null;
159
+ }
160
+ }
161
+
162
+ // src/ui/panel.ts
163
+ function renderDevtoolsPanel(target, events2) {
164
+ const model = buildPanelModel(events2);
165
+ target.innerHTML = [
166
+ '<section data-panel="timeline"><h2>Sync Timeline</h2>',
167
+ `<p>Total events: ${model.timeline.length}</p>`,
168
+ "<ul>",
169
+ ...model.timeline.slice(-20).map(
170
+ (item) => `<li><span style="color:${item.color}">${item.type}</span> \xB7 ${escapeHtml(item.label)}</li>`
171
+ ),
172
+ "</ul></section>",
173
+ '<section data-panel="conflicts"><h2>Conflict Inspector</h2>',
174
+ `<p>Conflicts: ${model.conflicts.length}</p>`,
175
+ "<ul>",
176
+ ...model.conflicts.slice(-20).map(
177
+ (item) => `<li>${escapeHtml(item.collection)}.${escapeHtml(item.field)} \xB7 ${escapeHtml(item.strategy)} \xB7 tier ${item.tier}</li>`
178
+ ),
179
+ "</ul></section>",
180
+ '<section data-panel="operations"><h2>Operation Log</h2>',
181
+ `<p>Operations: ${model.operations.length}</p>`,
182
+ "<ul>",
183
+ ...model.operations.slice(-20).map(
184
+ (item) => `<li>${escapeHtml(item.opType)} ${escapeHtml(item.collection)}/${escapeHtml(item.recordId)} (${escapeHtml(item.operationId)})</li>`
185
+ ),
186
+ "</ul></section>",
187
+ '<section data-panel="network"><h2>Network Status</h2>',
188
+ `<p>Connected: ${model.network.connected ? "yes" : "no"}</p>`,
189
+ `<p>Pending ACKs: ${model.network.pendingAcks}</p>`,
190
+ `<p>Sent ops: ${model.network.sentOps}</p>`,
191
+ `<p>Received ops: ${model.network.receivedOps}</p>`,
192
+ "</section>"
193
+ ].join("");
194
+ }
195
+ function escapeHtml(value) {
196
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
197
+ }
4
198
 
5
199
  // src/extension/panel.ts
6
200
  var extensionRoot = document.getElementById("kora-devtools-root");
7
201
  if (!extensionRoot) {
8
202
  throw new Error("Missing #kora-devtools-root element");
9
203
  }
10
- var runtime = globalThis.chrome?.runtime;
11
- var devtools = globalThis.chrome?.devtools;
12
204
  var events = [];
205
+ var runtime = chrome?.runtime;
206
+ var devtools = chrome?.devtools;
13
207
  if (runtime && devtools) {
208
+ const tabId = devtools.inspectedWindow.tabId;
14
209
  const port = runtime.connect({ name: "kora-panel" });
15
- port.postMessage({ type: "panel-init", tabId: devtools.inspectedWindow.tabId });
210
+ port.postMessage({ type: "panel-init", tabId });
16
211
  port.onMessage.addListener((message) => {
17
- const typed = message;
18
- if (!typed || typed.type !== "kora-event" || !typed.payload) return;
19
- events.push(typed.payload);
212
+ if (!message || message.type !== "kora-event" || !message.payload) return;
213
+ events.push(message.payload);
20
214
  renderDevtoolsPanel(extensionRoot, events);
21
215
  });
22
216
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/panel.ts"],"sourcesContent":["import type { TimestampedEvent } from '../types'\nimport { renderDevtoolsPanel } from '../ui/panel'\n\ninterface RuntimePort {\n\tonMessage: {\n\t\taddListener(callback: (message: unknown) => void): void\n\t}\n\tpostMessage(message: unknown): void\n}\n\ninterface RuntimeLike {\n\tconnect(info: { name: string }): RuntimePort\n}\n\ninterface DevtoolsLike {\n\tinspectedWindow: { tabId: number }\n}\n\nconst extensionRoot = document.getElementById('kora-devtools-root')\nif (!extensionRoot) {\n\tthrow new Error('Missing #kora-devtools-root element')\n}\n\nconst runtime = (globalThis as { chrome?: { runtime?: RuntimeLike } }).chrome?.runtime\nconst devtools = (globalThis as { chrome?: { devtools?: DevtoolsLike } }).chrome?.devtools\n\nconst events: TimestampedEvent[] = []\n\nif (runtime && devtools) {\n\tconst port = runtime.connect({ name: 'kora-panel' })\n\tport.postMessage({ type: 'panel-init', tabId: devtools.inspectedWindow.tabId })\n\n\tport.onMessage.addListener((message) => {\n\t\tconst typed = message as { type?: string; payload?: TimestampedEvent } | undefined\n\t\tif (!typed || typed.type !== 'kora-event' || !typed.payload) return\n\n\t\tevents.push(typed.payload)\n\t\trenderDevtoolsPanel(extensionRoot, events)\n\t})\n}\n\nrenderDevtoolsPanel(extensionRoot, events)\n"],"mappings":";;;;;AAkBA,IAAM,gBAAgB,SAAS,eAAe,oBAAoB;AAClE,IAAI,CAAC,eAAe;AACnB,QAAM,IAAI,MAAM,qCAAqC;AACtD;AAEA,IAAM,UAAW,WAAsD,QAAQ;AAC/E,IAAM,WAAY,WAAwD,QAAQ;AAElF,IAAM,SAA6B,CAAC;AAEpC,IAAI,WAAW,UAAU;AACxB,QAAM,OAAO,QAAQ,QAAQ,EAAE,MAAM,aAAa,CAAC;AACnD,OAAK,YAAY,EAAE,MAAM,cAAc,OAAO,SAAS,gBAAgB,MAAM,CAAC;AAE9E,OAAK,UAAU,YAAY,CAAC,YAAY;AACvC,UAAM,QAAQ;AACd,QAAI,CAAC,SAAS,MAAM,SAAS,gBAAgB,CAAC,MAAM,QAAS;AAE7D,WAAO,KAAK,MAAM,OAAO;AACzB,wBAAoB,eAAe,MAAM;AAAA,EAC1C,CAAC;AACF;AAEA,oBAAoB,eAAe,MAAM;","names":[]}
1
+ {"version":3,"sources":["../../src/ui/panel-state.ts","../../src/ui/panel.ts","../../src/extension/panel.ts"],"sourcesContent":["import type { KoraEvent, Operation } from '@korajs/core'\nimport type { TimestampedEvent } from '../types'\n\nexport interface TimelineItem {\n\tid: number\n\ttype: KoraEvent['type']\n\tlabel: string\n\tcolor: string\n\treceivedAt: number\n\tdependsOn: string[]\n}\n\nexport interface ConflictItem {\n\tid: number\n\ttimestamp: number\n\tcollection: string\n\tfield: string\n\tstrategy: string\n\ttier: 1 | 2 | 3\n\tinputA: unknown\n\tinputB: unknown\n\toutput: unknown\n\tconstraintViolated: string | null\n}\n\nexport interface OperationItem {\n\tid: number\n\ttimestamp: number\n\toperationId: string\n\tcollection: string\n\trecordId: string\n\topType: Operation['type']\n\tdata: Record<string, unknown> | null\n\tcausalDeps: string[]\n\tnodeId: string\n\tsequenceNumber: number\n}\n\nexport interface NetworkStatusModel {\n\tconnected: boolean\n\tquality: string | null\n\tpendingAcks: number\n\tlastSyncAt: number | null\n\tsentOps: number\n\treceivedOps: number\n\tversionVector: Array<{ nodeId: string; sequenceNumber: number }>\n}\n\nexport interface DevtoolsPanelModel {\n\ttimeline: TimelineItem[]\n\tconflicts: ConflictItem[]\n\toperations: OperationItem[]\n\tnetwork: NetworkStatusModel\n}\n\nexport function buildPanelModel(events: readonly TimestampedEvent[]): DevtoolsPanelModel {\n\tconst timeline = events.map((entry) => ({\n\t\tid: entry.id,\n\t\ttype: entry.event.type,\n\t\tlabel: timelineLabel(entry.event),\n\t\tcolor: timelineColor(entry.event.type),\n\t\treceivedAt: entry.receivedAt,\n\t\tdependsOn: extractCausalDependencies(entry.event),\n\t}))\n\n\tconst conflicts = events\n\t\t.flatMap((entry) => {\n\t\t\tif (entry.event.type !== 'merge:completed' && entry.event.type !== 'merge:conflict') {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\tconst trace = entry.event.trace\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tid: entry.id,\n\t\t\t\t\ttimestamp: entry.receivedAt,\n\t\t\t\t\tcollection: trace.operationA.collection,\n\t\t\t\t\tfield: trace.field,\n\t\t\t\t\tstrategy: trace.strategy,\n\t\t\t\t\ttier: trace.tier,\n\t\t\t\t\tinputA: trace.inputA,\n\t\t\t\t\tinputB: trace.inputB,\n\t\t\t\t\toutput: trace.output,\n\t\t\t\t\tconstraintViolated: trace.constraintViolated,\n\t\t\t\t},\n\t\t\t]\n\t\t})\n\n\tconst operations = events\n\t\t.map((entry) => {\n\t\t\tconst operation = extractOperation(entry.event)\n\t\t\tif (!operation) return null\n\n\t\t\treturn {\n\t\t\t\tid: entry.id,\n\t\t\t\ttimestamp: entry.receivedAt,\n\t\t\t\toperationId: operation.id,\n\t\t\t\tcollection: operation.collection,\n\t\t\t\trecordId: operation.recordId,\n\t\t\t\topType: operation.type,\n\t\t\t\tdata: operation.data,\n\t\t\t\tcausalDeps: operation.causalDeps,\n\t\t\t\tnodeId: operation.nodeId,\n\t\t\t\tsequenceNumber: operation.sequenceNumber,\n\t\t\t}\n\t\t})\n\t\t.filter((item): item is OperationItem => item !== null)\n\n\tconst network = buildNetworkStatus(events, operations)\n\n\treturn {\n\t\ttimeline,\n\t\tconflicts,\n\t\toperations,\n\t\tnetwork,\n\t}\n}\n\nfunction buildNetworkStatus(\n\tevents: readonly TimestampedEvent[],\n\toperations: readonly OperationItem[],\n): NetworkStatusModel {\n\tlet connected = false\n\tlet quality: string | null = null\n\tlet pendingAcks = 0\n\tlet lastSyncAt: number | null = null\n\tlet sentOps = 0\n\tlet receivedOps = 0\n\n\tfor (const entry of events) {\n\t\tswitch (entry.event.type) {\n\t\t\tcase 'sync:connected':\n\t\t\t\tconnected = true\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:disconnected':\n\t\t\t\tconnected = false\n\t\t\t\tbreak\n\t\t\tcase 'connection:quality':\n\t\t\t\tquality = entry.event.quality\n\t\t\t\tbreak\n\t\t\tcase 'sync:sent':\n\t\t\t\tsentOps += entry.event.operations.length\n\t\t\t\tpendingAcks += entry.event.operations.length\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:received':\n\t\t\t\treceivedOps += entry.event.operations.length\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t\tcase 'sync:acknowledged':\n\t\t\t\tpendingAcks = Math.max(0, pendingAcks - 1)\n\t\t\t\tlastSyncAt = entry.receivedAt\n\t\t\t\tbreak\n\t\t}\n\t}\n\n\tconst vector = new Map<string, number>()\n\tfor (const operation of operations) {\n\t\tconst current = vector.get(operation.nodeId) ?? 0\n\t\tif (operation.sequenceNumber > current) {\n\t\t\tvector.set(operation.nodeId, operation.sequenceNumber)\n\t\t}\n\t}\n\n\treturn {\n\t\tconnected,\n\t\tquality,\n\t\tpendingAcks,\n\t\tlastSyncAt,\n\t\tsentOps,\n\t\treceivedOps,\n\t\tversionVector: [...vector.entries()]\n\t\t\t.map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber }))\n\t\t\t.sort((left, right) => left.nodeId.localeCompare(right.nodeId)),\n\t}\n}\n\nfunction timelineLabel(event: KoraEvent): string {\n\tswitch (event.type) {\n\t\tcase 'operation:created':\n\t\tcase 'operation:applied':\n\t\t\treturn `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`\n\t\tcase 'merge:started':\n\t\t\treturn `merge start ${event.operationA.collection}`\n\t\tcase 'merge:completed':\n\t\t\treturn `merge complete ${event.trace.field}`\n\t\tcase 'merge:conflict':\n\t\t\treturn `merge conflict ${event.trace.field}`\n\t\tcase 'constraint:violated':\n\t\t\treturn `constraint ${event.constraint}`\n\t\tcase 'sync:connected':\n\t\t\treturn `sync connected ${event.nodeId}`\n\t\tcase 'sync:disconnected':\n\t\t\treturn `sync disconnected`\n\t\tcase 'sync:sent':\n\t\t\treturn `sync sent ${event.batchSize}`\n\t\tcase 'sync:received':\n\t\t\treturn `sync received ${event.batchSize}`\n\t\tcase 'sync:acknowledged':\n\t\t\treturn `sync ack ${event.sequenceNumber}`\n\t\tcase 'query:subscribed':\n\t\t\treturn `query subscribed ${event.collection}`\n\t\tcase 'query:invalidated':\n\t\t\treturn `query invalidated ${event.queryId}`\n\t\tcase 'query:executed':\n\t\t\treturn `query executed ${event.queryId}`\n\t\tcase 'connection:quality':\n\t\t\treturn `connection ${event.quality}`\n\t}\n}\n\nfunction timelineColor(type: KoraEvent['type']): string {\n\tif (type.startsWith('operation:')) return '#22c55e'\n\tif (type.startsWith('sync:')) return '#a855f7'\n\tif (type.startsWith('merge:') || type.startsWith('constraint:')) return '#f59e0b'\n\tif (type.startsWith('query:')) return '#0ea5e9'\n\treturn '#64748b'\n}\n\nfunction extractCausalDependencies(event: KoraEvent): string[] {\n\tconst operation = extractOperation(event)\n\treturn operation?.causalDeps ?? []\n}\n\nfunction extractOperation(event: KoraEvent): Operation | null {\n\tswitch (event.type) {\n\t\tcase 'operation:created':\n\t\tcase 'operation:applied':\n\t\t\treturn event.operation\n\t\tcase 'query:invalidated':\n\t\t\treturn event.trigger\n\t\tdefault:\n\t\t\treturn null\n\t}\n}\n","import type { TimestampedEvent } from '../types'\nimport { buildPanelModel } from './panel-state'\n\nexport function renderDevtoolsPanel(target: HTMLElement, events: readonly TimestampedEvent[]): void {\n\tconst model = buildPanelModel(events)\n\n\ttarget.innerHTML = [\n\t\t'<section data-panel=\"timeline\"><h2>Sync Timeline</h2>',\n\t\t`<p>Total events: ${model.timeline.length}</p>`,\n\t\t'<ul>',\n\t\t...model.timeline.slice(-20).map(\n\t\t\t(item) =>\n\t\t\t\t`<li><span style=\"color:${item.color}\">${item.type}</span> · ${escapeHtml(item.label)}</li>`,\n\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"conflicts\"><h2>Conflict Inspector</h2>',\n\t\t`<p>Conflicts: ${model.conflicts.length}</p>`,\n\t\t'<ul>',\n\t\t...model.conflicts\n\t\t\t.slice(-20)\n\t\t\t.map(\n\t\t\t\t(item) =>\n\t\t\t\t\t`<li>${escapeHtml(item.collection)}.${escapeHtml(item.field)} · ${escapeHtml(item.strategy)} · tier ${item.tier}</li>`,\n\t\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"operations\"><h2>Operation Log</h2>',\n\t\t`<p>Operations: ${model.operations.length}</p>`,\n\t\t'<ul>',\n\t\t...model.operations.slice(-20).map(\n\t\t\t(item) =>\n\t\t\t\t`<li>${escapeHtml(item.opType)} ${escapeHtml(item.collection)}/${escapeHtml(item.recordId)} (${escapeHtml(item.operationId)})</li>`,\n\t\t),\n\t\t'</ul></section>',\n\t\t'<section data-panel=\"network\"><h2>Network Status</h2>',\n\t\t`<p>Connected: ${model.network.connected ? 'yes' : 'no'}</p>`,\n\t\t`<p>Pending ACKs: ${model.network.pendingAcks}</p>`,\n\t\t`<p>Sent ops: ${model.network.sentOps}</p>`,\n\t\t`<p>Received ops: ${model.network.receivedOps}</p>`,\n\t\t'</section>',\n\t].join('')\n}\n\nfunction escapeHtml(value: string): string {\n\treturn value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')\n}\n","import type { TimestampedEvent } from '../types'\nimport { renderDevtoolsPanel } from '../ui/panel'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ndeclare const chrome: any\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nconst extensionRoot = document.getElementById('kora-devtools-root')\nif (!extensionRoot) {\n\tthrow new Error('Missing #kora-devtools-root element')\n}\n\nconst events: TimestampedEvent[] = []\n\nconst runtime = chrome?.runtime\nconst devtools = chrome?.devtools\n\nif (runtime && devtools) {\n\tconst tabId = devtools.inspectedWindow.tabId\n\tconst port = runtime.connect({ name: 'kora-panel' })\n\tport.postMessage({ type: 'panel-init', tabId })\n\n\tport.onMessage.addListener((message: { type?: string; payload?: TimestampedEvent }) => {\n\t\tif (!message || message.type !== 'kora-event' || !message.payload) return\n\n\t\tevents.push(message.payload)\n\t\trenderDevtoolsPanel(extensionRoot, events)\n\t})\n}\n\nrenderDevtoolsPanel(extensionRoot, events)\n"],"mappings":"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","names":["events","events"]}