@korajs/devtools 0.1.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.
Files changed (37) hide show
  1. package/dist/chunk-4ZQ2RTZM.js +203 -0
  2. package/dist/chunk-4ZQ2RTZM.js.map +1 -0
  3. package/dist/chunk-JH2X4T4Z.js +58 -0
  4. package/dist/chunk-JH2X4T4Z.js.map +1 -0
  5. package/dist/extension/background.cjs +65 -0
  6. package/dist/extension/background.cjs.map +1 -0
  7. package/dist/extension/background.d.cts +2 -0
  8. package/dist/extension/background.d.ts +2 -0
  9. package/dist/extension/background.js +13 -0
  10. package/dist/extension/background.js.map +1 -0
  11. package/dist/extension/content-script.cjs +18 -0
  12. package/dist/extension/content-script.cjs.map +1 -0
  13. package/dist/extension/content-script.d.cts +2 -0
  14. package/dist/extension/content-script.d.ts +2 -0
  15. package/dist/extension/content-script.js +16 -0
  16. package/dist/extension/content-script.js.map +1 -0
  17. package/dist/extension/devtools-page.html +45 -0
  18. package/dist/extension/devtools.cjs +9 -0
  19. package/dist/extension/devtools.cjs.map +1 -0
  20. package/dist/extension/devtools.d.cts +2 -0
  21. package/dist/extension/devtools.d.ts +2 -0
  22. package/dist/extension/devtools.js +7 -0
  23. package/dist/extension/devtools.js.map +1 -0
  24. package/dist/extension/manifest.json +20 -0
  25. package/dist/extension/panel.cjs +220 -0
  26. package/dist/extension/panel.cjs.map +1 -0
  27. package/dist/extension/panel.d.cts +2 -0
  28. package/dist/extension/panel.d.ts +2 -0
  29. package/dist/extension/panel.js +24 -0
  30. package/dist/extension/panel.js.map +1 -0
  31. package/dist/index.cjs +662 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +260 -0
  34. package/dist/index.d.ts +260 -0
  35. package/dist/index.js +383 -0
  36. package/dist/index.js.map +1 -0
  37. package/package.json +42 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,662 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ EventBuffer: () => EventBuffer,
