@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.
- package/dist/chunk-4ZQ2RTZM.js +203 -0
- package/dist/chunk-4ZQ2RTZM.js.map +1 -0
- package/dist/chunk-JH2X4T4Z.js +58 -0
- package/dist/chunk-JH2X4T4Z.js.map +1 -0
- package/dist/extension/background.cjs +65 -0
- package/dist/extension/background.cjs.map +1 -0
- package/dist/extension/background.d.cts +2 -0
- package/dist/extension/background.d.ts +2 -0
- package/dist/extension/background.js +13 -0
- package/dist/extension/background.js.map +1 -0
- package/dist/extension/content-script.cjs +18 -0
- package/dist/extension/content-script.cjs.map +1 -0
- package/dist/extension/content-script.d.cts +2 -0
- package/dist/extension/content-script.d.ts +2 -0
- package/dist/extension/content-script.js +16 -0
- package/dist/extension/content-script.js.map +1 -0
- package/dist/extension/devtools-page.html +45 -0
- package/dist/extension/devtools.cjs +9 -0
- package/dist/extension/devtools.cjs.map +1 -0
- package/dist/extension/devtools.d.cts +2 -0
- package/dist/extension/devtools.d.ts +2 -0
- package/dist/extension/devtools.js +7 -0
- package/dist/extension/devtools.js.map +1 -0
- package/dist/extension/manifest.json +20 -0
- package/dist/extension/panel.cjs +220 -0
- package/dist/extension/panel.cjs.map +1 -0
- package/dist/extension/panel.d.cts +2 -0
- package/dist/extension/panel.d.ts +2 -0
- package/dist/extension/panel.js +24 -0
- package/dist/extension/panel.js.map +1 -0
- package/dist/index.cjs +662 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +260 -0
- package/dist/index.d.ts +260 -0
- package/dist/index.js +383 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/bridge/message-bridge.ts","../src/buffer/event-buffer.ts","../src/instrumenter/instrumenter.ts","../src/types.ts","../src/filter/event-filter.ts","../src/stats/event-stats.ts","../src/ui/panel-state.ts","../src/ui/panel.ts","../src/extension/port-router.ts"],"sourcesContent":["// === Core ===\nexport { Instrumenter } from './instrumenter/instrumenter'\nexport { EventBuffer } from './buffer/event-buffer'\nexport { MessageBridge } from './bridge/message-bridge'\n\n// === Filtering & Stats ===\nexport { filterEvents, getEventCategory } from './filter/event-filter'\nexport { computeStatistics } from './stats/event-stats'\n\n// === DevTools UI / Extension ===\nexport { buildPanelModel } from './ui/panel-state'\nexport { renderDevtoolsPanel } from './ui/panel'\nexport { PortRouter } from './extension/port-router'\n\n// === Types ===\nexport type {\n\tDevtoolsConfig,\n\tEventCategory,\n\tEventFilterCriteria,\n\tEventStatistics,\n\tTimestampedEvent,\n} from './types'\n","import type { TimestampedEvent } from '../types'\n\nconst DEFAULT_CHANNEL = 'kora-devtools'\n\n/** Shape of messages posted through the bridge */\ninterface BridgeMessage {\n\tsource: string\n\tpayload: TimestampedEvent\n}\n\n/**\n * Communicates between the page context and a DevTools panel via window.postMessage.\n * All messages are namespaced with a `source` field to avoid collisions with other\n * postMessage consumers on the page.\n *\n * Safe to instantiate in non-browser environments (SSR/Node) — all operations\n * become no-ops when `window` is not available.\n */\nexport class MessageBridge {\n\tprivate readonly channelName: string\n\tprivate readonly listeners: Set<(event: TimestampedEvent) => void> = new Set()\n\tprivate readonly messageHandler: ((event: MessageEvent) => void) | null\n\tprivate destroyed = false\n\n\tconstructor(channelName: string = DEFAULT_CHANNEL) {\n\t\tthis.channelName = channelName\n\n\t\tif (typeof window === 'undefined') {\n\t\t\tthis.messageHandler = null\n\t\t\treturn\n\t\t}\n\n\t\t// Single shared handler that dispatches to all registered callbacks\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tif (this.destroyed) return\n\t\t\tconst data = event.data as Partial<BridgeMessage> | undefined\n\t\t\tif (!data || data.source !== this.channelName) return\n\n\t\t\tfor (const listener of this.listeners) {\n\t\t\t\tlistener(data.payload as TimestampedEvent)\n\t\t\t}\n\t\t}\n\n\t\twindow.addEventListener('message', this.messageHandler)\n\t}\n\n\t/**\n\t * Post a timestamped event through the bridge.\n\t * No-op if window is not available or the bridge has been destroyed.\n\t */\n\tsend(event: TimestampedEvent): void {\n\t\tif (this.destroyed || typeof window === 'undefined') return\n\n\t\tconst message: BridgeMessage = {\n\t\t\tsource: this.channelName,\n\t\t\tpayload: event,\n\t\t}\n\t\twindow.postMessage(message, '*')\n\t}\n\n\t/**\n\t * Register a callback for events received through the bridge.\n\t * Returns an unsubscribe function.\n\t */\n\tonReceive(callback: (event: TimestampedEvent) => void): () => void {\n\t\tif (this.destroyed) return () => {}\n\n\t\tthis.listeners.add(callback)\n\t\treturn () => {\n\t\t\tthis.listeners.delete(callback)\n\t\t}\n\t}\n\n\t/**\n\t * Remove all listeners and detach from window.\n\t * After calling destroy, all operations become no-ops.\n\t */\n\tdestroy(): void {\n\t\tif (this.destroyed) return\n\t\tthis.destroyed = true\n\t\tthis.listeners.clear()\n\n\t\tif (this.messageHandler && typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('message', this.messageHandler)\n\t\t}\n\t}\n}\n","import type { KoraEventType } from '@korajs/core'\nimport type { TimestampedEvent } from '../types'\n\nconst DEFAULT_CAPACITY = 10_000\n\n/**\n * Fixed-capacity ring buffer for storing timestamped events.\n * When the buffer is full, the oldest events are evicted to make room for new ones.\n * This prevents unbounded memory growth when events accumulate fast.\n */\nexport class EventBuffer {\n\tprivate readonly _capacity: number\n\tprivate readonly buffer: Array<TimestampedEvent | undefined>\n\t/** Index where the next event will be written */\n\tprivate head = 0\n\t/** Total number of events ever pushed (used to compute readable range) */\n\tprivate _totalPushed = 0\n\n\tconstructor(capacity: number = DEFAULT_CAPACITY) {\n\t\tif (capacity < 1) {\n\t\t\tthrow new Error('EventBuffer capacity must be at least 1')\n\t\t}\n\t\tthis._capacity = capacity\n\t\tthis.buffer = new Array<TimestampedEvent | undefined>(capacity)\n\t}\n\n\t/** Maximum number of events the buffer can hold */\n\tget capacity(): number {\n\t\treturn this._capacity\n\t}\n\n\t/** Current number of events in the buffer */\n\tget size(): number {\n\t\treturn Math.min(this._totalPushed, this._capacity)\n\t}\n\n\t/**\n\t * Append an event to the buffer.\n\t * If the buffer is at capacity, the oldest event is evicted.\n\t */\n\tpush(event: TimestampedEvent): void {\n\t\tthis.buffer[this.head] = event\n\t\tthis.head = (this.head + 1) % this._capacity\n\t\tthis._totalPushed++\n\t}\n\n\t/**\n\t * Returns all events in insertion order (oldest first).\n\t */\n\tgetAll(): readonly TimestampedEvent[] {\n\t\tif (this._totalPushed === 0) return []\n\n\t\tconst result: TimestampedEvent[] = []\n\n\t\tif (this._totalPushed <= this._capacity) {\n\t\t\t// Buffer hasn't wrapped yet — events are 0..head-1\n\t\t\tfor (let i = 0; i < this.head; i++) {\n\t\t\t\tconst event = this.buffer[i]\n\t\t\t\tif (event) result.push(event)\n\t\t\t}\n\t\t} else {\n\t\t\t// Buffer has wrapped — oldest is at head, newest is at head-1\n\t\t\tfor (let i = 0; i < this._capacity; i++) {\n\t\t\t\tconst index = (this.head + i) % this._capacity\n\t\t\t\tconst event = this.buffer[index]\n\t\t\t\tif (event) result.push(event)\n\t\t\t}\n\t\t}\n\n\t\treturn result\n\t}\n\n\t/**\n\t * Returns events whose sequential IDs fall within [start, end] (inclusive).\n\t */\n\tgetRange(start: number, end: number): readonly TimestampedEvent[] {\n\t\treturn this.getAll().filter((e) => e.id >= start && e.id <= end)\n\t}\n\n\t/**\n\t * Returns events matching a specific KoraEventType.\n\t */\n\tgetByType(type: KoraEventType): readonly TimestampedEvent[] {\n\t\treturn this.getAll().filter((e) => e.event.type === type)\n\t}\n\n\t/** Remove all events from the buffer */\n\tclear(): void {\n\t\tthis.buffer.fill(undefined)\n\t\tthis.head = 0\n\t\tthis._totalPushed = 0\n\t}\n}\n","import type { KoraEvent, KoraEventEmitter, KoraEventType } from '@korajs/core'\nimport { MessageBridge } from '../bridge/message-bridge'\nimport { EventBuffer } from '../buffer/event-buffer'\nimport type { DevtoolsConfig, TimestampedEvent } from '../types'\n\nconst DEFAULT_BUFFER_SIZE = 10_000\nconst DEFAULT_CHANNEL_NAME = 'kora-devtools'\n\n/** All event types the instrumenter subscribes to */\nconst ALL_EVENT_TYPES: readonly KoraEventType[] = [\n\t'operation:created',\n\t'operation:applied',\n\t'merge:started',\n\t'merge:completed',\n\t'merge:conflict',\n\t'constraint:violated',\n\t'sync:connected',\n\t'sync:disconnected',\n\t'sync:sent',\n\t'sync:received',\n\t'sync:acknowledged',\n\t'query:subscribed',\n\t'query:invalidated',\n\t'query:executed',\n\t'connection:quality',\n] as const\n\n/**\n * Core orchestrator for Kora DevTools instrumentation.\n *\n * Attaches to a KoraEventEmitter, records all emitted events into a ring buffer\n * with sequential IDs and reception timestamps, and optionally forwards them\n * through a MessageBridge for consumption by a DevTools panel.\n *\n * @example\n * ```typescript\n * const instrumenter = new Instrumenter(app.emitter, { bufferSize: 5000 })\n * const buffer = instrumenter.getBuffer()\n * // ... later\n * instrumenter.destroy()\n * ```\n */\nexport class Instrumenter {\n\tprivate readonly buffer: EventBuffer\n\tprivate readonly bridge: MessageBridge | null\n\tprivate readonly unsubscribers: Array<() => void> = []\n\tprivate nextId = 1\n\tprivate paused = false\n\tprivate destroyed = false\n\n\tconstructor(\n\t\tprivate readonly emitter: KoraEventEmitter,\n\t\tconfig?: DevtoolsConfig,\n\t) {\n\t\tconst bufferSize = config?.bufferSize ?? DEFAULT_BUFFER_SIZE\n\t\tconst bridgeEnabled = config?.bridgeEnabled ?? true\n\t\tconst channelName = config?.channelName ?? DEFAULT_CHANNEL_NAME\n\n\t\tthis.buffer = new EventBuffer(bufferSize)\n\t\tthis.bridge = bridgeEnabled ? new MessageBridge(channelName) : null\n\n\t\tthis.attachListeners()\n\t}\n\n\t/** Access the underlying event buffer */\n\tgetBuffer(): EventBuffer {\n\t\treturn this.buffer\n\t}\n\n\t/** Access the message bridge, or null if bridge is disabled */\n\tgetBridge(): MessageBridge | null {\n\t\treturn this.bridge\n\t}\n\n\t/** Temporarily stop recording events. Events emitted while paused are dropped. */\n\tpause(): void {\n\t\tthis.paused = true\n\t}\n\n\t/** Resume recording events after a pause. */\n\tresume(): void {\n\t\tthis.paused = false\n\t}\n\n\t/** Whether the instrumenter is currently paused */\n\tisPaused(): boolean {\n\t\treturn this.paused\n\t}\n\n\t/**\n\t * Detach all listeners from the emitter and destroy the bridge.\n\t * After calling destroy, the instrumenter is inert.\n\t */\n\tdestroy(): void {\n\t\tif (this.destroyed) return\n\t\tthis.destroyed = true\n\n\t\tfor (const unsub of this.unsubscribers) {\n\t\t\tunsub()\n\t\t}\n\t\tthis.unsubscribers.length = 0\n\n\t\tthis.bridge?.destroy()\n\t}\n\n\tprivate attachListeners(): void {\n\t\tfor (const eventType of ALL_EVENT_TYPES) {\n\t\t\tconst unsub = this.emitter.on(eventType, (event: KoraEvent) => {\n\t\t\t\tthis.handleEvent(event)\n\t\t\t})\n\t\t\tthis.unsubscribers.push(unsub)\n\t\t}\n\t}\n\n\tprivate handleEvent(event: KoraEvent): void {\n\t\tif (this.paused || this.destroyed) return\n\n\t\tconst timestamped: TimestampedEvent = {\n\t\t\tid: this.nextId++,\n\t\t\tevent,\n\t\t\treceivedAt: Date.now(),\n\t\t}\n\n\t\tthis.buffer.push(timestamped)\n\t\tthis.bridge?.send(timestamped)\n\t}\n}\n","import type { KoraEvent, KoraEventType } from '@korajs/core'\n\n/** A KoraEvent wrapped with a reception timestamp and sequential ID */\nexport interface TimestampedEvent {\n\t/** Auto-incrementing sequential ID */\n\tid: number\n\t/** The original framework event */\n\tevent: KoraEvent\n\t/** Date.now() when the instrumenter captured the event */\n\treceivedAt: number\n}\n\n/** Event categories for filtering and grouping */\nexport type EventCategory = 'operation' | 'merge' | 'sync' | 'query' | 'connection'\n\n/** Maps each KoraEventType to its category */\nconst EVENT_TYPE_CATEGORIES: Record<KoraEventType, EventCategory> = {\n\t'operation:created': 'operation',\n\t'operation:applied': 'operation',\n\t'merge:started': 'merge',\n\t'merge:completed': 'merge',\n\t'merge:conflict': 'merge',\n\t'constraint:violated': 'merge',\n\t'sync:connected': 'sync',\n\t'sync:disconnected': 'sync',\n\t'sync:sent': 'sync',\n\t'sync:received': 'sync',\n\t'sync:acknowledged': 'sync',\n\t'query:subscribed': 'query',\n\t'query:invalidated': 'query',\n\t'query:executed': 'query',\n\t'connection:quality': 'connection',\n}\n\n/** Look up the category for a given event type */\nexport function eventTypeToCategory(type: KoraEventType): EventCategory {\n\treturn EVENT_TYPE_CATEGORIES[type]\n}\n\n/** All event categories */\nexport const EVENT_CATEGORIES = ['operation', 'merge', 'sync', 'query', 'connection'] as const\n\n/** Configuration for the Instrumenter */\nexport interface DevtoolsConfig {\n\t/** Max events in the ring buffer (default: 10000) */\n\tbufferSize?: number\n\t/** Enable message bridge for DevTools panel communication (default: true) */\n\tbridgeEnabled?: boolean\n\t/** Custom message channel name (default: 'kora-devtools') */\n\tchannelName?: string\n}\n\n/** Filter criteria for querying events */\nexport interface EventFilterCriteria {\n\t/** Filter by event categories */\n\tcategories?: EventCategory[]\n\t/** Filter by specific event types */\n\ttypes?: KoraEventType[]\n\t/** Filter by reception time range */\n\ttimeRange?: { start: number; end: number }\n\t/** Filter by collection name (extracted from operation events) */\n\tcollection?: string\n}\n\n/** Aggregated statistics computed from a set of events */\nexport interface EventStatistics {\n\ttotalEvents: number\n\teventsByCategory: Record<EventCategory, number>\n\teventsByType: Partial<Record<KoraEventType, number>>\n\tmergeConflicts: number\n\tconstraintViolations: number\n\tavgMergeDuration: number | null\n\tavgQueryDuration: number | null\n\tsyncOperationsSent: number\n\tsyncOperationsReceived: number\n}\n","import type { KoraEventType } from '@korajs/core'\nimport type { EventCategory, EventFilterCriteria, TimestampedEvent } from '../types'\nimport { eventTypeToCategory } from '../types'\n\n/**\n * Maps a KoraEventType to its EventCategory.\n * Re-exported from types for public API convenience.\n */\nexport function getEventCategory(type: KoraEventType): EventCategory {\n\treturn eventTypeToCategory(type)\n}\n\n/**\n * Extracts the collection name from an event, if present.\n * Only operation events and query:subscribed carry a collection field directly.\n * For other event types that embed operations, we inspect the nested operation.\n */\nfunction extractCollection(event: TimestampedEvent): string | null {\n\tconst e = event.event\n\tswitch (e.type) {\n\t\tcase 'operation:created':\n\t\tcase 'operation:applied':\n\t\t\treturn e.operation.collection\n\t\tcase 'merge:started':\n\t\t\treturn e.operationA.collection\n\t\tcase 'merge:completed':\n\t\tcase 'merge:conflict':\n\t\t\treturn e.trace.operationA.collection\n\t\tcase 'constraint:violated':\n\t\t\treturn e.trace.operationA.collection\n\t\tcase 'query:subscribed':\n\t\t\treturn e.collection\n\t\tcase 'query:invalidated':\n\t\t\treturn e.trigger.collection\n\t\tcase 'sync:sent':\n\t\t\treturn e.operations[0]?.collection ?? null\n\t\tcase 'sync:received':\n\t\t\treturn e.operations[0]?.collection ?? null\n\t\tdefault:\n\t\t\treturn null\n\t}\n}\n\n/**\n * Filters a list of timestamped events by the given criteria.\n * All criteria are combined with AND logic: an event must match all specified criteria.\n * Returns all events if no criteria are specified.\n */\nexport function filterEvents(\n\tevents: readonly TimestampedEvent[],\n\tcriteria: EventFilterCriteria,\n): readonly TimestampedEvent[] {\n\tconst { categories, types, timeRange, collection } = criteria\n\n\t// Fast path: no filters\n\tif (!categories && !types && !timeRange && !collection) {\n\t\treturn events\n\t}\n\n\t// Pre-compute the set of allowed types from categories for efficient lookup\n\tconst categorySet = categories ? new Set<EventCategory>(categories) : null\n\tconst typeSet = types ? new Set<KoraEventType>(types) : null\n\n\treturn events.filter((event) => {\n\t\t// Category filter\n\t\tif (categorySet) {\n\t\t\tconst cat = eventTypeToCategory(event.event.type)\n\t\t\tif (!categorySet.has(cat)) return false\n\t\t}\n\n\t\t// Specific type filter\n\t\tif (typeSet) {\n\t\t\tif (!typeSet.has(event.event.type)) return false\n\t\t}\n\n\t\t// Time range filter (on receivedAt)\n\t\tif (timeRange) {\n\t\t\tif (event.receivedAt < timeRange.start || event.receivedAt > timeRange.end) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\t// Collection filter\n\t\tif (collection) {\n\t\t\tconst eventCollection = extractCollection(event)\n\t\t\tif (eventCollection !== collection) return false\n\t\t}\n\n\t\treturn true\n\t})\n}\n","import type { KoraEventType } from '@korajs/core'\nimport type { EventCategory, EventStatistics, TimestampedEvent } from '../types'\nimport { EVENT_CATEGORIES, eventTypeToCategory } from '../types'\n\n/**\n * Computes aggregate statistics from a collection of timestamped events.\n * Processes the event list in a single pass for efficiency.\n */\nexport function computeStatistics(events: readonly TimestampedEvent[]): EventStatistics {\n\tconst eventsByCategory: Record<EventCategory, number> = {\n\t\toperation: 0,\n\t\tmerge: 0,\n\t\tsync: 0,\n\t\tquery: 0,\n\t\tconnection: 0,\n\t}\n\tconst eventsByType: Partial<Record<KoraEventType, number>> = {}\n\n\tlet mergeConflicts = 0\n\tlet constraintViolations = 0\n\tlet mergeDurationSum = 0\n\tlet mergeDurationCount = 0\n\tlet queryDurationSum = 0\n\tlet queryDurationCount = 0\n\tlet syncOperationsSent = 0\n\tlet syncOperationsReceived = 0\n\n\tfor (const timestamped of events) {\n\t\tconst e = timestamped.event\n\n\t\t// Category count\n\t\tconst category = eventTypeToCategory(e.type)\n\t\teventsByCategory[category]++\n\n\t\t// Type count\n\t\teventsByType[e.type] = (eventsByType[e.type] ?? 0) + 1\n\n\t\t// Type-specific aggregations\n\t\tswitch (e.type) {\n\t\t\tcase 'merge:completed':\n\t\t\t\tmergeDurationSum += e.trace.duration\n\t\t\t\tmergeDurationCount++\n\t\t\t\tbreak\n\t\t\tcase 'merge:conflict':\n\t\t\t\tmergeConflicts++\n\t\t\t\tmergeDurationSum += e.trace.duration\n\t\t\t\tmergeDurationCount++\n\t\t\t\tbreak\n\t\t\tcase 'constraint:violated':\n\t\t\t\tconstraintViolations++\n\t\t\t\tbreak\n\t\t\tcase 'query:executed':\n\t\t\t\tqueryDurationSum += e.duration\n\t\t\t\tqueryDurationCount++\n\t\t\t\tbreak\n\t\t\tcase 'sync:sent':\n\t\t\t\tsyncOperationsSent += e.batchSize\n\t\t\t\tbreak\n\t\t\tcase 'sync:received':\n\t\t\t\tsyncOperationsReceived += e.batchSize\n\t\t\t\tbreak\n\t\t}\n\t}\n\n\treturn {\n\t\ttotalEvents: events.length,\n\t\teventsByCategory,\n\t\teventsByType,\n\t\tmergeConflicts,\n\t\tconstraintViolations,\n\t\tavgMergeDuration: mergeDurationCount > 0 ? mergeDurationSum / mergeDurationCount : null,\n\t\tavgQueryDuration: queryDurationCount > 0 ? queryDurationSum / queryDurationCount : null,\n\t\tsyncOperationsSent,\n\t\tsyncOperationsReceived,\n\t}\n}\n","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('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n}\n","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"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { KoraEvent, KoraEventType, KoraEventEmitter, Operation } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/** A KoraEvent wrapped with a reception timestamp and sequential ID */
|
|
4
|
+
interface TimestampedEvent {
|
|
5
|
+
/** Auto-incrementing sequential ID */
|
|
6
|
+
id: number;
|
|
7
|
+
/** The original framework event */
|
|
8
|
+
event: KoraEvent;
|
|
9
|
+
/** Date.now() when the instrumenter captured the event */
|
|
10
|
+
receivedAt: number;
|
|
11
|
+
}
|
|
12
|
+
/** Event categories for filtering and grouping */
|
|
13
|
+
type EventCategory = 'operation' | 'merge' | 'sync' | 'query' | 'connection';
|
|
14
|
+
/** Configuration for the Instrumenter */
|
|
15
|
+
interface DevtoolsConfig {
|
|
16
|
+
/** Max events in the ring buffer (default: 10000) */
|
|
17
|
+
bufferSize?: number;
|
|
18
|
+
/** Enable message bridge for DevTools panel communication (default: true) */
|
|
19
|
+
bridgeEnabled?: boolean;
|
|
20
|
+
/** Custom message channel name (default: 'kora-devtools') */
|
|
21
|
+
channelName?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Filter criteria for querying events */
|
|
24
|
+
interface EventFilterCriteria {
|
|
25
|
+
/** Filter by event categories */
|
|
26
|
+
categories?: EventCategory[];
|
|
27
|
+
/** Filter by specific event types */
|
|
28
|
+
types?: KoraEventType[];
|
|
29
|
+
/** Filter by reception time range */
|
|
30
|
+
timeRange?: {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
};
|
|
34
|
+
/** Filter by collection name (extracted from operation events) */
|
|
35
|
+
collection?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Aggregated statistics computed from a set of events */
|
|
38
|
+
interface EventStatistics {
|
|
39
|
+
totalEvents: number;
|
|
40
|
+
eventsByCategory: Record<EventCategory, number>;
|
|
41
|
+
eventsByType: Partial<Record<KoraEventType, number>>;
|
|
42
|
+
mergeConflicts: number;
|
|
43
|
+
constraintViolations: number;
|
|
44
|
+
avgMergeDuration: number | null;
|
|
45
|
+
avgQueryDuration: number | null;
|
|
46
|
+
syncOperationsSent: number;
|
|
47
|
+
syncOperationsReceived: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Communicates between the page context and a DevTools panel via window.postMessage.
|
|
52
|
+
* All messages are namespaced with a `source` field to avoid collisions with other
|
|
53
|
+
* postMessage consumers on the page.
|
|
54
|
+
*
|
|
55
|
+
* Safe to instantiate in non-browser environments (SSR/Node) — all operations
|
|
56
|
+
* become no-ops when `window` is not available.
|
|
57
|
+
*/
|
|
58
|
+
declare class MessageBridge {
|
|
59
|
+
private readonly channelName;
|
|
60
|
+
private readonly listeners;
|
|
61
|
+
private readonly messageHandler;
|
|
62
|
+
private destroyed;
|
|
63
|
+
constructor(channelName?: string);
|
|
64
|
+
/**
|
|
65
|
+
* Post a timestamped event through the bridge.
|
|
66
|
+
* No-op if window is not available or the bridge has been destroyed.
|
|
67
|
+
*/
|
|
68
|
+
send(event: TimestampedEvent): void;
|
|
69
|
+
/**
|
|
70
|
+
* Register a callback for events received through the bridge.
|
|
71
|
+
* Returns an unsubscribe function.
|
|
72
|
+
*/
|
|
73
|
+
onReceive(callback: (event: TimestampedEvent) => void): () => void;
|
|
74
|
+
/**
|
|
75
|
+
* Remove all listeners and detach from window.
|
|
76
|
+
* After calling destroy, all operations become no-ops.
|
|
77
|
+
*/
|
|
78
|
+
destroy(): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fixed-capacity ring buffer for storing timestamped events.
|
|
83
|
+
* When the buffer is full, the oldest events are evicted to make room for new ones.
|
|
84
|
+
* This prevents unbounded memory growth when events accumulate fast.
|
|
85
|
+
*/
|
|
86
|
+
declare class EventBuffer {
|
|
87
|
+
private readonly _capacity;
|
|
88
|
+
private readonly buffer;
|
|
89
|
+
/** Index where the next event will be written */
|
|
90
|
+
private head;
|
|
91
|
+
/** Total number of events ever pushed (used to compute readable range) */
|
|
92
|
+
private _totalPushed;
|
|
93
|
+
constructor(capacity?: number);
|
|
94
|
+
/** Maximum number of events the buffer can hold */
|
|
95
|
+
get capacity(): number;
|
|
96
|
+
/** Current number of events in the buffer */
|
|
97
|
+
get size(): number;
|
|
98
|
+
/**
|
|
99
|
+
* Append an event to the buffer.
|
|
100
|
+
* If the buffer is at capacity, the oldest event is evicted.
|
|
101
|
+
*/
|
|
102
|
+
push(event: TimestampedEvent): void;
|
|
103
|
+
/**
|
|
104
|
+
* Returns all events in insertion order (oldest first).
|
|
105
|
+
*/
|
|
106
|
+
getAll(): readonly TimestampedEvent[];
|
|
107
|
+
/**
|
|
108
|
+
* Returns events whose sequential IDs fall within [start, end] (inclusive).
|
|
109
|
+
*/
|
|
110
|
+
getRange(start: number, end: number): readonly TimestampedEvent[];
|
|
111
|
+
/**
|
|
112
|
+
* Returns events matching a specific KoraEventType.
|
|
113
|
+
*/
|
|
114
|
+
getByType(type: KoraEventType): readonly TimestampedEvent[];
|
|
115
|
+
/** Remove all events from the buffer */
|
|
116
|
+
clear(): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Core orchestrator for Kora DevTools instrumentation.
|
|
121
|
+
*
|
|
122
|
+
* Attaches to a KoraEventEmitter, records all emitted events into a ring buffer
|
|
123
|
+
* with sequential IDs and reception timestamps, and optionally forwards them
|
|
124
|
+
* through a MessageBridge for consumption by a DevTools panel.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const instrumenter = new Instrumenter(app.emitter, { bufferSize: 5000 })
|
|
129
|
+
* const buffer = instrumenter.getBuffer()
|
|
130
|
+
* // ... later
|
|
131
|
+
* instrumenter.destroy()
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
declare class Instrumenter {
|
|
135
|
+
private readonly emitter;
|
|
136
|
+
private readonly buffer;
|
|
137
|
+
private readonly bridge;
|
|
138
|
+
private readonly unsubscribers;
|
|
139
|
+
private nextId;
|
|
140
|
+
private paused;
|
|
141
|
+
private destroyed;
|
|
142
|
+
constructor(emitter: KoraEventEmitter, config?: DevtoolsConfig);
|
|
143
|
+
/** Access the underlying event buffer */
|
|
144
|
+
getBuffer(): EventBuffer;
|
|
145
|
+
/** Access the message bridge, or null if bridge is disabled */
|
|
146
|
+
getBridge(): MessageBridge | null;
|
|
147
|
+
/** Temporarily stop recording events. Events emitted while paused are dropped. */
|
|
148
|
+
pause(): void;
|
|
149
|
+
/** Resume recording events after a pause. */
|
|
150
|
+
resume(): void;
|
|
151
|
+
/** Whether the instrumenter is currently paused */
|
|
152
|
+
isPaused(): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Detach all listeners from the emitter and destroy the bridge.
|
|
155
|
+
* After calling destroy, the instrumenter is inert.
|
|
156
|
+
*/
|
|
157
|
+
destroy(): void;
|
|
158
|
+
private attachListeners;
|
|
159
|
+
private handleEvent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Maps a KoraEventType to its EventCategory.
|
|
164
|
+
* Re-exported from types for public API convenience.
|
|
165
|
+
*/
|
|
166
|
+
declare function getEventCategory(type: KoraEventType): EventCategory;
|
|
167
|
+
/**
|
|
168
|
+
* Filters a list of timestamped events by the given criteria.
|
|
169
|
+
* All criteria are combined with AND logic: an event must match all specified criteria.
|
|
170
|
+
* Returns all events if no criteria are specified.
|
|
171
|
+
*/
|
|
172
|
+
declare function filterEvents(events: readonly TimestampedEvent[], criteria: EventFilterCriteria): readonly TimestampedEvent[];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Computes aggregate statistics from a collection of timestamped events.
|
|
176
|
+
* Processes the event list in a single pass for efficiency.
|
|
177
|
+
*/
|
|
178
|
+
declare function computeStatistics(events: readonly TimestampedEvent[]): EventStatistics;
|
|
179
|
+
|
|
180
|
+
interface TimelineItem {
|
|
181
|
+
id: number;
|
|
182
|
+
type: KoraEvent['type'];
|
|
183
|
+
label: string;
|
|
184
|
+
color: string;
|
|
185
|
+
receivedAt: number;
|
|
186
|
+
dependsOn: string[];
|
|
187
|
+
}
|
|
188
|
+
interface ConflictItem {
|
|
189
|
+
id: number;
|
|
190
|
+
timestamp: number;
|
|
191
|
+
collection: string;
|
|
192
|
+
field: string;
|
|
193
|
+
strategy: string;
|
|
194
|
+
tier: 1 | 2 | 3;
|
|
195
|
+
inputA: unknown;
|
|
196
|
+
inputB: unknown;
|
|
197
|
+
output: unknown;
|
|
198
|
+
constraintViolated: string | null;
|
|
199
|
+
}
|
|
200
|
+
interface OperationItem {
|
|
201
|
+
id: number;
|
|
202
|
+
timestamp: number;
|
|
203
|
+
operationId: string;
|
|
204
|
+
collection: string;
|
|
205
|
+
recordId: string;
|
|
206
|
+
opType: Operation['type'];
|
|
207
|
+
data: Record<string, unknown> | null;
|
|
208
|
+
causalDeps: string[];
|
|
209
|
+
nodeId: string;
|
|
210
|
+
sequenceNumber: number;
|
|
211
|
+
}
|
|
212
|
+
interface NetworkStatusModel {
|
|
213
|
+
connected: boolean;
|
|
214
|
+
quality: string | null;
|
|
215
|
+
pendingAcks: number;
|
|
216
|
+
lastSyncAt: number | null;
|
|
217
|
+
sentOps: number;
|
|
218
|
+
receivedOps: number;
|
|
219
|
+
versionVector: Array<{
|
|
220
|
+
nodeId: string;
|
|
221
|
+
sequenceNumber: number;
|
|
222
|
+
}>;
|
|
223
|
+
}
|
|
224
|
+
interface DevtoolsPanelModel {
|
|
225
|
+
timeline: TimelineItem[];
|
|
226
|
+
conflicts: ConflictItem[];
|
|
227
|
+
operations: OperationItem[];
|
|
228
|
+
network: NetworkStatusModel;
|
|
229
|
+
}
|
|
230
|
+
declare function buildPanelModel(events: readonly TimestampedEvent[]): DevtoolsPanelModel;
|
|
231
|
+
|
|
232
|
+
declare function renderDevtoolsPanel(target: HTMLElement, events: readonly TimestampedEvent[]): void;
|
|
233
|
+
|
|
234
|
+
interface ExtensionPort {
|
|
235
|
+
name: string;
|
|
236
|
+
onMessage: {
|
|
237
|
+
addListener(callback: (message: unknown) => void): void;
|
|
238
|
+
};
|
|
239
|
+
onDisconnect: {
|
|
240
|
+
addListener(callback: () => void): void;
|
|
241
|
+
};
|
|
242
|
+
postMessage(message: unknown): void;
|
|
243
|
+
sender?: {
|
|
244
|
+
tab?: {
|
|
245
|
+
id?: number;
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Routes content-script events to the matching DevTools panel by tab.
|
|
251
|
+
*/
|
|
252
|
+
declare class PortRouter {
|
|
253
|
+
private readonly panelClients;
|
|
254
|
+
private readonly contentClients;
|
|
255
|
+
handleConnection(port: ExtensionPort): void;
|
|
256
|
+
private attachPanel;
|
|
257
|
+
private attachContent;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { type DevtoolsConfig, EventBuffer, type EventCategory, type EventFilterCriteria, type EventStatistics, Instrumenter, MessageBridge, PortRouter, type TimestampedEvent, buildPanelModel, computeStatistics, filterEvents, getEventCategory, renderDevtoolsPanel };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { KoraEvent, KoraEventType, KoraEventEmitter, Operation } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/** A KoraEvent wrapped with a reception timestamp and sequential ID */
|
|
4
|
+
interface TimestampedEvent {
|
|
5
|
+
/** Auto-incrementing sequential ID */
|
|
6
|
+
id: number;
|
|
7
|
+
/** The original framework event */
|
|
8
|
+
event: KoraEvent;
|
|
9
|
+
/** Date.now() when the instrumenter captured the event */
|
|
10
|
+
receivedAt: number;
|
|
11
|
+
}
|
|
12
|
+
/** Event categories for filtering and grouping */
|
|
13
|
+
type EventCategory = 'operation' | 'merge' | 'sync' | 'query' | 'connection';
|
|
14
|
+
/** Configuration for the Instrumenter */
|
|
15
|
+
interface DevtoolsConfig {
|
|
16
|
+
/** Max events in the ring buffer (default: 10000) */
|
|
17
|
+
bufferSize?: number;
|
|
18
|
+
/** Enable message bridge for DevTools panel communication (default: true) */
|
|
19
|
+
bridgeEnabled?: boolean;
|
|
20
|
+
/** Custom message channel name (default: 'kora-devtools') */
|
|
21
|
+
channelName?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Filter criteria for querying events */
|
|
24
|
+
interface EventFilterCriteria {
|
|
25
|
+
/** Filter by event categories */
|
|
26
|
+
categories?: EventCategory[];
|
|
27
|
+
/** Filter by specific event types */
|
|
28
|
+
types?: KoraEventType[];
|
|
29
|
+
/** Filter by reception time range */
|
|
30
|
+
timeRange?: {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
};
|
|
34
|
+
/** Filter by collection name (extracted from operation events) */
|
|
35
|
+
collection?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Aggregated statistics computed from a set of events */
|
|
38
|
+
interface EventStatistics {
|
|
39
|
+
totalEvents: number;
|
|
40
|
+
eventsByCategory: Record<EventCategory, number>;
|
|
41
|
+
eventsByType: Partial<Record<KoraEventType, number>>;
|
|
42
|
+
mergeConflicts: number;
|
|
43
|
+
constraintViolations: number;
|
|
44
|
+
avgMergeDuration: number | null;
|
|
45
|
+
avgQueryDuration: number | null;
|
|
46
|
+
syncOperationsSent: number;
|
|
47
|
+
syncOperationsReceived: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Communicates between the page context and a DevTools panel via window.postMessage.
|
|
52
|
+
* All messages are namespaced with a `source` field to avoid collisions with other
|
|
53
|
+
* postMessage consumers on the page.
|
|
54
|
+
*
|
|
55
|
+
* Safe to instantiate in non-browser environments (SSR/Node) — all operations
|
|
56
|
+
* become no-ops when `window` is not available.
|
|
57
|
+
*/
|
|
58
|
+
declare class MessageBridge {
|
|
59
|
+
private readonly channelName;
|
|
60
|
+
private readonly listeners;
|
|
61
|
+
private readonly messageHandler;
|
|
62
|
+
private destroyed;
|
|
63
|
+
constructor(channelName?: string);
|
|
64
|
+
/**
|
|
65
|
+
* Post a timestamped event through the bridge.
|
|
66
|
+
* No-op if window is not available or the bridge has been destroyed.
|
|
67
|
+
*/
|
|
68
|
+
send(event: TimestampedEvent): void;
|
|
69
|
+
/**
|
|
70
|
+
* Register a callback for events received through the bridge.
|
|
71
|
+
* Returns an unsubscribe function.
|
|
72
|
+
*/
|
|
73
|
+
onReceive(callback: (event: TimestampedEvent) => void): () => void;
|
|
74
|
+
/**
|
|
75
|
+
* Remove all listeners and detach from window.
|
|
76
|
+
* After calling destroy, all operations become no-ops.
|
|
77
|
+
*/
|
|
78
|
+
destroy(): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fixed-capacity ring buffer for storing timestamped events.
|
|
83
|
+
* When the buffer is full, the oldest events are evicted to make room for new ones.
|
|
84
|
+
* This prevents unbounded memory growth when events accumulate fast.
|
|
85
|
+
*/
|
|
86
|
+
declare class EventBuffer {
|
|
87
|
+
private readonly _capacity;
|
|
88
|
+
private readonly buffer;
|
|
89
|
+
/** Index where the next event will be written */
|
|
90
|
+
private head;
|
|
91
|
+
/** Total number of events ever pushed (used to compute readable range) */
|
|
92
|
+
private _totalPushed;
|
|
93
|
+
constructor(capacity?: number);
|
|
94
|
+
/** Maximum number of events the buffer can hold */
|
|
95
|
+
get capacity(): number;
|
|
96
|
+
/** Current number of events in the buffer */
|
|
97
|
+
get size(): number;
|
|
98
|
+
/**
|
|
99
|
+
* Append an event to the buffer.
|
|
100
|
+
* If the buffer is at capacity, the oldest event is evicted.
|
|
101
|
+
*/
|
|
102
|
+
push(event: TimestampedEvent): void;
|
|
103
|
+
/**
|
|
104
|
+
* Returns all events in insertion order (oldest first).
|
|
105
|
+
*/
|
|
106
|
+
getAll(): readonly TimestampedEvent[];
|
|
107
|
+
/**
|
|
108
|
+
* Returns events whose sequential IDs fall within [start, end] (inclusive).
|
|
109
|
+
*/
|
|
110
|
+
getRange(start: number, end: number): readonly TimestampedEvent[];
|
|
111
|
+
/**
|
|
112
|
+
* Returns events matching a specific KoraEventType.
|
|
113
|
+
*/
|
|
114
|
+
getByType(type: KoraEventType): readonly TimestampedEvent[];
|
|
115
|
+
/** Remove all events from the buffer */
|
|
116
|
+
clear(): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Core orchestrator for Kora DevTools instrumentation.
|
|
121
|
+
*
|
|
122
|
+
* Attaches to a KoraEventEmitter, records all emitted events into a ring buffer
|
|
123
|
+
* with sequential IDs and reception timestamps, and optionally forwards them
|
|
124
|
+
* through a MessageBridge for consumption by a DevTools panel.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const instrumenter = new Instrumenter(app.emitter, { bufferSize: 5000 })
|
|
129
|
+
* const buffer = instrumenter.getBuffer()
|
|
130
|
+
* // ... later
|
|
131
|
+
* instrumenter.destroy()
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
declare class Instrumenter {
|
|
135
|
+
private readonly emitter;
|
|
136
|
+
private readonly buffer;
|
|
137
|
+
private readonly bridge;
|
|
138
|
+
private readonly unsubscribers;
|
|
139
|
+
private nextId;
|
|
140
|
+
private paused;
|
|
141
|
+
private destroyed;
|
|
142
|
+
constructor(emitter: KoraEventEmitter, config?: DevtoolsConfig);
|
|
143
|
+
/** Access the underlying event buffer */
|
|
144
|
+
getBuffer(): EventBuffer;
|
|
145
|
+
/** Access the message bridge, or null if bridge is disabled */
|
|
146
|
+
getBridge(): MessageBridge | null;
|
|
147
|
+
/** Temporarily stop recording events. Events emitted while paused are dropped. */
|
|
148
|
+
pause(): void;
|
|
149
|
+
/** Resume recording events after a pause. */
|
|
150
|
+
resume(): void;
|
|
151
|
+
/** Whether the instrumenter is currently paused */
|
|
152
|
+
isPaused(): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Detach all listeners from the emitter and destroy the bridge.
|
|
155
|
+
* After calling destroy, the instrumenter is inert.
|
|
156
|
+
*/
|
|
157
|
+
destroy(): void;
|
|
158
|
+
private attachListeners;
|
|
159
|
+
private handleEvent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Maps a KoraEventType to its EventCategory.
|
|
164
|
+
* Re-exported from types for public API convenience.
|
|
165
|
+
*/
|
|
166
|
+
declare function getEventCategory(type: KoraEventType): EventCategory;
|
|
167
|
+
/**
|
|
168
|
+
* Filters a list of timestamped events by the given criteria.
|
|
169
|
+
* All criteria are combined with AND logic: an event must match all specified criteria.
|
|
170
|
+
* Returns all events if no criteria are specified.
|
|
171
|
+
*/
|
|
172
|
+
declare function filterEvents(events: readonly TimestampedEvent[], criteria: EventFilterCriteria): readonly TimestampedEvent[];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Computes aggregate statistics from a collection of timestamped events.
|
|
176
|
+
* Processes the event list in a single pass for efficiency.
|
|
177
|
+
*/
|
|
178
|
+
declare function computeStatistics(events: readonly TimestampedEvent[]): EventStatistics;
|
|
179
|
+
|
|
180
|
+
interface TimelineItem {
|
|
181
|
+
id: number;
|
|
182
|
+
type: KoraEvent['type'];
|
|
183
|
+
label: string;
|
|
184
|
+
color: string;
|
|
185
|
+
receivedAt: number;
|
|
186
|
+
dependsOn: string[];
|
|
187
|
+
}
|
|
188
|
+
interface ConflictItem {
|
|
189
|
+
id: number;
|
|
190
|
+
timestamp: number;
|
|
191
|
+
collection: string;
|
|
192
|
+
field: string;
|
|
193
|
+
strategy: string;
|
|
194
|
+
tier: 1 | 2 | 3;
|
|
195
|
+
inputA: unknown;
|
|
196
|
+
inputB: unknown;
|
|
197
|
+
output: unknown;
|
|
198
|
+
constraintViolated: string | null;
|
|
199
|
+
}
|
|
200
|
+
interface OperationItem {
|
|
201
|
+
id: number;
|
|
202
|
+
timestamp: number;
|
|
203
|
+
operationId: string;
|
|
204
|
+
collection: string;
|
|
205
|
+
recordId: string;
|
|
206
|
+
opType: Operation['type'];
|
|
207
|
+
data: Record<string, unknown> | null;
|
|
208
|
+
causalDeps: string[];
|
|
209
|
+
nodeId: string;
|
|
210
|
+
sequenceNumber: number;
|
|
211
|
+
}
|
|
212
|
+
interface NetworkStatusModel {
|
|
213
|
+
connected: boolean;
|
|
214
|
+
quality: string | null;
|
|
215
|
+
pendingAcks: number;
|
|
216
|
+
lastSyncAt: number | null;
|
|
217
|
+
sentOps: number;
|
|
218
|
+
receivedOps: number;
|
|
219
|
+
versionVector: Array<{
|
|
220
|
+
nodeId: string;
|
|
221
|
+
sequenceNumber: number;
|
|
222
|
+
}>;
|
|
223
|
+
}
|
|
224
|
+
interface DevtoolsPanelModel {
|
|
225
|
+
timeline: TimelineItem[];
|
|
226
|
+
conflicts: ConflictItem[];
|
|
227
|
+
operations: OperationItem[];
|
|
228
|
+
network: NetworkStatusModel;
|
|
229
|
+
}
|
|
230
|
+
declare function buildPanelModel(events: readonly TimestampedEvent[]): DevtoolsPanelModel;
|
|
231
|
+
|
|
232
|
+
declare function renderDevtoolsPanel(target: HTMLElement, events: readonly TimestampedEvent[]): void;
|
|
233
|
+
|
|
234
|
+
interface ExtensionPort {
|
|
235
|
+
name: string;
|
|
236
|
+
onMessage: {
|
|
237
|
+
addListener(callback: (message: unknown) => void): void;
|
|
238
|
+
};
|
|
239
|
+
onDisconnect: {
|
|
240
|
+
addListener(callback: () => void): void;
|
|
241
|
+
};
|
|
242
|
+
postMessage(message: unknown): void;
|
|
243
|
+
sender?: {
|
|
244
|
+
tab?: {
|
|
245
|
+
id?: number;
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Routes content-script events to the matching DevTools panel by tab.
|
|
251
|
+
*/
|
|
252
|
+
declare class PortRouter {
|
|
253
|
+
private readonly panelClients;
|
|
254
|
+
private readonly contentClients;
|
|
255
|
+
handleConnection(port: ExtensionPort): void;
|
|
256
|
+
private attachPanel;
|
|
257
|
+
private attachContent;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { type DevtoolsConfig, EventBuffer, type EventCategory, type EventFilterCriteria, type EventStatistics, Instrumenter, MessageBridge, PortRouter, type TimestampedEvent, buildPanelModel, computeStatistics, filterEvents, getEventCategory, renderDevtoolsPanel };
|