24
+ Instrumenter: () => Instrumenter,
25
+ MessageBridge: () => MessageBridge,
26
+ PortRouter: () => PortRouter,
27
+ buildPanelModel: () => buildPanelModel,
28
+ computeStatistics: () => computeStatistics,
29
+ filterEvents: () => filterEvents,
30
+ getEventCategory: () => getEventCategory,
31
+ renderDevtoolsPanel: () => renderDevtoolsPanel
32
+ });
33
+ module.exports = __toCommonJS(src_exports);
34
+
35
+ // src/bridge/message-bridge.ts
36
+ var DEFAULT_CHANNEL = "kora-devtools";
37
+ var MessageBridge = class {
38
+ channelName;
39
+ listeners = /* @__PURE__ */ new Set();
40
+ messageHandler;
41
+ destroyed = false;
42
+ constructor(channelName = DEFAULT_CHANNEL) {
43
+ this.channelName = channelName;
44
+ if (typeof window === "undefined") {
45
+ this.messageHandler = null;
46
+ return;
47
+ }
48
+ this.messageHandler = (event) => {
49
+ if (this.destroyed) return;
50
+ const data = event.data;
51
+ if (!data || data.source !== this.channelName) return;
52
+ for (const listener of this.listeners) {
53
+ listener(data.payload);
54
+ }
55
+ };
56
+ window.addEventListener("message", this.messageHandler);
57
+ }
58
+ /**
59
+ * Post a timestamped event through the bridge.
60
+ * No-op if window is not available or the bridge has been destroyed.
61
+ */
62
+ send(event) {
63
+ if (this.destroyed || typeof window === "undefined") return;
64
+ const message = {
65
+ source: this.channelName,
66
+ payload: event
67
+ };
68
+ window.postMessage(message, "*");
69
+ }
70
+ /**
71
+ * Register a callback for events received through the bridge.
72
+ * Returns an unsubscribe function.
73
+ */
74
+ onReceive(callback) {
75
+ if (this.destroyed) return () => {
76
+ };
77
+ this.listeners.add(callback);
78
+ return () => {
79
+ this.listeners.delete(callback);
80
+ };
81
+ }
82
+ /**
83
+ * Remove all listeners and detach from window.
84
+ * After calling destroy, all operations become no-ops.
85
+ */
86
+ destroy() {
87
+ if (this.destroyed) return;
88
+ this.destroyed = true;
89
+ this.listeners.clear();
90
+ if (this.messageHandler && typeof window !== "undefined") {
91
+ window.removeEventListener("message", this.messageHandler);
92
+ }
93
+ }
94
+ };
95
+
96
+ // src/buffer/event-buffer.ts
97
+ var DEFAULT_CAPACITY = 1e4;
98
+ var EventBuffer = class {
99
+ _capacity;
100
+ buffer;
101
+ /** Index where the next event will be written */
102
+ head = 0;
103
+ /** Total number of events ever pushed (used to compute readable range) */
104
+ _totalPushed = 0;
105
+ constructor(capacity = DEFAULT_CAPACITY) {
106
+ if (capacity < 1) {
107
+ throw new Error("EventBuffer capacity must be at least 1");
108
+ }
109
+ this._capacity = capacity;
110
+ this.buffer = new Array(capacity);
111
+ }
112
+ /** Maximum number of events the buffer can hold */
113
+ get capacity() {
114
+ return this._capacity;
115
+ }
116
+ /** Current number of events in the buffer */
117
+ get size() {
118
+ return Math.min(this._totalPushed, this._capacity);
119
+ }
120
+ /**
121
+ * Append an event to the buffer.
122
+ * If the buffer is at capacity, the oldest event is evicted.
123
+ */
124
+ push(event) {
125
+ this.buffer[this.head] = event;
126
+ this.head = (this.head + 1) % this._capacity;
127
+ this._totalPushed++;
128
+ }
129
+ /**
130
+ * Returns all events in insertion order (oldest first).
131
+ */
132
+ getAll() {
133
+ if (this._totalPushed === 0) return [];
134
+ const result = [];
135
+ if (this._totalPushed <= this._capacity) {
136
+ for (let i = 0; i < this.head; i++) {
137
+ const event = this.buffer[i];
138
+ if (event) result.push(event);
139
+ }
140
+ } else {
141
+ for (let i = 0; i < this._capacity; i++) {
142
+ const index = (this.head + i) % this._capacity;
143
+ const event = this.buffer[index];
144
+ if (event) result.push(event);
145
+ }
146
+ }
147
+ return result;
148
+ }
149
+ /**
150
+ * Returns events whose sequential IDs fall within [start, end] (inclusive).
151
+ */
152
+ getRange(start, end) {
153
+ return this.getAll().filter((e) => e.id >= start && e.id <= end);
154
+ }
155
+ /**
156
+ * Returns events matching a specific KoraEventType.
157
+ */
158
+ getByType(type) {
159
+ return this.getAll().filter((e) => e.event.type === type);
160
+ }
161
+ /** Remove all events from the buffer */
162
+ clear() {
163
+ this.buffer.fill(void 0);
164
+ this.head = 0;
165
+ this._totalPushed = 0;
166
+ }
167
+ };
168
+
169
+ // src/instrumenter/instrumenter.ts
170
+ var DEFAULT_BUFFER_SIZE = 1e4;
171
+ var DEFAULT_CHANNEL_NAME = "kora-devtools";
172
+ var ALL_EVENT_TYPES = [
173
+ "operation:created",
174
+ "operation:applied",
175
+ "merge:started",
176
+ "merge:completed",
177
+ "merge:conflict",
178
+ "constraint:violated",
179
+ "sync:connected",
180
+ "sync:disconnected",
181
+ "sync:sent",
182
+ "sync:received",
183
+ "sync:acknowledged",
184
+ "query:subscribed",
185
+ "query:invalidated",
186
+ "query:executed",
187
+ "connection:quality"
188
+ ];
189
+ var Instrumenter = class {
190
+ constructor(emitter, config) {
191
+ this.emitter = emitter;
192
+ const bufferSize = config?.bufferSize ?? DEFAULT_BUFFER_SIZE;
193
+ const bridgeEnabled = config?.bridgeEnabled ?? true;
194
+ const channelName = config?.channelName ?? DEFAULT_CHANNEL_NAME;
195
+ this.buffer = new EventBuffer(bufferSize);
196
+ this.bridge = bridgeEnabled ? new MessageBridge(channelName) : null;
197
+ this.attachListeners();
198
+ }
199
+ emitter;
200
+ buffer;
201
+ bridge;
202
+ unsubscribers = [];
203
+ nextId = 1;
204
+ paused = false;
205
+ destroyed = false;
206
+ /** Access the underlying event buffer */
207
+ getBuffer() {
208
+ return this.buffer;
209
+ }
210
+ /** Access the message bridge, or null if bridge is disabled */
211
+ getBridge() {
212
+ return this.bridge;
213
+ }
214
+ /** Temporarily stop recording events. Events emitted while paused are dropped. */
215
+ pause() {
216
+ this.paused = true;
217
+ }
218
+ /** Resume recording events after a pause. */
219
+ resume() {
220
+ this.paused = false;
221
+ }
222
+ /** Whether the instrumenter is currently paused */
223
+ isPaused() {
224
+ return this.paused;
225
+ }
226
+ /**
227
+ * Detach all listeners from the emitter and destroy the bridge.
228
+ * After calling destroy, the instrumenter is inert.
229
+ */
230
+ destroy() {
231
+ if (this.destroyed) return;
232
+ this.destroyed = true;
233
+ for (const unsub of this.unsubscribers) {
234
+ unsub();
235
+ }
236
+ this.unsubscribers.length = 0;
237
+ this.bridge?.destroy();
238
+ }
239
+ attachListeners() {
240
+ for (const eventType of ALL_EVENT_TYPES) {
241
+ const unsub = this.emitter.on(eventType, (event) => {
242
+ this.handleEvent(event);
243
+ });
244
+ this.unsubscribers.push(unsub);
245
+ }
246
+ }
247
+ handleEvent(event) {
248
+ if (this.paused || this.destroyed) return;
249
+ const timestamped = {
250
+ id: this.nextId++,
251
+ event,
252
+ receivedAt: Date.now()
253
+ };
254
+ this.buffer.push(timestamped);
255
+ this.bridge?.send(timestamped);
256
+ }
257
+ };
258
+
259
+ // src/types.ts
260
+ var EVENT_TYPE_CATEGORIES = {
261
+ "operation:created": "operation",
262
+ "operation:applied": "operation",
263
+ "merge:started": "merge",
264
+ "merge:completed": "merge",
265
+ "merge:conflict": "merge",
266
+ "constraint:violated": "merge",
267
+ "sync:connected": "sync",
268
+ "sync:disconnected": "sync",
269
+ "sync:sent": "sync",
270
+ "sync:received": "sync",
271
+ "sync:acknowledged": "sync",
272
+ "query:subscribed": "query",
273
+ "query:invalidated": "query",
274
+ "query:executed": "query",
275
+ "connection:quality": "connection"
276
+ };
277
+ function eventTypeToCategory(type) {
278
+ return EVENT_TYPE_CATEGORIES[type];
279
+ }
280
+
281
+ // src/filter/event-filter.ts
282
+ function getEventCategory(type) {
283
+ return eventTypeToCategory(type);
284
+ }
285
+ function extractCollection(event) {
286
+ const e = event.event;
287
+ switch (e.type) {
288
+ case "operation:created":
289
+ case "operation:applied":
290
+ return e.operation.collection;
291
+ case "merge:started":
292
+ return e.operationA.collection;
293
+ case "merge:completed":
294
+ case "merge:conflict":
295
+ return e.trace.operationA.collection;
296
+ case "constraint:violated":
297
+ return e.trace.operationA.collection;
298
+ case "query:subscribed":
299
+ return e.collection;
300
+ case "query:invalidated":
301
+ return e.trigger.collection;
302
+ case "sync:sent":
303
+ return e.operations[0]?.collection ?? null;
304
+ case "sync:received":
305
+ return e.operations[0]?.collection ?? null;
306
+ default:
307
+ return null;
308
+ }
309
+ }
310
+ function filterEvents(events, criteria) {
311
+ const { categories, types, timeRange, collection } = criteria;
312
+ if (!categories && !types && !timeRange && !collection) {
313
+ return events;
314
+ }
315
+ const categorySet = categories ? new Set(categories) : null;
316
+ const typeSet = types ? new Set(types) : null;
317
+ return events.filter((event) => {
318
+ if (categorySet) {
319
+ const cat = eventTypeToCategory(event.event.type);
320
+ if (!categorySet.has(cat)) return false;
321
+ }
322
+ if (typeSet) {
323
+ if (!typeSet.has(event.event.type)) return false;
324
+ }
325
+ if (timeRange) {
326
+ if (event.receivedAt < timeRange.start || event.receivedAt > timeRange.end) {
327
+ return false;
328
+ }
329
+ }
330
+ if (collection) {
331
+ const eventCollection = extractCollection(event);
332
+ if (eventCollection !== collection) return false;
333
+ }
334
+ return true;
335
+ });
336
+ }
337
+
338
+ // src/stats/event-stats.ts
339
+ function computeStatistics(events) {
340
+ const eventsByCategory = {
341
+ operation: 0,
342
+ merge: 0,
343
+ sync: 0,
344
+ query: 0,
345
+ connection: 0
346
+ };
347
+ const eventsByType = {};
348
+ let mergeConflicts = 0;
349
+ let constraintViolations = 0;
350
+ let mergeDurationSum = 0;
351
+ let mergeDurationCount = 0;
352
+ let queryDurationSum = 0;
353
+ let queryDurationCount = 0;
354
+ let syncOperationsSent = 0;
355
+ let syncOperationsReceived = 0;
356
+ for (const timestamped of events) {
357
+ const e = timestamped.event;
358
+ const category = eventTypeToCategory(e.type);
359
+ eventsByCategory[category]++;
360
+ eventsByType[e.type] = (eventsByType[e.type] ?? 0) + 1;
361
+ switch (e.type) {
362
+ case "merge:completed":
363
+ mergeDurationSum += e.trace.duration;
364
+ mergeDurationCount++;
365
+ break;
366
+ case "merge:conflict":
367
+ mergeConflicts++;
368
+ mergeDurationSum += e.trace.duration;
369
+ mergeDurationCount++;
370
+ break;
371
+ case "constraint:violated":
372
+ constraintViolations++;
373
+ break;
374
+ case "query:executed":
375
+ queryDurationSum += e.duration;
376
+ queryDurationCount++;
377
+ break;
378
+ case "sync:sent":
379
+ syncOperationsSent += e.batchSize;
380
+ break;
381
+ case "sync:received":
382
+ syncOperationsReceived += e.batchSize;
383
+ break;
384
+ }
385
+ }
386
+ return {
387
+ totalEvents: events.length,
388
+ eventsByCategory,
389
+ eventsByType,
390
+ mergeConflicts,
391
+ constraintViolations,
392
+ avgMergeDuration: mergeDurationCount > 0 ? mergeDurationSum / mergeDurationCount : null,
393
+ avgQueryDuration: queryDurationCount > 0 ? queryDurationSum / queryDurationCount : null,
394
+ syncOperationsSent,
395
+ syncOperationsReceived
396
+ };
397
+ }
398
+
399
+ // src/ui/panel-state.ts
400
+ function buildPanelModel(events) {
401
+ const timeline = events.map((entry) => ({
402
+ id: entry.id,
403
+ type: entry.event.type,
404
+ label: timelineLabel(entry.event),
405
+ color: timelineColor(entry.event.type),
406
+ receivedAt: entry.receivedAt,
407
+ dependsOn: extractCausalDependencies(entry.event)
408
+ }));
409
+ const conflicts = events.flatMap((entry) => {
410
+ if (entry.event.type !== "merge:completed" && entry.event.type !== "merge:conflict") {
411
+ return [];
412
+ }
413
+ const trace = entry.event.trace;
414
+ return [
415
+ {
416
+ id: entry.id,
417
+ timestamp: entry.receivedAt,
418
+ collection: trace.operationA.collection,
419
+ field: trace.field,
420
+ strategy: trace.strategy,
421
+ tier: trace.tier,
422
+ inputA: trace.inputA,
423
+ inputB: trace.inputB,
424
+ output: trace.output,
425
+ constraintViolated: trace.constraintViolated
426
+ }
427
+ ];
428
+ });
429
+ const operations = events.map((entry) => {
430
+ const operation = extractOperation(entry.event);
431
+ if (!operation) return null;
432
+ return {
433
+ id: entry.id,
434
+ timestamp: entry.receivedAt,
435
+ operationId: operation.id,
436
+ collection: operation.collection,
437
+ recordId: operation.recordId,
438
+ opType: operation.type,
439
+ data: operation.data,
440
+ causalDeps: operation.causalDeps,
441
+ nodeId: operation.nodeId,
442
+ sequenceNumber: operation.sequenceNumber
443
+ };
444
+ }).filter((item) => item !== null);
445
+ const network = buildNetworkStatus(events, operations);
446
+ return {
447
+ timeline,
448
+ conflicts,
449
+ operations,
450
+ network
451
+ };
452
+ }
453
+ function buildNetworkStatus(events, operations) {
454
+ let connected = false;
455
+ let quality = null;
456
+ let pendingAcks = 0;
457
+ let lastSyncAt = null;
458
+ let sentOps = 0;
459
+ let receivedOps = 0;
460
+ for (const entry of events) {
461
+ switch (entry.event.type) {
462
+ case "sync:connected":
463
+ connected = true;
464
+ lastSyncAt = entry.receivedAt;
465
+ break;
466
+ case "sync:disconnected":
467
+ connected = false;
468
+ break;
469
+ case "connection:quality":
470
+ quality = entry.event.quality;
471
+ break;
472
+ case "sync:sent":
473
+ sentOps += entry.event.operations.length;
474
+ pendingAcks += entry.event.operations.length;
475
+ lastSyncAt = entry.receivedAt;
476
+ break;
477
+ case "sync:received":
478
+ receivedOps += entry.event.operations.length;
479
+ lastSyncAt = entry.receivedAt;
480
+ break;
481
+ case "sync:acknowledged":
482
+ pendingAcks = Math.max(0, pendingAcks - 1);
483
+ lastSyncAt = entry.receivedAt;
484
+ break;
485
+ }
486
+ }
487
+ const vector = /* @__PURE__ */ new Map();
488
+ for (const operation of operations) {
489
+ const current = vector.get(operation.nodeId) ?? 0;
490
+ if (operation.sequenceNumber > current) {
491
+ vector.set(operation.nodeId, operation.sequenceNumber);
492
+ }
493
+ }
494
+ return {
495
+ connected,
496
+ quality,
497
+ pendingAcks,
498
+ lastSyncAt,
499
+ sentOps,
500
+ receivedOps,
501
+ versionVector: [...vector.entries()].map(([nodeId, sequenceNumber]) => ({ nodeId, sequenceNumber })).sort((left, right) => left.nodeId.localeCompare(right.nodeId))
502
+ };
503
+ }
504
+ function timelineLabel(event) {
505
+ switch (event.type) {
506
+ case "operation:created":
507
+ case "operation:applied":
508
+ return `${event.operation.type} ${event.operation.collection}/${event.operation.recordId}`;
509
+ case "merge:started":
510
+ return `merge start ${event.operationA.collection}`;
511
+ case "merge:completed":
512
+ return `merge complete ${event.trace.field}`;
513
+ case "merge:conflict":
514
+ return `merge conflict ${event.trace.field}`;
515
+ case "constraint:violated":
516
+ return `constraint ${event.constraint}`;
517
+ case "sync:connected":
518
+ return `sync connected ${event.nodeId}`;
519
+ case "sync:disconnected":
520
+ return `sync disconnected`;
521
+ case "sync:sent":
522
+ return `sync sent ${event.batchSize}`;
523
+ case "sync:received":
524
+ return `sync received ${event.batchSize}`;
525
+ case "sync:acknowledged":
526
+ return `sync ack ${event.sequenceNumber}`;
527
+ case "query:subscribed":
528
+ return `query subscribed ${event.collection}`;
529
+ case "query:invalidated":
530
+ return `query invalidated ${event.queryId}`;
531
+ case "query:executed":
532
+ return `query executed ${event.queryId}`;
533
+ case "connection:quality":
534
+ return `connection ${event.quality}`;
535
+ }
536
+ }
537
+ function timelineColor(type) {
538
+ if (type.startsWith("operation:")) return "#22c55e";
539
+ if (type.startsWith("sync:")) return "#a855f7";
540
+ if (type.startsWith("merge:") || type.startsWith("constraint:")) return "#f59e0b";
541
+ if (type.startsWith("query:")) return "#0ea5e9";
542
+ return "#64748b";
543
+ }
544
+ function extractCausalDependencies(event) {
545
+ const operation = extractOperation(event);
546
+ return operation?.causalDeps ?? [];
547
+ }
548
+ function extractOperation(event) {
549
+ switch (event.type) {
550
+ case "operation:created":
551
+ case "operation:applied":
552
+ return event.operation;
553
+ case "query:invalidated":
554
+ return event.trigger;
555
+ default:
556
+ return null;
557
+ }
558
+ }
559
+
560
+ // src/ui/panel.ts
561
+ function renderDevtoolsPanel(target, events) {
562
+ const model = buildPanelModel(events);
563
+ target.innerHTML = [
564
+ '<section data-panel="timeline"><h2>Sync Timeline</h2>',
565
+ `<p>Total events: ${model.timeline.length}</p>`,
566
+ "<ul>",
567
+ ...model.timeline.slice(-20).map(
568
+ (item) => `<li><span style="color:${item.color}">${item.type}</span> \xB7 ${escapeHtml(item.label)}</li>`
569
+ ),
570
+ "</ul></section>",
571
+ '<section data-panel="conflicts"><h2>Conflict Inspector</h2>',
572
+ `<p>Conflicts: ${model.conflicts.length}</p>`,
573
+ "<ul>",
574
+ ...model.conflicts.slice(-20).map(
575
+ (item) => `<li>${escapeHtml(item.collection)}.${escapeHtml(item.field)} \xB7 ${escapeHtml(item.strategy)} \xB7 tier ${item.tier}</li>`
576
+ ),
577
+ "</ul></section>",
578
+ '<section data-panel="operations"><h2>Operation Log</h2>',
579
+ `<p>Operations: ${model.operations.length}</p>`,
580
+ "<ul>",
581
+ ...model.operations.slice(-20).map(
582
+ (item) => `<li>${escapeHtml(item.opType)} ${escapeHtml(item.collection)}/${escapeHtml(item.recordId)} (${escapeHtml(item.operationId)})</li>`
583
+ ),
584
+ "</ul></section>",
585
+ '<section data-panel="network"><h2>Network Status</h2>',
586
+ `<p>Connected: ${model.network.connected ? "yes" : "no"}</p>`,
587
+ `<p>Pending ACKs: ${model.network.pendingAcks}</p>`,
588
+ `<p>Sent ops: ${model.network.sentOps}</p>`,
589
+ `<p>Received ops: ${model.network.receivedOps}</p>`,
590
+ "</section>"
591
+ ].join("");
592
+ }
593
+ function escapeHtml(value) {
594
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
595
+ }
596
+
597
+ // src/extension/port-router.ts
598
+ var PortRouter = class {
599
+ panelClients = /* @__PURE__ */ new Map();
600
+ contentClients = /* @__PURE__ */ new Map();
601
+ handleConnection(port) {
602
+ if (port.name === "kora-panel") {
603
+ this.attachPanel(port);
604
+ return;
605
+ }
606
+ if (port.name === "kora-content") {
607
+ this.attachContent(port);
608
+ }
609
+ }
610
+ attachPanel(port) {
611
+ port.onMessage.addListener((message) => {
612
+ if (!isPanelInitMessage(message)) return;
613
+ this.panelClients.set(message.tabId, { tabId: message.tabId, port });
614
+ });
615
+ port.onDisconnect.addListener(() => {
616
+ for (const [tabId, client] of this.panelClients) {
617
+ if (client.port === port) {
618
+ this.panelClients.delete(tabId);
619
+ }
620
+ }
621
+ });
622
+ }
623
+ attachContent(port) {
624
+ const tabId = port.sender?.tab?.id;
625
+ if (typeof tabId !== "number") {
626
+ return;
627
+ }
628
+ this.contentClients.set(tabId, port);
629
+ port.onMessage.addListener((message) => {
630
+ if (!isContentEventMessage(message)) return;
631
+ const panel = this.panelClients.get(tabId);
632
+ if (!panel) return;
633
+ panel.port.postMessage({ type: "kora-event", payload: message.payload });
634
+ });
635
+ port.onDisconnect.addListener(() => {
636
+ this.contentClients.delete(tabId);
637
+ });
638
+ }
639
+ };
640
+ function isPanelInitMessage(value) {
641
+ if (typeof value !== "object" || value === null) return false;
642
+ const record = value;
643
+ return record.type === "panel-init" && typeof record.tabId === "number";
644
+ }
645
+ function isContentEventMessage(value) {
646
+ if (typeof value !== "object" || value === null) return false;
647
+ const record = value;
648
+ return record.type === "kora-event" && "payload" in record;
649
+ }
650
+ // Annotate the CommonJS export names for ESM import in node:
651
+ 0 && (module.exports = {
652
+ EventBuffer,
653
+ Instrumenter,
654
+ MessageBridge,
655
+ PortRouter,
656
+ buildPanelModel,
657
+ computeStatistics,
658
+ filterEvents,
659
+ getEventCategory,
660
+ renderDevtoolsPanel
661
+ });
662
+ //# sourceMappingURL=index.cjs.map