@korajs/test 0.6.0 → 1.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dr. Obed Ehoneah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs CHANGED
@@ -30,14 +30,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- ChaosTransport: () => import_sync3.ChaosTransport,
33
+ ChaosTransport: () => import_sync4.ChaosTransport,
34
34
  TestDevice: () => TestDevice,
35
35
  TestServer: () => TestServer,
36
36
  checkConvergence: () => checkConvergence,
37
37
  createMixedTestNetwork: () => createMixedTestNetwork,
38
38
  createTestNetwork: () => createTestNetwork,
39
39
  expectConverged: () => expectConverged,
40
- expectConvergedEventually: () => expectConvergedEventually
40
+ expectConvergedEventually: () => expectConvergedEventually,
41
+ wrapTransportPairWithProtobufWire: () => wrapTransportPairWithProtobufWire,
42
+ wrapTransportPairWithServerClock: () => wrapTransportPairWithServerClock
41
43
  });
42
44
  module.exports = __toCommonJS(index_exports);
43
45
 
@@ -71,6 +73,7 @@ var TestDevice = class {
71
73
  syncEngine = null;
72
74
  currentTransport = null;
73
75
  unsubscribeSync = null;
76
+ unsubscribeAudit = null;
74
77
  closing = false;
75
78
  constructor(options) {
76
79
  this.name = options.name;
@@ -100,6 +103,7 @@ var TestDevice = class {
100
103
  emitter: this.emitter
101
104
  });
102
105
  this.store.setLocalMutationHandler(this.applyPipeline);
106
+ this.unsubscribeAudit = (0, import_testing.wireAuditPersistence)(this.store, this.emitter);
103
107
  }
104
108
  /**
105
109
  * Connect to the test server and perform initial sync.
@@ -201,6 +205,10 @@ var TestDevice = class {
201
205
  */
202
206
  async close() {
203
207
  this.closing = true;
208
+ if (this.unsubscribeAudit) {
209
+ this.unsubscribeAudit();
210
+ this.unsubscribeAudit = null;
211
+ }
204
212
  await this.disconnect();
205
213
  await this.store.close();
206
214
  this.emitter.clear();
@@ -292,10 +300,11 @@ async function createTestNetwork(schema, options) {
292
300
  const pair = (0, import_internal2.createServerTransportPair)();
293
301
  const client = pair.client;
294
302
  const chaos = options?.chaos;
295
- return {
303
+ const base = {
296
304
  client: chaos ? new import_sync2.ChaosTransport(client, chaos) : client,
297
305
  serverTransport: pair.server
298
306
  };
307
+ return options?.wrapTransport ? options.wrapTransport(base) : base;
299
308
  },
300
309
  tmpDir
301
310
  });
@@ -489,8 +498,128 @@ function deepEqual(a, b) {
489
498
  return false;
490
499
  }
491
500
 
492
- // src/index.ts
501
+ // src/protobuf-wire-transport.ts
493
502
  var import_sync3 = require("@korajs/sync");
503
+ var PROTOBUF_WIRE_TYPES = /* @__PURE__ */ new Set([
504
+ "handshake",
505
+ "handshake-response",
506
+ "operation-batch",
507
+ "acknowledgment",
508
+ "error"
509
+ ]);
510
+ function roundTripThroughProtobuf(message, serializer) {
511
+ if (!PROTOBUF_WIRE_TYPES.has(message.type)) {
512
+ return structuredClone(message);
513
+ }
514
+ const encoded = serializer.encode(message);
515
+ if (!(encoded instanceof Uint8Array)) {
516
+ throw new Error("Expected the protobuf serializer to encode to bytes");
517
+ }
518
+ return serializer.decode(encoded);
519
+ }
520
+ var ProtobufWireClientTransport = class {
521
+ constructor(inner, serializer) {
522
+ this.inner = inner;
523
+ this.serializer = serializer;
524
+ }
525
+ inner;
526
+ serializer;
527
+ connect(url, options) {
528
+ return this.inner.connect(url, options);
529
+ }
530
+ disconnect() {
531
+ return this.inner.disconnect();
532
+ }
533
+ send(message) {
534
+ this.inner.send(roundTripThroughProtobuf(message, this.serializer));
535
+ }
536
+ onMessage(handler) {
537
+ this.inner.onMessage(handler);
538
+ }
539
+ onClose(handler) {
540
+ this.inner.onClose(handler);
541
+ }
542
+ onError(handler) {
543
+ this.inner.onError(handler);
544
+ }
545
+ isConnected() {
546
+ return this.inner.isConnected();
547
+ }
548
+ };
549
+ var ProtobufWireServerTransport = class {
550
+ constructor(inner, serializer) {
551
+ this.inner = inner;
552
+ this.serializer = serializer;
553
+ }
554
+ inner;
555
+ serializer;
556
+ send(message) {
557
+ this.inner.send(roundTripThroughProtobuf(message, this.serializer));
558
+ }
559
+ onMessage(handler) {
560
+ this.inner.onMessage(handler);
561
+ }
562
+ onClose(handler) {
563
+ this.inner.onClose(handler);
564
+ }
565
+ onError(handler) {
566
+ this.inner.onError(handler);
567
+ }
568
+ isConnected() {
569
+ return this.inner.isConnected();
570
+ }
571
+ close(code, reason) {
572
+ this.inner.close(code, reason);
573
+ }
574
+ };
575
+ function wrapTransportPairWithProtobufWire(pair) {
576
+ const serializer = new import_sync3.ProtobufMessageSerializer();
577
+ return {
578
+ client: new ProtobufWireClientTransport(pair.client, serializer),
579
+ serverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer)
580
+ };
581
+ }
582
+
583
+ // src/server-clock-transport.ts
584
+ var ServerClockServerTransport = class {
585
+ constructor(inner, serverNow) {
586
+ this.inner = inner;
587
+ this.serverNow = serverNow;
588
+ }
589
+ inner;
590
+ serverNow;
591
+ send(message) {
592
+ if (message.type === "handshake-response") {
593
+ this.inner.send({ ...message, serverTime: this.serverNow() });
594
+ return;
595
+ }
596
+ this.inner.send(message);
597
+ }
598
+ onMessage(handler) {
599
+ this.inner.onMessage(handler);
600
+ }
601
+ onClose(handler) {
602
+ this.inner.onClose(handler);
603
+ }
604
+ onError(handler) {
605
+ this.inner.onError(handler);
606
+ }
607
+ isConnected() {
608
+ return this.inner.isConnected();
609
+ }
610
+ close(code, reason) {
611
+ this.inner.close(code, reason);
612
+ }
613
+ };
614
+ function wrapTransportPairWithServerClock(pair, serverNow) {
615
+ return {
616
+ client: pair.client,
617
+ serverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow)
618
+ };
619
+ }
620
+
621
+ // src/index.ts
622
+ var import_sync4 = require("@korajs/sync");
494
623
  // Annotate the CommonJS export names for ESM import in node:
495
624
  0 && (module.exports = {
496
625
  ChaosTransport,
@@ -500,6 +629,8 @@ var import_sync3 = require("@korajs/sync");
500
629
  createMixedTestNetwork,
501
630
  createTestNetwork,
502
631
  expectConverged,
503
- expectConvergedEventually
632
+ expectConvergedEventually,
633
+ wrapTransportPairWithProtobufWire,
634
+ wrapTransportPairWithServerClock
504
635
  });
505
636
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/test-network.ts","../src/test-device.ts","../src/test-server.ts","../src/assertions.ts"],"sourcesContent":["// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n","import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n}\n\n/**\n * A test network with a server and multiple devices.\n */\nexport interface TestNetwork {\n\t/** The test server */\n\tserver: TestServer\n\t/** All devices in the network */\n\tdevices: TestDevice[]\n\t/** Temporary directory for DB files */\n\ttmpDir: string\n\t/** Close all devices and the server, clean up temp files */\n\tclose(): Promise<void>\n}\n\n/**\n * Create a test network with a server and multiple virtual devices.\n *\n * Each device has its own local SQLite store and SyncEngine. Devices\n * communicate with the server via in-memory transports.\n *\n * @param schema - The schema all devices share\n * @param options - Network configuration\n * @returns A test network with server and devices ready for use\n *\n * @example\n * ```typescript\n * const network = await createTestNetwork(schema, { devices: 2 })\n * const [deviceA, deviceB] = network.devices\n *\n * await deviceA.collection('todos').insert({ title: 'Hello' })\n * await deviceA.sync()\n * await deviceB.sync()\n *\n * const todos = await deviceB.getState('todos')\n * expect(todos).toHaveLength(1)\n *\n * await network.close()\n * ```\n */\nexport async function createTestNetwork(\n\tschema: SchemaDefinition,\n\toptions?: TestNetworkOptions,\n): Promise<TestNetwork> {\n\tconst deviceCount = options?.devices ?? 2\n\tconst deviceNames =\n\t\toptions?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`)\n\n\t// Create temp directory for DB files\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\n\t// Create server\n\tconst server = new TestServer(schema)\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\treturn {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport { Store } from '@korajs/store'\nimport type { CollectionAccessor, StorageAdapter } from '@korajs/store'\nimport { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\nimport { SyncEngine } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport {\n\tApplyPipeline,\n\tMergeAwareSyncStore,\n\tStoreQueueStorage,\n\tStoreSyncStatePersistence,\n} from 'korajs/testing'\nimport type { TestServer } from './test-server'\n\n/**\n * Options for creating a TestDevice.\n */\nexport interface TestDeviceOptions {\n\t/** Unique device name (used for DB file naming) */\n\tname: string\n\t/** Schema definition */\n\tschema: SchemaDefinition\n\t/** Test server to connect to */\n\tserver: TestServer\n\t/** Transport factory — creates a linked client/server transport pair */\n\tcreateTransportPair: () => {\n\t\tclient: SyncTransport\n\t\tserverTransport: import('@korajs/server').ServerTransport\n\t}\n\t/** Optional directory for temp DB files */\n\ttmpDir: string\n\t/** Client handshake schema version. Defaults to `schema.version`. */\n\tsyncSchemaVersion?: number\n\t/** Transforms applied to inbound operations before local apply. */\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * A virtual device in a test network.\n * Each device has its own Store (with real SQLite), SyncEngine, and MergeEngine.\n * Provides high-level methods for syncing, disconnecting, and inspecting state.\n */\nexport class TestDevice {\n\treadonly name: string\n\treadonly store: Store\n\treadonly emitter: KoraEventEmitter & { clear(): void }\n\n\tprivate readonly schema: SchemaDefinition\n\tprivate readonly server: TestServer\n\tprivate readonly mergeEngine: MergeEngine\n\tprivate readonly createTransportPair: TestDeviceOptions['createTransportPair']\n\tprivate readonly adapter: StorageAdapter\n\tprivate readonly dbPath: string\n\tprivate readonly syncSchemaVersion: number\n\tprivate readonly operationTransforms: OperationTransform[]\n\n\tprivate applyPipeline: ApplyPipeline | null = null\n\tprivate syncEngine: SyncEngine | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Wire local mutations to sync outbound queue\n\t\tconst engine = this.syncEngine\n\t\tthis.unsubscribeSync = this.emitter.on('operation:created', (event) => {\n\t\t\tif (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {\n\t\t\t\t// Catch async errors from push racing with disconnect during teardown\n\t\t\t\tthis.syncEngine.pushOperation(event.operation).catch(() => {})\n\t\t\t}\n\t\t})\n\n\t\t// Register server-side connection\n\t\tthis.server.handleConnection(serverTransport)\n\n\t\t// Start sync engine (connects, handshakes, exchanges deltas)\n\t\tawait this.syncEngine.start()\n\n\t\t// Wait for sync messages to propagate (in-memory transport is synchronous\n\t\t// but some processing is async)\n\t\tawait this.waitForSettled()\n\t}\n\n\t/**\n\t * Disconnect from the test server.\n\t */\n\tasync disconnect(): Promise<void> {\n\t\tif (this.unsubscribeSync) {\n\t\t\tthis.unsubscribeSync()\n\t\t\tthis.unsubscribeSync = null\n\t\t}\n\t\tif (this.syncEngine) {\n\t\t\tawait this.syncEngine.stop()\n\t\t\tthis.syncEngine = null\n\t\t}\n\t\tthis.currentTransport = null\n\t}\n\n\t/**\n\t * Reconnect to the test server after a disconnect.\n\t */\n\tasync reconnect(): Promise<void> {\n\t\tawait this.sync()\n\t}\n\n\t/**\n\t * Get a collection accessor for performing CRUD operations.\n\t */\n\tcollection(name: string): CollectionAccessor {\n\t\treturn this.store.collection(name)\n\t}\n\n\t/**\n\t * Get all records from a collection (convenience method).\n\t */\n\tasync getState(collectionName: string): Promise<Record<string, unknown>[]> {\n\t\tconst accessor = this.store.collection(collectionName)\n\t\treturn accessor.where({}).exec()\n\t}\n\n\t/**\n\t * Get the device's node ID.\n\t */\n\tgetNodeId(): string {\n\t\treturn this.store.getNodeId()\n\t}\n\n\t/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */\n\tgetSyncEngine(): SyncEngine | null {\n\t\treturn this.syncEngine\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\n\n/**\n * In-memory test server wrapping KoraSyncServer with MemoryServerStore.\n * Handles client connections via memory transports.\n */\nexport interface TestServerOptions {\n\t/** Handshake schema version advertised by the server. Defaults to `schema.version`. */\n\tschemaVersion?: number\n\t/** Inclusive client schema versions accepted at handshake. */\n\tsupportedSchemaVersions?: { min: number; max: number }\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition, options?: TestServerOptions) {\n\t\tthis.store = new MemoryServerStore()\n\t\tconst schemaVersion = options?.schemaVersion ?? schema.version\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion,\n\t\t\tsupportedSchemaVersions: options?.supportedSchemaVersions ?? {\n\t\t\t\tmin: schemaVersion,\n\t\t\t\tmax: schemaVersion,\n\t\t\t},\n\t\t})\n\t\tvoid this.store.setSchema(schema)\n\t}\n\n\t/**\n\t * Register a client connection transport with the server.\n\t * Returns the session ID assigned by the server.\n\t */\n\thandleConnection(transport: ServerTransport): string {\n\t\treturn this.syncServer.handleConnection(transport)\n\t}\n\n\t/**\n\t * Get all operations stored on the server.\n\t */\n\tgetAllOperations(): Operation[] {\n\t\treturn this.store.getAllOperations()\n\t}\n\n\t/**\n\t * Get the number of connected clients.\n\t */\n\tgetConnectionCount(): number {\n\t\treturn this.syncServer.getConnectionCount()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n"],"mappings":"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","names":["import_internal","import_sync","import_server","import_sync"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/test-network.ts","../src/test-device.ts","../src/test-server.ts","../src/assertions.ts","../src/protobuf-wire-transport.ts","../src/server-clock-transport.ts"],"sourcesContent":["// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Protobuf wire transport (for testing convergence through the real wire format) ===\nexport { wrapTransportPairWithProtobufWire } from './protobuf-wire-transport'\nexport type { TransportPair } from './protobuf-wire-transport'\n\n// === Server-clock transport (for testing clock-skew / rebase integration) ===\nexport { wrapTransportPairWithServerClock } from './server-clock-transport'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n","import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n\t/**\n\t * Optional wrapper applied to each device's transport pair after the base\n\t * (in-memory, optionally chaos-wrapped) pair is created. Use it to route\n\t * messages through the protobuf wire codec\n\t * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable\n\t * server clock. Called once per device connection.\n\t */\n\twrapTransport?: (pair: TransportPair) => TransportPair\n}\n\n/**\n * A test network with a server and multiple devices.\n */\nexport interface TestNetwork {\n\t/** The test server */\n\tserver: TestServer\n\t/** All devices in the network */\n\tdevices: TestDevice[]\n\t/** Temporary directory for DB files */\n\ttmpDir: string\n\t/** Close all devices and the server, clean up temp files */\n\tclose(): Promise<void>\n}\n\n/**\n * Create a test network with a server and multiple virtual devices.\n *\n * Each device has its own local SQLite store and SyncEngine. Devices\n * communicate with the server via in-memory transports.\n *\n * @param schema - The schema all devices share\n * @param options - Network configuration\n * @returns A test network with server and devices ready for use\n *\n * @example\n * ```typescript\n * const network = await createTestNetwork(schema, { devices: 2 })\n * const [deviceA, deviceB] = network.devices\n *\n * await deviceA.collection('todos').insert({ title: 'Hello' })\n * await deviceA.sync()\n * await deviceB.sync()\n *\n * const todos = await deviceB.getState('todos')\n * expect(todos).toHaveLength(1)\n *\n * await network.close()\n * ```\n */\nexport async function createTestNetwork(\n\tschema: SchemaDefinition,\n\toptions?: TestNetworkOptions,\n): Promise<TestNetwork> {\n\tconst deviceCount = options?.devices ?? 2\n\tconst deviceNames =\n\t\toptions?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`)\n\n\t// Create temp directory for DB files\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\n\t// Create server\n\tconst server = new TestServer(schema)\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\tconst base: TransportPair = {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t\treturn options?.wrapTransport ? options.wrapTransport(base) : base\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport { Store } from '@korajs/store'\nimport type { CollectionAccessor, StorageAdapter } from '@korajs/store'\nimport { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\nimport { SyncEngine } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport {\n\tApplyPipeline,\n\tMergeAwareSyncStore,\n\tStoreQueueStorage,\n\tStoreSyncStatePersistence,\n\twireAuditPersistence,\n} from 'korajs/testing'\nimport type { TestServer } from './test-server'\n\n/**\n * Options for creating a TestDevice.\n */\nexport interface TestDeviceOptions {\n\t/** Unique device name (used for DB file naming) */\n\tname: string\n\t/** Schema definition */\n\tschema: SchemaDefinition\n\t/** Test server to connect to */\n\tserver: TestServer\n\t/** Transport factory — creates a linked client/server transport pair */\n\tcreateTransportPair: () => {\n\t\tclient: SyncTransport\n\t\tserverTransport: import('@korajs/server').ServerTransport\n\t}\n\t/** Optional directory for temp DB files */\n\ttmpDir: string\n\t/** Client handshake schema version. Defaults to `schema.version`. */\n\tsyncSchemaVersion?: number\n\t/** Transforms applied to inbound operations before local apply. */\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * A virtual device in a test network.\n * Each device has its own Store (with real SQLite), SyncEngine, and MergeEngine.\n * Provides high-level methods for syncing, disconnecting, and inspecting state.\n */\nexport class TestDevice {\n\treadonly name: string\n\treadonly store: Store\n\treadonly emitter: KoraEventEmitter & { clear(): void }\n\n\tprivate readonly schema: SchemaDefinition\n\tprivate readonly server: TestServer\n\tprivate readonly mergeEngine: MergeEngine\n\tprivate readonly createTransportPair: TestDeviceOptions['createTransportPair']\n\tprivate readonly adapter: StorageAdapter\n\tprivate readonly dbPath: string\n\tprivate readonly syncSchemaVersion: number\n\tprivate readonly operationTransforms: OperationTransform[]\n\n\tprivate applyPipeline: ApplyPipeline | null = null\n\tprivate syncEngine: SyncEngine | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate unsubscribeAudit: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t\t// Match production wiring (createApp): merge/constraint traces persist to\n\t\t// `_kora_audit_traces`. Without this, harness devices emit merge events\n\t\t// but the durable audit trail every real app has stays empty — a fidelity\n\t\t// gap that hid from tests until Studio's Merges view made it visible.\n\t\tthis.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Wire local mutations to sync outbound queue\n\t\tconst engine = this.syncEngine\n\t\tthis.unsubscribeSync = this.emitter.on('operation:created', (event) => {\n\t\t\tif (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {\n\t\t\t\t// Catch async errors from push racing with disconnect during teardown\n\t\t\t\tthis.syncEngine.pushOperation(event.operation).catch(() => {})\n\t\t\t}\n\t\t})\n\n\t\t// Register server-side connection\n\t\tthis.server.handleConnection(serverTransport)\n\n\t\t// Start sync engine (connects, handshakes, exchanges deltas)\n\t\tawait this.syncEngine.start()\n\n\t\t// Wait for sync messages to propagate (in-memory transport is synchronous\n\t\t// but some processing is async)\n\t\tawait this.waitForSettled()\n\t}\n\n\t/**\n\t * Disconnect from the test server.\n\t */\n\tasync disconnect(): Promise<void> {\n\t\tif (this.unsubscribeSync) {\n\t\t\tthis.unsubscribeSync()\n\t\t\tthis.unsubscribeSync = null\n\t\t}\n\t\tif (this.syncEngine) {\n\t\t\tawait this.syncEngine.stop()\n\t\t\tthis.syncEngine = null\n\t\t}\n\t\tthis.currentTransport = null\n\t}\n\n\t/**\n\t * Reconnect to the test server after a disconnect.\n\t */\n\tasync reconnect(): Promise<void> {\n\t\tawait this.sync()\n\t}\n\n\t/**\n\t * Get a collection accessor for performing CRUD operations.\n\t */\n\tcollection(name: string): CollectionAccessor {\n\t\treturn this.store.collection(name)\n\t}\n\n\t/**\n\t * Get all records from a collection (convenience method).\n\t */\n\tasync getState(collectionName: string): Promise<Record<string, unknown>[]> {\n\t\tconst accessor = this.store.collection(collectionName)\n\t\treturn accessor.where({}).exec()\n\t}\n\n\t/**\n\t * Get the device's node ID.\n\t */\n\tgetNodeId(): string {\n\t\treturn this.store.getNodeId()\n\t}\n\n\t/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */\n\tgetSyncEngine(): SyncEngine | null {\n\t\treturn this.syncEngine\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tif (this.unsubscribeAudit) {\n\t\t\tthis.unsubscribeAudit()\n\t\t\tthis.unsubscribeAudit = null\n\t\t}\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\n\n/**\n * In-memory test server wrapping KoraSyncServer with MemoryServerStore.\n * Handles client connections via memory transports.\n */\nexport interface TestServerOptions {\n\t/** Handshake schema version advertised by the server. Defaults to `schema.version`. */\n\tschemaVersion?: number\n\t/** Inclusive client schema versions accepted at handshake. */\n\tsupportedSchemaVersions?: { min: number; max: number }\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition, options?: TestServerOptions) {\n\t\tthis.store = new MemoryServerStore()\n\t\tconst schemaVersion = options?.schemaVersion ?? schema.version\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion,\n\t\t\tsupportedSchemaVersions: options?.supportedSchemaVersions ?? {\n\t\t\t\tmin: schemaVersion,\n\t\t\t\tmax: schemaVersion,\n\t\t\t},\n\t\t})\n\t\tvoid this.store.setSchema(schema)\n\t}\n\n\t/**\n\t * Register a client connection transport with the server.\n\t * Returns the session ID assigned by the server.\n\t */\n\thandleConnection(transport: ServerTransport): string {\n\t\treturn this.syncServer.handleConnection(transport)\n\t}\n\n\t/**\n\t * Get all operations stored on the server.\n\t */\n\tgetAllOperations(): Operation[] {\n\t\treturn this.store.getAllOperations()\n\t}\n\n\t/**\n\t * Get the number of connected clients.\n\t */\n\tgetConnectionCount(): number {\n\t\treturn this.syncServer.getConnectionCount()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { MessageSerializer, SyncMessage, SyncTransport, TransportOptions } from '@korajs/sync'\nimport { ProtobufMessageSerializer } from '@korajs/sync'\n\n/**\n * A linked client/server transport pair, as consumed by {@link TestDevice}.\n */\nexport interface TransportPair {\n\tclient: SyncTransport\n\tserverTransport: ServerTransport\n}\n\n/**\n * Message types the protobuf serializer models. Ephemeral messages\n * (awareness-update, yjs-doc-update) are JSON-only in the protocol, so they are\n * deep-copied instead of forced through the protobuf envelope.\n */\nconst PROTOBUF_WIRE_TYPES: ReadonlySet<SyncMessage['type']> = new Set([\n\t'handshake',\n\t'handshake-response',\n\t'operation-batch',\n\t'acknowledgment',\n\t'error',\n])\n\n/**\n * Round-trips a message through the real protobuf wire codec: object → protobuf\n * bytes → object. This is what makes a transport pair exercise the true wire\n * format rather than passing live object references between the two ends.\n */\nfunction roundTripThroughProtobuf(\n\tmessage: SyncMessage,\n\tserializer: MessageSerializer,\n): SyncMessage {\n\tif (!PROTOBUF_WIRE_TYPES.has(message.type)) {\n\t\t// Not part of the protobuf schema — still copy so no live reference leaks\n\t\t// across the \"wire\".\n\t\treturn structuredClone(message)\n\t}\n\n\tconst encoded = serializer.encode(message)\n\tif (!(encoded instanceof Uint8Array)) {\n\t\tthrow new Error('Expected the protobuf serializer to encode to bytes')\n\t}\n\treturn serializer.decode(encoded)\n}\n\n/**\n * Wraps a client transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireClientTransport implements SyncTransport {\n\tconstructor(\n\t\tprivate readonly inner: SyncTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tconnect(url: string, options?: TransportOptions): Promise<void> {\n\t\treturn this.inner.connect(url, options)\n\t}\n\n\tdisconnect(): Promise<void> {\n\t\treturn this.inner.disconnect()\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n}\n\n/**\n * Wraps a server transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so that every sync message exchanged between the two\n * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for\n * a real network hop that uses the protobuf wire format.\n *\n * Both directions are wrapped, so operations, handshakes, batches, and acks all\n * travel as bytes rather than shared object references.\n *\n * @param pair - The underlying (in-memory) transport pair to wrap\n * @returns A transport pair that routes messages through the protobuf codec\n */\nexport function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair {\n\tconst serializer = new ProtobufMessageSerializer()\n\treturn {\n\t\tclient: new ProtobufWireClientTransport(pair.client, serializer),\n\t\tserverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer),\n\t}\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { SyncMessage } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\n\n/**\n * Wraps a server transport so its handshake responses advertise a caller-chosen\n * wall-clock time, independent of the process `Date.now()`.\n *\n * The sync engine measures clock skew as `serverTime - Date.now()` at handshake.\n * To simulate a device whose clock is fast while the server's clock is correct —\n * both running in one test process — the client mocks `Date.now()` and this\n * wrapper injects the server's true time into the handshake response, so the\n * skew the engine sees is real.\n */\nclass ServerClockServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serverNow: () => number,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tif (message.type === 'handshake-response') {\n\t\t\tthis.inner.send({ ...message, serverTime: this.serverNow() })\n\t\t\treturn\n\t\t}\n\t\tthis.inner.send(message)\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so the server side reports `serverNow()` as its\n * handshake wall-clock time.\n *\n * @param pair - The underlying transport pair\n * @param serverNow - Returns the server's true current time (ms since epoch)\n * @returns A transport pair whose handshake responses carry the injected time\n */\nexport function wrapTransportPairWithServerClock(\n\tpair: TransportPair,\n\tserverNow: () => number,\n): TransportPair {\n\treturn {\n\t\tclient: pair.client,\n\t\tserverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow),\n\t}\n}\n"],"mappings":"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","names":["import_internal","import_sync","import_server","import_sync","import_sync"]}
package/dist/index.d.cts CHANGED
@@ -2,9 +2,29 @@ import { SchemaDefinition, Operation, KoraEventEmitter, OperationTransform, Vers
2
2
  import { SyncTransport, SyncEngine, ChaosConfig } from '@korajs/sync';
3
3
  export { ChaosConfig, ChaosTransport } from '@korajs/sync';
4
4
  import * as _korajs_server from '@korajs/server';
5
- import { MemoryServerStore, ServerTransport } from '@korajs/server';
5
+ import { ServerTransport, MemoryServerStore } from '@korajs/server';
6
6
  import { Store, CollectionAccessor } from '@korajs/store';
7
7
 
8
+ /**
9
+ * A linked client/server transport pair, as consumed by {@link TestDevice}.
10
+ */
11
+ interface TransportPair {
12
+ client: SyncTransport;
13
+ serverTransport: ServerTransport;
14
+ }
15
+ /**
16
+ * Wraps a transport pair so that every sync message exchanged between the two
17
+ * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for
18
+ * a real network hop that uses the protobuf wire format.
19
+ *
20
+ * Both directions are wrapped, so operations, handshakes, batches, and acks all
21
+ * travel as bytes rather than shared object references.
22
+ *
23
+ * @param pair - The underlying (in-memory) transport pair to wrap
24
+ * @returns A transport pair that routes messages through the protobuf codec
25
+ */
26
+ declare function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair;
27
+
8
28
  /**
9
29
  * In-memory test server wrapping KoraSyncServer with MemoryServerStore.
10
30
  * Handles client connections via memory transports.
@@ -86,6 +106,7 @@ declare class TestDevice {
86
106
  private syncEngine;
87
107
  private currentTransport;
88
108
  private unsubscribeSync;
109
+ private unsubscribeAudit;
89
110
  private closing;
90
111
  constructor(options: TestDeviceOptions);
91
112
  /**
@@ -152,6 +173,14 @@ interface TestNetworkOptions {
152
173
  deviceNames?: string[];
153
174
  /** Optional chaos transport settings applied to each device link. */
154
175
  chaos?: ChaosConfig;
176
+ /**
177
+ * Optional wrapper applied to each device's transport pair after the base
178
+ * (in-memory, optionally chaos-wrapped) pair is created. Use it to route
179
+ * messages through the protobuf wire codec
180
+ * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable
181
+ * server clock. Called once per device connection.
182
+ */
183
+ wrapTransport?: (pair: TransportPair) => TransportPair;
155
184
  }
156
185
  /**
157
186
  * A test network with a server and multiple devices.
@@ -273,4 +302,14 @@ declare function expectConvergedEventually(devices: TestDevice[], schema: Schema
273
302
  */
274
303
  declare function checkConvergence(devices: TestDevice[], schema: SchemaDefinition): Promise<ConvergenceResult>;
275
304
 
276
- export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually };
305
+ /**
306
+ * Wraps a transport pair so the server side reports `serverNow()` as its
307
+ * handshake wall-clock time.
308
+ *
309
+ * @param pair - The underlying transport pair
310
+ * @param serverNow - Returns the server's true current time (ms since epoch)
311
+ * @returns A transport pair whose handshake responses carry the injected time
312
+ */
313
+ declare function wrapTransportPairWithServerClock(pair: TransportPair, serverNow: () => number): TransportPair;
314
+
315
+ export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, type TransportPair, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually, wrapTransportPairWithProtobufWire, wrapTransportPairWithServerClock };
package/dist/index.d.ts CHANGED
@@ -2,9 +2,29 @@ import { SchemaDefinition, Operation, KoraEventEmitter, OperationTransform, Vers
2
2
  import { SyncTransport, SyncEngine, ChaosConfig } from '@korajs/sync';
3
3
  export { ChaosConfig, ChaosTransport } from '@korajs/sync';
4
4
  import * as _korajs_server from '@korajs/server';
5
- import { MemoryServerStore, ServerTransport } from '@korajs/server';
5
+ import { ServerTransport, MemoryServerStore } from '@korajs/server';
6
6
  import { Store, CollectionAccessor } from '@korajs/store';
7
7
 
8
+ /**
9
+ * A linked client/server transport pair, as consumed by {@link TestDevice}.
10
+ */
11
+ interface TransportPair {
12
+ client: SyncTransport;
13
+ serverTransport: ServerTransport;
14
+ }
15
+ /**
16
+ * Wraps a transport pair so that every sync message exchanged between the two
17
+ * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for
18
+ * a real network hop that uses the protobuf wire format.
19
+ *
20
+ * Both directions are wrapped, so operations, handshakes, batches, and acks all
21
+ * travel as bytes rather than shared object references.
22
+ *
23
+ * @param pair - The underlying (in-memory) transport pair to wrap
24
+ * @returns A transport pair that routes messages through the protobuf codec
25
+ */
26
+ declare function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair;
27
+
8
28
  /**
9
29
  * In-memory test server wrapping KoraSyncServer with MemoryServerStore.
10
30
  * Handles client connections via memory transports.
@@ -86,6 +106,7 @@ declare class TestDevice {
86
106
  private syncEngine;
87
107
  private currentTransport;
88
108
  private unsubscribeSync;
109
+ private unsubscribeAudit;
89
110
  private closing;
90
111
  constructor(options: TestDeviceOptions);
91
112
  /**
@@ -152,6 +173,14 @@ interface TestNetworkOptions {
152
173
  deviceNames?: string[];
153
174
  /** Optional chaos transport settings applied to each device link. */
154
175
  chaos?: ChaosConfig;
176
+ /**
177
+ * Optional wrapper applied to each device's transport pair after the base
178
+ * (in-memory, optionally chaos-wrapped) pair is created. Use it to route
179
+ * messages through the protobuf wire codec
180
+ * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable
181
+ * server clock. Called once per device connection.
182
+ */
183
+ wrapTransport?: (pair: TransportPair) => TransportPair;
155
184
  }
156
185
  /**
157
186
  * A test network with a server and multiple devices.
@@ -273,4 +302,14 @@ declare function expectConvergedEventually(devices: TestDevice[], schema: Schema
273
302
  */
274
303
  declare function checkConvergence(devices: TestDevice[], schema: SchemaDefinition): Promise<ConvergenceResult>;
275
304
 
276
- export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually };
305
+ /**
306
+ * Wraps a transport pair so the server side reports `serverNow()` as its
307
+ * handshake wall-clock time.
308
+ *
309
+ * @param pair - The underlying transport pair
310
+ * @param serverNow - Returns the server's true current time (ms since epoch)
311
+ * @returns A transport pair whose handshake responses carry the injected time
312
+ */
313
+ declare function wrapTransportPairWithServerClock(pair: TransportPair, serverNow: () => number): TransportPair;
314
+
315
+ export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, type TransportPair, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually, wrapTransportPairWithProtobufWire, wrapTransportPairWithServerClock };
package/dist/index.js CHANGED
@@ -15,7 +15,8 @@ import {
15
15
  ApplyPipeline,
16
16
  MergeAwareSyncStore,
17
17
  StoreQueueStorage,
18
- StoreSyncStatePersistence
18
+ StoreSyncStatePersistence,
19
+ wireAuditPersistence
19
20
  } from "korajs/testing";
20
21
  var TestDevice = class {
21
22
  name;
@@ -33,6 +34,7 @@ var TestDevice = class {
33
34
  syncEngine = null;
34
35
  currentTransport = null;
35
36
  unsubscribeSync = null;
37
+ unsubscribeAudit = null;
36
38
  closing = false;
37
39
  constructor(options) {
38
40
  this.name = options.name;
@@ -62,6 +64,7 @@ var TestDevice = class {
62
64
  emitter: this.emitter
63
65
  });
64
66
  this.store.setLocalMutationHandler(this.applyPipeline);
67
+ this.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter);
65
68
  }
66
69
  /**
67
70
  * Connect to the test server and perform initial sync.
@@ -163,6 +166,10 @@ var TestDevice = class {
163
166
  */
164
167
  async close() {
165
168
  this.closing = true;
169
+ if (this.unsubscribeAudit) {
170
+ this.unsubscribeAudit();
171
+ this.unsubscribeAudit = null;
172
+ }
166
173
  await this.disconnect();
167
174
  await this.store.close();
168
175
  this.emitter.clear();
@@ -254,10 +261,11 @@ async function createTestNetwork(schema, options) {
254
261
  const pair = createServerTransportPair();
255
262
  const client = pair.client;
256
263
  const chaos = options?.chaos;
257
- return {
264
+ const base = {
258
265
  client: chaos ? new ChaosTransport(client, chaos) : client,
259
266
  serverTransport: pair.server
260
267
  };
268
+ return options?.wrapTransport ? options.wrapTransport(base) : base;
261
269
  },
262
270
  tmpDir
263
271
  });
@@ -451,6 +459,126 @@ function deepEqual(a, b) {
451
459
  return false;
452
460
  }
453
461
 
462
+ // src/protobuf-wire-transport.ts
463
+ import { ProtobufMessageSerializer } from "@korajs/sync";
464
+ var PROTOBUF_WIRE_TYPES = /* @__PURE__ */ new Set([
465
+ "handshake",
466
+ "handshake-response",
467
+ "operation-batch",
468
+ "acknowledgment",
469
+ "error"
470
+ ]);
471
+ function roundTripThroughProtobuf(message, serializer) {
472
+ if (!PROTOBUF_WIRE_TYPES.has(message.type)) {
473
+ return structuredClone(message);
474
+ }
475
+ const encoded = serializer.encode(message);
476
+ if (!(encoded instanceof Uint8Array)) {
477
+ throw new Error("Expected the protobuf serializer to encode to bytes");
478
+ }
479
+ return serializer.decode(encoded);
480
+ }
481
+ var ProtobufWireClientTransport = class {
482
+ constructor(inner, serializer) {
483
+ this.inner = inner;
484
+ this.serializer = serializer;
485
+ }
486
+ inner;
487
+ serializer;
488
+ connect(url, options) {
489
+ return this.inner.connect(url, options);
490
+ }
491
+ disconnect() {
492
+ return this.inner.disconnect();
493
+ }
494
+ send(message) {
495
+ this.inner.send(roundTripThroughProtobuf(message, this.serializer));
496
+ }
497
+ onMessage(handler) {
498
+ this.inner.onMessage(handler);
499
+ }
500
+ onClose(handler) {
501
+ this.inner.onClose(handler);
502
+ }
503
+ onError(handler) {
504
+ this.inner.onError(handler);
505
+ }
506
+ isConnected() {
507
+ return this.inner.isConnected();
508
+ }
509
+ };
510
+ var ProtobufWireServerTransport = class {
511
+ constructor(inner, serializer) {
512
+ this.inner = inner;
513
+ this.serializer = serializer;
514
+ }
515
+ inner;
516
+ serializer;
517
+ send(message) {
518
+ this.inner.send(roundTripThroughProtobuf(message, this.serializer));
519
+ }
520
+ onMessage(handler) {
521
+ this.inner.onMessage(handler);
522
+ }
523
+ onClose(handler) {
524
+ this.inner.onClose(handler);
525
+ }
526
+ onError(handler) {
527
+ this.inner.onError(handler);
528
+ }
529
+ isConnected() {
530
+ return this.inner.isConnected();
531
+ }
532
+ close(code, reason) {
533
+ this.inner.close(code, reason);
534
+ }
535
+ };
536
+ function wrapTransportPairWithProtobufWire(pair) {
537
+ const serializer = new ProtobufMessageSerializer();
538
+ return {
539
+ client: new ProtobufWireClientTransport(pair.client, serializer),
540
+ serverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer)
541
+ };
542
+ }
543
+
544
+ // src/server-clock-transport.ts
545
+ var ServerClockServerTransport = class {
546
+ constructor(inner, serverNow) {
547
+ this.inner = inner;
548
+ this.serverNow = serverNow;
549
+ }
550
+ inner;
551
+ serverNow;
552
+ send(message) {
553
+ if (message.type === "handshake-response") {
554
+ this.inner.send({ ...message, serverTime: this.serverNow() });
555
+ return;
556
+ }
557
+ this.inner.send(message);
558
+ }
559
+ onMessage(handler) {
560
+ this.inner.onMessage(handler);
561
+ }
562
+ onClose(handler) {
563
+ this.inner.onClose(handler);
564
+ }
565
+ onError(handler) {
566
+ this.inner.onError(handler);
567
+ }
568
+ isConnected() {
569
+ return this.inner.isConnected();
570
+ }
571
+ close(code, reason) {
572
+ this.inner.close(code, reason);
573
+ }
574
+ };
575
+ function wrapTransportPairWithServerClock(pair, serverNow) {
576
+ return {
577
+ client: pair.client,
578
+ serverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow)
579
+ };
580
+ }
581
+
454
582
  // src/index.ts
455
583
  import { ChaosTransport as ChaosTransport2 } from "@korajs/sync";
456
584
  export {
@@ -461,6 +589,8 @@ export {
461
589
  createMixedTestNetwork,
462
590
  createTestNetwork,
463
591
  expectConverged,
464
- expectConvergedEventually
592
+ expectConvergedEventually,
593
+ wrapTransportPairWithProtobufWire,
594
+ wrapTransportPairWithServerClock
465
595
  };
466
596
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/test-network.ts","../src/test-device.ts","../src/test-server.ts","../src/assertions.ts","../src/index.ts"],"sourcesContent":["import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n}\n\n/**\n * A test network with a server and multiple devices.\n */\nexport interface TestNetwork {\n\t/** The test server */\n\tserver: TestServer\n\t/** All devices in the network */\n\tdevices: TestDevice[]\n\t/** Temporary directory for DB files */\n\ttmpDir: string\n\t/** Close all devices and the server, clean up temp files */\n\tclose(): Promise<void>\n}\n\n/**\n * Create a test network with a server and multiple virtual devices.\n *\n * Each device has its own local SQLite store and SyncEngine. Devices\n * communicate with the server via in-memory transports.\n *\n * @param schema - The schema all devices share\n * @param options - Network configuration\n * @returns A test network with server and devices ready for use\n *\n * @example\n * ```typescript\n * const network = await createTestNetwork(schema, { devices: 2 })\n * const [deviceA, deviceB] = network.devices\n *\n * await deviceA.collection('todos').insert({ title: 'Hello' })\n * await deviceA.sync()\n * await deviceB.sync()\n *\n * const todos = await deviceB.getState('todos')\n * expect(todos).toHaveLength(1)\n *\n * await network.close()\n * ```\n */\nexport async function createTestNetwork(\n\tschema: SchemaDefinition,\n\toptions?: TestNetworkOptions,\n): Promise<TestNetwork> {\n\tconst deviceCount = options?.devices ?? 2\n\tconst deviceNames =\n\t\toptions?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`)\n\n\t// Create temp directory for DB files\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\n\t// Create server\n\tconst server = new TestServer(schema)\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\treturn {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport { Store } from '@korajs/store'\nimport type { CollectionAccessor, StorageAdapter } from '@korajs/store'\nimport { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\nimport { SyncEngine } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport {\n\tApplyPipeline,\n\tMergeAwareSyncStore,\n\tStoreQueueStorage,\n\tStoreSyncStatePersistence,\n} from 'korajs/testing'\nimport type { TestServer } from './test-server'\n\n/**\n * Options for creating a TestDevice.\n */\nexport interface TestDeviceOptions {\n\t/** Unique device name (used for DB file naming) */\n\tname: string\n\t/** Schema definition */\n\tschema: SchemaDefinition\n\t/** Test server to connect to */\n\tserver: TestServer\n\t/** Transport factory — creates a linked client/server transport pair */\n\tcreateTransportPair: () => {\n\t\tclient: SyncTransport\n\t\tserverTransport: import('@korajs/server').ServerTransport\n\t}\n\t/** Optional directory for temp DB files */\n\ttmpDir: string\n\t/** Client handshake schema version. Defaults to `schema.version`. */\n\tsyncSchemaVersion?: number\n\t/** Transforms applied to inbound operations before local apply. */\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * A virtual device in a test network.\n * Each device has its own Store (with real SQLite), SyncEngine, and MergeEngine.\n * Provides high-level methods for syncing, disconnecting, and inspecting state.\n */\nexport class TestDevice {\n\treadonly name: string\n\treadonly store: Store\n\treadonly emitter: KoraEventEmitter & { clear(): void }\n\n\tprivate readonly schema: SchemaDefinition\n\tprivate readonly server: TestServer\n\tprivate readonly mergeEngine: MergeEngine\n\tprivate readonly createTransportPair: TestDeviceOptions['createTransportPair']\n\tprivate readonly adapter: StorageAdapter\n\tprivate readonly dbPath: string\n\tprivate readonly syncSchemaVersion: number\n\tprivate readonly operationTransforms: OperationTransform[]\n\n\tprivate applyPipeline: ApplyPipeline | null = null\n\tprivate syncEngine: SyncEngine | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Wire local mutations to sync outbound queue\n\t\tconst engine = this.syncEngine\n\t\tthis.unsubscribeSync = this.emitter.on('operation:created', (event) => {\n\t\t\tif (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {\n\t\t\t\t// Catch async errors from push racing with disconnect during teardown\n\t\t\t\tthis.syncEngine.pushOperation(event.operation).catch(() => {})\n\t\t\t}\n\t\t})\n\n\t\t// Register server-side connection\n\t\tthis.server.handleConnection(serverTransport)\n\n\t\t// Start sync engine (connects, handshakes, exchanges deltas)\n\t\tawait this.syncEngine.start()\n\n\t\t// Wait for sync messages to propagate (in-memory transport is synchronous\n\t\t// but some processing is async)\n\t\tawait this.waitForSettled()\n\t}\n\n\t/**\n\t * Disconnect from the test server.\n\t */\n\tasync disconnect(): Promise<void> {\n\t\tif (this.unsubscribeSync) {\n\t\t\tthis.unsubscribeSync()\n\t\t\tthis.unsubscribeSync = null\n\t\t}\n\t\tif (this.syncEngine) {\n\t\t\tawait this.syncEngine.stop()\n\t\t\tthis.syncEngine = null\n\t\t}\n\t\tthis.currentTransport = null\n\t}\n\n\t/**\n\t * Reconnect to the test server after a disconnect.\n\t */\n\tasync reconnect(): Promise<void> {\n\t\tawait this.sync()\n\t}\n\n\t/**\n\t * Get a collection accessor for performing CRUD operations.\n\t */\n\tcollection(name: string): CollectionAccessor {\n\t\treturn this.store.collection(name)\n\t}\n\n\t/**\n\t * Get all records from a collection (convenience method).\n\t */\n\tasync getState(collectionName: string): Promise<Record<string, unknown>[]> {\n\t\tconst accessor = this.store.collection(collectionName)\n\t\treturn accessor.where({}).exec()\n\t}\n\n\t/**\n\t * Get the device's node ID.\n\t */\n\tgetNodeId(): string {\n\t\treturn this.store.getNodeId()\n\t}\n\n\t/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */\n\tgetSyncEngine(): SyncEngine | null {\n\t\treturn this.syncEngine\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\n\n/**\n * In-memory test server wrapping KoraSyncServer with MemoryServerStore.\n * Handles client connections via memory transports.\n */\nexport interface TestServerOptions {\n\t/** Handshake schema version advertised by the server. Defaults to `schema.version`. */\n\tschemaVersion?: number\n\t/** Inclusive client schema versions accepted at handshake. */\n\tsupportedSchemaVersions?: { min: number; max: number }\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition, options?: TestServerOptions) {\n\t\tthis.store = new MemoryServerStore()\n\t\tconst schemaVersion = options?.schemaVersion ?? schema.version\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion,\n\t\t\tsupportedSchemaVersions: options?.supportedSchemaVersions ?? {\n\t\t\t\tmin: schemaVersion,\n\t\t\t\tmax: schemaVersion,\n\t\t\t},\n\t\t})\n\t\tvoid this.store.setSchema(schema)\n\t}\n\n\t/**\n\t * Register a client connection transport with the server.\n\t * Returns the session ID assigned by the server.\n\t */\n\thandleConnection(transport: ServerTransport): string {\n\t\treturn this.syncServer.handleConnection(transport)\n\t}\n\n\t/**\n\t * Get all operations stored on the server.\n\t */\n\tgetAllOperations(): Operation[] {\n\t\treturn this.store.getAllOperations()\n\t}\n\n\t/**\n\t * Get the number of connected clients.\n\t */\n\tgetConnectionCount(): number {\n\t\treturn this.syncServer.getConnectionCount()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n","// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n"],"mappings":";AAAA,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,YAAY;AAErB,SAAS,iCAAiC;AAE1C,SAAS,sBAAsB;;;ACH/B,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AAEtB,SAAS,4BAA4B;AACrC,SAAS,kBAAkB;AAE3B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AA+BA,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,gBAAsC;AAAA,EACtC,aAAgC;AAAA,EAChC,mBAAyC;AAAA,EACzC,kBAAuC;AAAA,EACvC,UAAU;AAAA,EAElB,YAAY,SAA4B;AACvC,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ;AACtB,SAAK,sBAAsB,QAAQ;AACnC,SAAK,SAAS,GAAG,QAAQ,MAAM,gBAAgB,QAAQ,IAAI;AAC3D,SAAK,oBAAoB,QAAQ,qBAAqB,QAAQ,OAAO;AACrE,SAAK,sBAAsB,QAAQ,uBAAuB,CAAC;AAE3D,SAAK,UAAU,IAAI,mBAAmB;AACtC,SAAK,cAAc,IAAI,YAAY;AACnC,SAAK,UAAU,IAAI,qBAAqB,KAAK,MAAM;AACnD,SAAK,QAAQ,IAAI,MAAM;AAAA,MACtB,QAAQ,QAAQ;AAAA,MAChB,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,IACf,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC3B,UAAM,KAAK,MAAM,KAAK;AACtB,SAAK,gBAAgB,IAAI,cAAc;AAAA,MACtC,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB,SAAS,KAAK;AAAA,IACf,CAAC;AACD,SAAK,MAAM,wBAAwB,KAAK,aAAa;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAsB;AAC3B,QAAI,KAAK,cAAc,KAAK,kBAAkB,YAAY,GAAG;AAE5D,YAAM,KAAK,kBAAkB;AAC7B,YAAM,KAAK,eAAe;AAC1B;AAAA,IACD;AAGA,UAAM,EAAE,QAAQ,gBAAgB,IAAI,KAAK,oBAAoB;AAC7D,SAAK,mBAAmB;AAExB,UAAM,kBAAuC,CAAC;AAC9C,UAAM,YAAY,IAAI,oBAAoB,KAAK,OAAO,KAAK,aAAa,KAAK,SAAS;AAAA,MACrF,iBAAiB,MAAM,gBAAgB,KAAK;AAAA,IAC7C,CAAC;AAED,SAAK,aAAa,IAAI,WAAW;AAAA,MAChC,WAAW;AAAA,MACX,OAAO;AAAA,MACP,cAAc,IAAI,kBAAkB,KAAK,OAAO;AAAA,MAChD,WAAW,IAAI,0BAA0B,KAAK,KAAK;AAAA,MACnD,QAAQ;AAAA,QACP,KAAK;AAAA,QACL,eAAe,KAAK;AAAA,QACpB,qBACC,KAAK,oBAAoB,SAAS,IAAI,KAAK,sBAAsB;AAAA,MACnE;AAAA,MACA,SAAS,KAAK;AAAA,IACf,CAAC;AACD,oBAAgB,KAAK,MAAM,KAAK,YAAY,eAAe;AAG3D,UAAM,SAAS,KAAK;AACpB,SAAK,kBAAkB,KAAK,QAAQ,GAAG,qBAAqB,CAAC,UAAU;AACtE,UAAI,CAAC,KAAK,WAAW,KAAK,eAAe,UAAU,KAAK,kBAAkB,YAAY,GAAG;AAExF,aAAK,WAAW,cAAc,MAAM,SAAS,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAC9D;AAAA,IACD,CAAC;AAGD,SAAK,OAAO,iBAAiB,eAAe;AAG5C,UAAM,KAAK,WAAW,MAAM;AAI5B,UAAM,KAAK,eAAe;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAA4B;AACjC,QAAI,KAAK,iBAAiB;AACzB,WAAK,gBAAgB;AACrB,WAAK,kBAAkB;AAAA,IACxB;AACA,QAAI,KAAK,YAAY;AACpB,YAAM,KAAK,WAAW,KAAK;AAC3B,WAAK,aAAa;AAAA,IACnB;AACA,SAAK,mBAAmB;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAA2B;AAChC,UAAM,KAAK,KAAK;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAAkC;AAC5C,WAAO,KAAK,MAAM,WAAW,IAAI;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,gBAA4D;AAC1E,UAAM,WAAW,KAAK,MAAM,WAAW,cAAc;AACrD,WAAO,SAAS,MAAM,CAAC,CAAC,EAAE,KAAK;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAoB;AACnB,WAAO,KAAK,MAAM,UAAU;AAAA,EAC7B;AAAA;AAAA,EAGA,gBAAmC;AAClC,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAkC;AACjC,WAAO,KAAK,MAAM,iBAAiB;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,cAAuB;AACtB,WAAO,KAAK,kBAAkB,YAAY,KAAK;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC5B,SAAK,UAAU;AACf,UAAM,KAAK,WAAW;AACtB,UAAM,KAAK,MAAM,MAAM;AACvB,SAAK,QAAQ,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,iBAAgC;AAC7C,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC5B,YAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC7D;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAAmC;AAChD,QAAI,CAAC,KAAK,WAAY;AACtB,UAAM,UAAU;AAChB,UAAM,QAAQ,KAAK,IAAI;AACvB,WAAO,KAAK,IAAI,IAAI,QAAQ,SAAS;AACpC,YAAM,SAAS,KAAK,WAAW,UAAU;AACzC,UAAI,OAAO,sBAAsB,EAAG;AACpC,YAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC7D;AAAA,EACD;AACD;;;ACzPA,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAcxB,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACQ;AAAA,EAEjB,YAAY,QAA0B,SAA6B;AAClE,SAAK,QAAQ,IAAI,kBAAkB;AACnC,UAAM,gBAAgB,SAAS,iBAAiB,OAAO;AACvD,SAAK,aAAa,IAAI,eAAe;AAAA,MACpC,OAAO,KAAK;AAAA,MACZ;AAAA,MACA,yBAAyB,SAAS,2BAA2B;AAAA,QAC5D,KAAK;AAAA,QACL,KAAK;AAAA,MACN;AAAA,IACD,CAAC;AACD,SAAK,KAAK,MAAM,UAAU,MAAM;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB,WAAoC;AACpD,WAAO,KAAK,WAAW,iBAAiB,SAAS;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAgC;AAC/B,WAAO,KAAK,MAAM,iBAAiB;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,qBAA6B;AAC5B,WAAO,KAAK,WAAW,mBAAmB;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC5B,UAAM,KAAK,WAAW,KAAK;AAC3B,UAAM,KAAK,MAAM,MAAM;AAAA,EACxB;AACD;;;AFDA,eAAsB,kBACrB,QACA,SACuB;AACvB,QAAM,cAAc,SAAS,WAAW;AACxC,QAAM,cACL,SAAS,eAAe,MAAM,KAAK,EAAE,QAAQ,YAAY,GAAG,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE;AAGpF,QAAM,SAAS,YAAY,KAAK,OAAO,GAAG,YAAY,CAAC;AAGvD,QAAM,SAAS,IAAI,WAAW,MAAM;AAGpC,QAAM,UAAwB,CAAC;AAC/B,aAAW,QAAQ,aAAa;AAC/B,UAAM,SAAS,IAAI,WAAW;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA,qBAAqB,MAAM;AAC1B,cAAM,OAAO,0BAA0B;AACvC,cAAM,SAAS,KAAK;AACpB,cAAM,QAAQ,SAAS;AACvB,eAAO;AAAA,UACN,QAAQ,QAAQ,IAAI,eAAe,QAAQ,KAAK,IAAI;AAAA,UACpD,iBAAiB,KAAK;AAAA,QACvB;AAAA,MACD;AAAA,MACA;AAAA,IACD,CAAC;AACD,UAAM,OAAO,KAAK;AAClB,YAAQ,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,QAAuB;AAC5B,iBAAW,UAAU,SAAS;AAC7B,cAAM,OAAO,MAAM;AAAA,MACpB;AACA,YAAM,OAAO,MAAM;AAEnB,UAAI;AACH,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,IAAS;AACzC,eAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MAChD,QAAQ;AAAA,MAER;AAAA,IACD;AAAA,EACD;AACD;AAgBA,eAAsB,uBACrB,cACA,eACA,eACuB;AACvB,QAAM,SAAS,YAAY,KAAK,OAAO,GAAG,YAAY,CAAC;AACvD,QAAM,SAAS,IAAI,WAAW,cAAc,aAAa;AAEzD,QAAM,UAAwB,CAAC;AAC/B,aAAW,UAAU,eAAe;AACnC,UAAM,SAAS,IAAI,WAAW;AAAA,MAC7B,MAAM,OAAO;AAAA,MACb,QAAQ,OAAO;AAAA,MACf;AAAA,MACA,mBAAmB,OAAO;AAAA,MAC1B,qBAAqB,OAAO;AAAA,MAC5B,qBAAqB,MAAM;AAC1B,cAAM,OAAO,0BAA0B;AACvC,eAAO;AAAA,UACN,QAAQ,KAAK;AAAA,UACb,iBAAiB,KAAK;AAAA,QACvB;AAAA,MACD;AAAA,MACA;AAAA,IACD,CAAC;AACD,UAAM,OAAO,KAAK;AAClB,YAAQ,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,QAAuB;AAC5B,iBAAW,UAAU,SAAS;AAC7B,cAAM,OAAO,MAAM;AAAA,MACpB;AACA,YAAM,OAAO,MAAM;AACnB,UAAI;AACH,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,IAAS;AACzC,eAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MAChD,QAAQ;AAAA,MAER;AAAA,IACD;AAAA,EACD;AACD;;;AG5HA,eAAsB,gBACrB,SACA,QACgB;AAChB,MAAI,QAAQ,SAAS,EAAG;AAExB,QAAM,SAAS,MAAM,iBAAiB,SAAS,MAAM;AACrD,MAAI,CAAC,OAAO,WAAW;AACtB,UAAM,UAAU,OAAO,YACrB,IAAI,CAAC,MAAM;AACX,YAAM,QAAkB;AAAA,QACvB,iBAAiB,EAAE,UAAU,qBAAqB,EAAE,OAAO,QAAQ,EAAE,OAAO;AAAA,MAC7E;AACA,UAAI,EAAE,WAAW,SAAS,GAAG;AAC5B,cAAM,KAAK,kBAAkB,EAAE,OAAO,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC,EAAE;AAAA,MACrE;AACA,UAAI,EAAE,WAAW,SAAS,GAAG;AAC5B,cAAM,KAAK,kBAAkB,EAAE,OAAO,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC,EAAE;AAAA,MACrE;AACA,iBAAW,MAAM,EAAE,kBAAkB;AACpC,cAAM;AAAA,UACL,eAAe,GAAG,QAAQ,YAAY,GAAG,KAAK,MAAM,KAAK,UAAU,GAAG,QAAQ,CAAC,OAAO,KAAK,UAAU,GAAG,QAAQ,CAAC;AAAA,QAClH;AAAA,MACD;AACA,aAAO,MAAM,KAAK,IAAI;AAAA,IACvB,CAAC,EACA,KAAK,IAAI;AAEX,UAAM,IAAI,MAAM;AAAA,EAAgC,OAAO,EAAE;AAAA,EAC1D;AACD;AAMA,eAAsB,0BACrB,SACA,QACA,SACgB;AAChB,MAAI,QAAQ,SAAS,EAAG;AAExB,QAAM,YAAY,SAAS,aAAa;AACxC,QAAM,aAAa,SAAS,cAAc;AAC1C,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC7B,UAAM,SAAS,MAAM,iBAAiB,SAAS,MAAM;AACrD,QAAI,OAAO,WAAW;AACrB;AAAA,IACD;AACA,UAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,UAAU,CAAC;AAAA,EACrE;AAEA,QAAM,gBAAgB,SAAS,MAAM;AACtC;AASA,eAAsB,iBACrB,SACA,QAC6B;AAC7B,QAAM,cAAsC,CAAC;AAC7C,QAAM,kBAAkB,OAAO,KAAK,OAAO,WAAW;AAEtD,WAAS,IAAI,GAAG,IAAI,QAAQ,SAAS,GAAG,KAAK;AAC5C,aAAS,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AAC5C,YAAM,UAAU,QAAQ,CAAC;AACzB,YAAM,UAAU,QAAQ,CAAC;AAEzB,iBAAW,cAAc,iBAAiB;AACzC,cAAM,SAAS,MAAM,QAAQ,SAAS,UAAU;AAChD,cAAM,SAAS,MAAM,QAAQ,SAAS,UAAU;AAEhD,cAAM,OAAO,wBAAwB,YAAY,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM;AAE3F,YAAI,MAAM;AACT,sBAAY,KAAK,IAAI;AAAA,QACtB;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,WAAW,YAAY,WAAW;AAAA,IAClC;AAAA,EACD;AACD;AAKA,SAAS,wBACR,YACA,OACA,OACA,QACA,QAC8B;AAC9B,QAAM,OAAO,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,IAAc,CAAC,CAAC,CAAC;AAC3D,QAAM,OAAO,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,IAAc,CAAC,CAAC,CAAC;AAE3D,QAAM,aAAuB,CAAC;AAC9B,QAAM,aAAuB,CAAC;AAC9B,QAAM,mBAAsC,CAAC;AAG7C,aAAW,CAAC,IAAI,OAAO,KAAK,MAAM;AACjC,UAAM,UAAU,KAAK,IAAI,EAAE;AAC3B,QAAI,CAAC,SAAS;AACb,iBAAW,KAAK,EAAE;AAClB;AAAA,IACD;AAGA,UAAM,YAAY,oBAAI,IAAI;AAAA,MACzB,GAAG,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AAAA,MACxD,GAAG,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AAAA,IACzD,CAAC;AAED,eAAW,SAAS,WAAW;AAC9B,UAAI,UAAU,KAAM;AACpB,YAAM,OAAO,QAAQ,KAAK;AAC1B,YAAM,OAAO,QAAQ,KAAK;AAC1B,UAAI,CAAC,UAAU,MAAM,IAAI,GAAG;AAC3B,yBAAiB,KAAK;AAAA,UACrB,UAAU;AAAA,UACV;AAAA,UACA,UAAU;AAAA,UACV,UAAU;AAAA,QACX,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AAGA,aAAW,MAAM,KAAK,KAAK,GAAG;AAC7B,QAAI,CAAC,KAAK,IAAI,EAAE,GAAG;AAClB,iBAAW,KAAK,EAAE;AAAA,IACnB;AAAA,EACD;AAEA,MAAI,WAAW,WAAW,KAAK,WAAW,WAAW,KAAK,iBAAiB,WAAW,GAAG;AACxF,WAAO;AAAA,EACR;AAEA,SAAO;AAAA,IACN;AAAA,IACA,SAAS;AAAA,IACT,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACD;AAKA,SAAS,UAAU,GAAY,GAAqB;AACnD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,MAAM,QAAQ,MAAM,KAAM,QAAO;AACrC,MAAI,MAAM,UAAa,MAAM,OAAW,QAAO;AAC/C,MAAI,OAAO,MAAM,OAAO,EAAG,QAAO;AAElC,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACzC,QAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAO,EAAE,MAAM,CAAC,KAAK,MAAM,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC;AAAA,EAChD;AAEA,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AACnD,UAAM,QAAQ,OAAO,KAAK,CAA4B;AACtD,UAAM,QAAQ,OAAO,KAAK,CAA4B;AACtD,QAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1C,WAAO,MAAM;AAAA,MAAM,CAAC,QACnB,UAAW,EAA8B,GAAG,GAAI,EAA8B,GAAG,CAAC;AAAA,IACnF;AAAA,EACD;AAEA,SAAO;AACR;;;ACrNA,SAAS,kBAAAA,uBAAsB;","names":["ChaosTransport"]}
1
+ {"version":3,"sources":["../src/test-network.ts","../src/test-device.ts","../src/test-server.ts","../src/assertions.ts","../src/protobuf-wire-transport.ts","../src/server-clock-transport.ts","../src/index.ts"],"sourcesContent":["import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n\t/**\n\t * Optional wrapper applied to each device's transport pair after the base\n\t * (in-memory, optionally chaos-wrapped) pair is created. Use it to route\n\t * messages through the protobuf wire codec\n\t * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable\n\t * server clock. Called once per device connection.\n\t */\n\twrapTransport?: (pair: TransportPair) => TransportPair\n}\n\n/**\n * A test network with a server and multiple devices.\n */\nexport interface TestNetwork {\n\t/** The test server */\n\tserver: TestServer\n\t/** All devices in the network */\n\tdevices: TestDevice[]\n\t/** Temporary directory for DB files */\n\ttmpDir: string\n\t/** Close all devices and the server, clean up temp files */\n\tclose(): Promise<void>\n}\n\n/**\n * Create a test network with a server and multiple virtual devices.\n *\n * Each device has its own local SQLite store and SyncEngine. Devices\n * communicate with the server via in-memory transports.\n *\n * @param schema - The schema all devices share\n * @param options - Network configuration\n * @returns A test network with server and devices ready for use\n *\n * @example\n * ```typescript\n * const network = await createTestNetwork(schema, { devices: 2 })\n * const [deviceA, deviceB] = network.devices\n *\n * await deviceA.collection('todos').insert({ title: 'Hello' })\n * await deviceA.sync()\n * await deviceB.sync()\n *\n * const todos = await deviceB.getState('todos')\n * expect(todos).toHaveLength(1)\n *\n * await network.close()\n * ```\n */\nexport async function createTestNetwork(\n\tschema: SchemaDefinition,\n\toptions?: TestNetworkOptions,\n): Promise<TestNetwork> {\n\tconst deviceCount = options?.devices ?? 2\n\tconst deviceNames =\n\t\toptions?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`)\n\n\t// Create temp directory for DB files\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\n\t// Create server\n\tconst server = new TestServer(schema)\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\tconst base: TransportPair = {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t\treturn options?.wrapTransport ? options.wrapTransport(base) : base\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport { Store } from '@korajs/store'\nimport type { CollectionAccessor, StorageAdapter } from '@korajs/store'\nimport { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\nimport { SyncEngine } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport {\n\tApplyPipeline,\n\tMergeAwareSyncStore,\n\tStoreQueueStorage,\n\tStoreSyncStatePersistence,\n\twireAuditPersistence,\n} from 'korajs/testing'\nimport type { TestServer } from './test-server'\n\n/**\n * Options for creating a TestDevice.\n */\nexport interface TestDeviceOptions {\n\t/** Unique device name (used for DB file naming) */\n\tname: string\n\t/** Schema definition */\n\tschema: SchemaDefinition\n\t/** Test server to connect to */\n\tserver: TestServer\n\t/** Transport factory — creates a linked client/server transport pair */\n\tcreateTransportPair: () => {\n\t\tclient: SyncTransport\n\t\tserverTransport: import('@korajs/server').ServerTransport\n\t}\n\t/** Optional directory for temp DB files */\n\ttmpDir: string\n\t/** Client handshake schema version. Defaults to `schema.version`. */\n\tsyncSchemaVersion?: number\n\t/** Transforms applied to inbound operations before local apply. */\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * A virtual device in a test network.\n * Each device has its own Store (with real SQLite), SyncEngine, and MergeEngine.\n * Provides high-level methods for syncing, disconnecting, and inspecting state.\n */\nexport class TestDevice {\n\treadonly name: string\n\treadonly store: Store\n\treadonly emitter: KoraEventEmitter & { clear(): void }\n\n\tprivate readonly schema: SchemaDefinition\n\tprivate readonly server: TestServer\n\tprivate readonly mergeEngine: MergeEngine\n\tprivate readonly createTransportPair: TestDeviceOptions['createTransportPair']\n\tprivate readonly adapter: StorageAdapter\n\tprivate readonly dbPath: string\n\tprivate readonly syncSchemaVersion: number\n\tprivate readonly operationTransforms: OperationTransform[]\n\n\tprivate applyPipeline: ApplyPipeline | null = null\n\tprivate syncEngine: SyncEngine | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate unsubscribeAudit: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t\t// Match production wiring (createApp): merge/constraint traces persist to\n\t\t// `_kora_audit_traces`. Without this, harness devices emit merge events\n\t\t// but the durable audit trail every real app has stays empty — a fidelity\n\t\t// gap that hid from tests until Studio's Merges view made it visible.\n\t\tthis.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Wire local mutations to sync outbound queue\n\t\tconst engine = this.syncEngine\n\t\tthis.unsubscribeSync = this.emitter.on('operation:created', (event) => {\n\t\t\tif (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {\n\t\t\t\t// Catch async errors from push racing with disconnect during teardown\n\t\t\t\tthis.syncEngine.pushOperation(event.operation).catch(() => {})\n\t\t\t}\n\t\t})\n\n\t\t// Register server-side connection\n\t\tthis.server.handleConnection(serverTransport)\n\n\t\t// Start sync engine (connects, handshakes, exchanges deltas)\n\t\tawait this.syncEngine.start()\n\n\t\t// Wait for sync messages to propagate (in-memory transport is synchronous\n\t\t// but some processing is async)\n\t\tawait this.waitForSettled()\n\t}\n\n\t/**\n\t * Disconnect from the test server.\n\t */\n\tasync disconnect(): Promise<void> {\n\t\tif (this.unsubscribeSync) {\n\t\t\tthis.unsubscribeSync()\n\t\t\tthis.unsubscribeSync = null\n\t\t}\n\t\tif (this.syncEngine) {\n\t\t\tawait this.syncEngine.stop()\n\t\t\tthis.syncEngine = null\n\t\t}\n\t\tthis.currentTransport = null\n\t}\n\n\t/**\n\t * Reconnect to the test server after a disconnect.\n\t */\n\tasync reconnect(): Promise<void> {\n\t\tawait this.sync()\n\t}\n\n\t/**\n\t * Get a collection accessor for performing CRUD operations.\n\t */\n\tcollection(name: string): CollectionAccessor {\n\t\treturn this.store.collection(name)\n\t}\n\n\t/**\n\t * Get all records from a collection (convenience method).\n\t */\n\tasync getState(collectionName: string): Promise<Record<string, unknown>[]> {\n\t\tconst accessor = this.store.collection(collectionName)\n\t\treturn accessor.where({}).exec()\n\t}\n\n\t/**\n\t * Get the device's node ID.\n\t */\n\tgetNodeId(): string {\n\t\treturn this.store.getNodeId()\n\t}\n\n\t/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */\n\tgetSyncEngine(): SyncEngine | null {\n\t\treturn this.syncEngine\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tif (this.unsubscribeAudit) {\n\t\t\tthis.unsubscribeAudit()\n\t\t\tthis.unsubscribeAudit = null\n\t\t}\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\n\n/**\n * In-memory test server wrapping KoraSyncServer with MemoryServerStore.\n * Handles client connections via memory transports.\n */\nexport interface TestServerOptions {\n\t/** Handshake schema version advertised by the server. Defaults to `schema.version`. */\n\tschemaVersion?: number\n\t/** Inclusive client schema versions accepted at handshake. */\n\tsupportedSchemaVersions?: { min: number; max: number }\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition, options?: TestServerOptions) {\n\t\tthis.store = new MemoryServerStore()\n\t\tconst schemaVersion = options?.schemaVersion ?? schema.version\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion,\n\t\t\tsupportedSchemaVersions: options?.supportedSchemaVersions ?? {\n\t\t\t\tmin: schemaVersion,\n\t\t\t\tmax: schemaVersion,\n\t\t\t},\n\t\t})\n\t\tvoid this.store.setSchema(schema)\n\t}\n\n\t/**\n\t * Register a client connection transport with the server.\n\t * Returns the session ID assigned by the server.\n\t */\n\thandleConnection(transport: ServerTransport): string {\n\t\treturn this.syncServer.handleConnection(transport)\n\t}\n\n\t/**\n\t * Get all operations stored on the server.\n\t */\n\tgetAllOperations(): Operation[] {\n\t\treturn this.store.getAllOperations()\n\t}\n\n\t/**\n\t * Get the number of connected clients.\n\t */\n\tgetConnectionCount(): number {\n\t\treturn this.syncServer.getConnectionCount()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { MessageSerializer, SyncMessage, SyncTransport, TransportOptions } from '@korajs/sync'\nimport { ProtobufMessageSerializer } from '@korajs/sync'\n\n/**\n * A linked client/server transport pair, as consumed by {@link TestDevice}.\n */\nexport interface TransportPair {\n\tclient: SyncTransport\n\tserverTransport: ServerTransport\n}\n\n/**\n * Message types the protobuf serializer models. Ephemeral messages\n * (awareness-update, yjs-doc-update) are JSON-only in the protocol, so they are\n * deep-copied instead of forced through the protobuf envelope.\n */\nconst PROTOBUF_WIRE_TYPES: ReadonlySet<SyncMessage['type']> = new Set([\n\t'handshake',\n\t'handshake-response',\n\t'operation-batch',\n\t'acknowledgment',\n\t'error',\n])\n\n/**\n * Round-trips a message through the real protobuf wire codec: object → protobuf\n * bytes → object. This is what makes a transport pair exercise the true wire\n * format rather than passing live object references between the two ends.\n */\nfunction roundTripThroughProtobuf(\n\tmessage: SyncMessage,\n\tserializer: MessageSerializer,\n): SyncMessage {\n\tif (!PROTOBUF_WIRE_TYPES.has(message.type)) {\n\t\t// Not part of the protobuf schema — still copy so no live reference leaks\n\t\t// across the \"wire\".\n\t\treturn structuredClone(message)\n\t}\n\n\tconst encoded = serializer.encode(message)\n\tif (!(encoded instanceof Uint8Array)) {\n\t\tthrow new Error('Expected the protobuf serializer to encode to bytes')\n\t}\n\treturn serializer.decode(encoded)\n}\n\n/**\n * Wraps a client transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireClientTransport implements SyncTransport {\n\tconstructor(\n\t\tprivate readonly inner: SyncTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tconnect(url: string, options?: TransportOptions): Promise<void> {\n\t\treturn this.inner.connect(url, options)\n\t}\n\n\tdisconnect(): Promise<void> {\n\t\treturn this.inner.disconnect()\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n}\n\n/**\n * Wraps a server transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so that every sync message exchanged between the two\n * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for\n * a real network hop that uses the protobuf wire format.\n *\n * Both directions are wrapped, so operations, handshakes, batches, and acks all\n * travel as bytes rather than shared object references.\n *\n * @param pair - The underlying (in-memory) transport pair to wrap\n * @returns A transport pair that routes messages through the protobuf codec\n */\nexport function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair {\n\tconst serializer = new ProtobufMessageSerializer()\n\treturn {\n\t\tclient: new ProtobufWireClientTransport(pair.client, serializer),\n\t\tserverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer),\n\t}\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { SyncMessage } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\n\n/**\n * Wraps a server transport so its handshake responses advertise a caller-chosen\n * wall-clock time, independent of the process `Date.now()`.\n *\n * The sync engine measures clock skew as `serverTime - Date.now()` at handshake.\n * To simulate a device whose clock is fast while the server's clock is correct —\n * both running in one test process — the client mocks `Date.now()` and this\n * wrapper injects the server's true time into the handshake response, so the\n * skew the engine sees is real.\n */\nclass ServerClockServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serverNow: () => number,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tif (message.type === 'handshake-response') {\n\t\t\tthis.inner.send({ ...message, serverTime: this.serverNow() })\n\t\t\treturn\n\t\t}\n\t\tthis.inner.send(message)\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so the server side reports `serverNow()` as its\n * handshake wall-clock time.\n *\n * @param pair - The underlying transport pair\n * @param serverNow - Returns the server's true current time (ms since epoch)\n * @returns A transport pair whose handshake responses carry the injected time\n */\nexport function wrapTransportPairWithServerClock(\n\tpair: TransportPair,\n\tserverNow: () => number,\n): TransportPair {\n\treturn {\n\t\tclient: pair.client,\n\t\tserverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow),\n\t}\n}\n","// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Protobuf wire transport (for testing convergence through the real wire format) ===\nexport { wrapTransportPairWithProtobufWire } from './protobuf-wire-transport'\nexport type { TransportPair } from './protobuf-wire-transport'\n\n// === Server-clock transport (for testing clock-skew / rebase integration) ===\nexport { wrapTransportPairWithServerClock } from './server-clock-transport'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n"],"mappings":";AAAA,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,YAAY;AAErB,SAAS,iCAAiC;AAE1C,SAAS,sBAAsB;;;ACH/B,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AAEtB,SAAS,4BAA4B;AACrC,SAAS,kBAAkB;AAE3B;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AA+BA,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,gBAAsC;AAAA,EACtC,aAAgC;AAAA,EAChC,mBAAyC;AAAA,EACzC,kBAAuC;AAAA,EACvC,mBAAwC;AAAA,EACxC,UAAU;AAAA,EAElB,YAAY,SAA4B;AACvC,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ;AACtB,SAAK,sBAAsB,QAAQ;AACnC,SAAK,SAAS,GAAG,QAAQ,MAAM,gBAAgB,QAAQ,IAAI;AAC3D,SAAK,oBAAoB,QAAQ,qBAAqB,QAAQ,OAAO;AACrE,SAAK,sBAAsB,QAAQ,uBAAuB,CAAC;AAE3D,SAAK,UAAU,IAAI,mBAAmB;AACtC,SAAK,cAAc,IAAI,YAAY;AACnC,SAAK,UAAU,IAAI,qBAAqB,KAAK,MAAM;AACnD,SAAK,QAAQ,IAAI,MAAM;AAAA,MACtB,QAAQ,QAAQ;AAAA,MAChB,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,IACf,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC3B,UAAM,KAAK,MAAM,KAAK;AACtB,SAAK,gBAAgB,IAAI,cAAc;AAAA,MACtC,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB,SAAS,KAAK;AAAA,IACf,CAAC;AACD,SAAK,MAAM,wBAAwB,KAAK,aAAa;AAKrD,SAAK,mBAAmB,qBAAqB,KAAK,OAAO,KAAK,OAAO;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAsB;AAC3B,QAAI,KAAK,cAAc,KAAK,kBAAkB,YAAY,GAAG;AAE5D,YAAM,KAAK,kBAAkB;AAC7B,YAAM,KAAK,eAAe;AAC1B;AAAA,IACD;AAGA,UAAM,EAAE,QAAQ,gBAAgB,IAAI,KAAK,oBAAoB;AAC7D,SAAK,mBAAmB;AAExB,UAAM,kBAAuC,CAAC;AAC9C,UAAM,YAAY,IAAI,oBAAoB,KAAK,OAAO,KAAK,aAAa,KAAK,SAAS;AAAA,MACrF,iBAAiB,MAAM,gBAAgB,KAAK;AAAA,IAC7C,CAAC;AAED,SAAK,aAAa,IAAI,WAAW;AAAA,MAChC,WAAW;AAAA,MACX,OAAO;AAAA,MACP,cAAc,IAAI,kBAAkB,KAAK,OAAO;AAAA,MAChD,WAAW,IAAI,0BAA0B,KAAK,KAAK;AAAA,MACnD,QAAQ;AAAA,QACP,KAAK;AAAA,QACL,eAAe,KAAK;AAAA,QACpB,qBACC,KAAK,oBAAoB,SAAS,IAAI,KAAK,sBAAsB;AAAA,MACnE;AAAA,MACA,SAAS,KAAK;AAAA,IACf,CAAC;AACD,oBAAgB,KAAK,MAAM,KAAK,YAAY,eAAe;AAG3D,UAAM,SAAS,KAAK;AACpB,SAAK,kBAAkB,KAAK,QAAQ,GAAG,qBAAqB,CAAC,UAAU;AACtE,UAAI,CAAC,KAAK,WAAW,KAAK,eAAe,UAAU,KAAK,kBAAkB,YAAY,GAAG;AAExF,aAAK,WAAW,cAAc,MAAM,SAAS,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAC9D;AAAA,IACD,CAAC;AAGD,SAAK,OAAO,iBAAiB,eAAe;AAG5C,UAAM,KAAK,WAAW,MAAM;AAI5B,UAAM,KAAK,eAAe;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAA4B;AACjC,QAAI,KAAK,iBAAiB;AACzB,WAAK,gBAAgB;AACrB,WAAK,kBAAkB;AAAA,IACxB;AACA,QAAI,KAAK,YAAY;AACpB,YAAM,KAAK,WAAW,KAAK;AAC3B,WAAK,aAAa;AAAA,IACnB;AACA,SAAK,mBAAmB;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAA2B;AAChC,UAAM,KAAK,KAAK;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAAkC;AAC5C,WAAO,KAAK,MAAM,WAAW,IAAI;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,gBAA4D;AAC1E,UAAM,WAAW,KAAK,MAAM,WAAW,cAAc;AACrD,WAAO,SAAS,MAAM,CAAC,CAAC,EAAE,KAAK;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAoB;AACnB,WAAO,KAAK,MAAM,UAAU;AAAA,EAC7B;AAAA;AAAA,EAGA,gBAAmC;AAClC,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAkC;AACjC,WAAO,KAAK,MAAM,iBAAiB;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,cAAuB;AACtB,WAAO,KAAK,kBAAkB,YAAY,KAAK;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC5B,SAAK,UAAU;AACf,QAAI,KAAK,kBAAkB;AAC1B,WAAK,iBAAiB;AACtB,WAAK,mBAAmB;AAAA,IACzB;AACA,UAAM,KAAK,WAAW;AACtB,UAAM,KAAK,MAAM,MAAM;AACvB,SAAK,QAAQ,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,iBAAgC;AAC7C,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC5B,YAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC7D;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAAmC;AAChD,QAAI,CAAC,KAAK,WAAY;AACtB,UAAM,UAAU;AAChB,UAAM,QAAQ,KAAK,IAAI;AACvB,WAAO,KAAK,IAAI,IAAI,QAAQ,SAAS;AACpC,YAAM,SAAS,KAAK,WAAW,UAAU;AACzC,UAAI,OAAO,sBAAsB,EAAG;AACpC,YAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,IAC7D;AAAA,EACD;AACD;;;ACpQA,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAcxB,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACQ;AAAA,EAEjB,YAAY,QAA0B,SAA6B;AAClE,SAAK,QAAQ,IAAI,kBAAkB;AACnC,UAAM,gBAAgB,SAAS,iBAAiB,OAAO;AACvD,SAAK,aAAa,IAAI,eAAe;AAAA,MACpC,OAAO,KAAK;AAAA,MACZ;AAAA,MACA,yBAAyB,SAAS,2BAA2B;AAAA,QAC5D,KAAK;AAAA,QACL,KAAK;AAAA,MACN;AAAA,IACD,CAAC;AACD,SAAK,KAAK,MAAM,UAAU,MAAM;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB,WAAoC;AACpD,WAAO,KAAK,WAAW,iBAAiB,SAAS;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAgC;AAC/B,WAAO,KAAK,MAAM,iBAAiB;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,qBAA6B;AAC5B,WAAO,KAAK,WAAW,mBAAmB;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC5B,UAAM,KAAK,WAAW,KAAK;AAC3B,UAAM,KAAK,MAAM,MAAM;AAAA,EACxB;AACD;;;AFQA,eAAsB,kBACrB,QACA,SACuB;AACvB,QAAM,cAAc,SAAS,WAAW;AACxC,QAAM,cACL,SAAS,eAAe,MAAM,KAAK,EAAE,QAAQ,YAAY,GAAG,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE;AAGpF,QAAM,SAAS,YAAY,KAAK,OAAO,GAAG,YAAY,CAAC;AAGvD,QAAM,SAAS,IAAI,WAAW,MAAM;AAGpC,QAAM,UAAwB,CAAC;AAC/B,aAAW,QAAQ,aAAa;AAC/B,UAAM,SAAS,IAAI,WAAW;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA,qBAAqB,MAAM;AAC1B,cAAM,OAAO,0BAA0B;AACvC,cAAM,SAAS,KAAK;AACpB,cAAM,QAAQ,SAAS;AACvB,cAAM,OAAsB;AAAA,UAC3B,QAAQ,QAAQ,IAAI,eAAe,QAAQ,KAAK,IAAI;AAAA,UACpD,iBAAiB,KAAK;AAAA,QACvB;AACA,eAAO,SAAS,gBAAgB,QAAQ,cAAc,IAAI,IAAI;AAAA,MAC/D;AAAA,MACA;AAAA,IACD,CAAC;AACD,UAAM,OAAO,KAAK;AAClB,YAAQ,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,QAAuB;AAC5B,iBAAW,UAAU,SAAS;AAC7B,cAAM,OAAO,MAAM;AAAA,MACpB;AACA,YAAM,OAAO,MAAM;AAEnB,UAAI;AACH,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,IAAS;AACzC,eAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MAChD,QAAQ;AAAA,MAER;AAAA,IACD;AAAA,EACD;AACD;AAgBA,eAAsB,uBACrB,cACA,eACA,eACuB;AACvB,QAAM,SAAS,YAAY,KAAK,OAAO,GAAG,YAAY,CAAC;AACvD,QAAM,SAAS,IAAI,WAAW,cAAc,aAAa;AAEzD,QAAM,UAAwB,CAAC;AAC/B,aAAW,UAAU,eAAe;AACnC,UAAM,SAAS,IAAI,WAAW;AAAA,MAC7B,MAAM,OAAO;AAAA,MACb,QAAQ,OAAO;AAAA,MACf;AAAA,MACA,mBAAmB,OAAO;AAAA,MAC1B,qBAAqB,OAAO;AAAA,MAC5B,qBAAqB,MAAM;AAC1B,cAAM,OAAO,0BAA0B;AACvC,eAAO;AAAA,UACN,QAAQ,KAAK;AAAA,UACb,iBAAiB,KAAK;AAAA,QACvB;AAAA,MACD;AAAA,MACA;AAAA,IACD,CAAC;AACD,UAAM,OAAO,KAAK;AAClB,YAAQ,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,QAAuB;AAC5B,iBAAW,UAAU,SAAS;AAC7B,cAAM,OAAO,MAAM;AAAA,MACpB;AACA,YAAM,OAAO,MAAM;AACnB,UAAI;AACH,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,IAAS;AACzC,eAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,MAChD,QAAQ;AAAA,MAER;AAAA,IACD;AAAA,EACD;AACD;;;AGtIA,eAAsB,gBACrB,SACA,QACgB;AAChB,MAAI,QAAQ,SAAS,EAAG;AAExB,QAAM,SAAS,MAAM,iBAAiB,SAAS,MAAM;AACrD,MAAI,CAAC,OAAO,WAAW;AACtB,UAAM,UAAU,OAAO,YACrB,IAAI,CAAC,MAAM;AACX,YAAM,QAAkB;AAAA,QACvB,iBAAiB,EAAE,UAAU,qBAAqB,EAAE,OAAO,QAAQ,EAAE,OAAO;AAAA,MAC7E;AACA,UAAI,EAAE,WAAW,SAAS,GAAG;AAC5B,cAAM,KAAK,kBAAkB,EAAE,OAAO,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC,EAAE;AAAA,MACrE;AACA,UAAI,EAAE,WAAW,SAAS,GAAG;AAC5B,cAAM,KAAK,kBAAkB,EAAE,OAAO,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC,EAAE;AAAA,MACrE;AACA,iBAAW,MAAM,EAAE,kBAAkB;AACpC,cAAM;AAAA,UACL,eAAe,GAAG,QAAQ,YAAY,GAAG,KAAK,MAAM,KAAK,UAAU,GAAG,QAAQ,CAAC,OAAO,KAAK,UAAU,GAAG,QAAQ,CAAC;AAAA,QAClH;AAAA,MACD;AACA,aAAO,MAAM,KAAK,IAAI;AAAA,IACvB,CAAC,EACA,KAAK,IAAI;AAEX,UAAM,IAAI,MAAM;AAAA,EAAgC,OAAO,EAAE;AAAA,EAC1D;AACD;AAMA,eAAsB,0BACrB,SACA,QACA,SACgB;AAChB,MAAI,QAAQ,SAAS,EAAG;AAExB,QAAM,YAAY,SAAS,aAAa;AACxC,QAAM,aAAa,SAAS,cAAc;AAC1C,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC7B,UAAM,SAAS,MAAM,iBAAiB,SAAS,MAAM;AACrD,QAAI,OAAO,WAAW;AACrB;AAAA,IACD;AACA,UAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,UAAU,CAAC;AAAA,EACrE;AAEA,QAAM,gBAAgB,SAAS,MAAM;AACtC;AASA,eAAsB,iBACrB,SACA,QAC6B;AAC7B,QAAM,cAAsC,CAAC;AAC7C,QAAM,kBAAkB,OAAO,KAAK,OAAO,WAAW;AAEtD,WAAS,IAAI,GAAG,IAAI,QAAQ,SAAS,GAAG,KAAK;AAC5C,aAAS,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AAC5C,YAAM,UAAU,QAAQ,CAAC;AACzB,YAAM,UAAU,QAAQ,CAAC;AAEzB,iBAAW,cAAc,iBAAiB;AACzC,cAAM,SAAS,MAAM,QAAQ,SAAS,UAAU;AAChD,cAAM,SAAS,MAAM,QAAQ,SAAS,UAAU;AAEhD,cAAM,OAAO,wBAAwB,YAAY,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM;AAE3F,YAAI,MAAM;AACT,sBAAY,KAAK,IAAI;AAAA,QACtB;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,WAAW,YAAY,WAAW;AAAA,IAClC;AAAA,EACD;AACD;AAKA,SAAS,wBACR,YACA,OACA,OACA,QACA,QAC8B;AAC9B,QAAM,OAAO,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,IAAc,CAAC,CAAC,CAAC;AAC3D,QAAM,OAAO,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,IAAc,CAAC,CAAC,CAAC;AAE3D,QAAM,aAAuB,CAAC;AAC9B,QAAM,aAAuB,CAAC;AAC9B,QAAM,mBAAsC,CAAC;AAG7C,aAAW,CAAC,IAAI,OAAO,KAAK,MAAM;AACjC,UAAM,UAAU,KAAK,IAAI,EAAE;AAC3B,QAAI,CAAC,SAAS;AACb,iBAAW,KAAK,EAAE;AAClB;AAAA,IACD;AAGA,UAAM,YAAY,oBAAI,IAAI;AAAA,MACzB,GAAG,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AAAA,MACxD,GAAG,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AAAA,IACzD,CAAC;AAED,eAAW,SAAS,WAAW;AAC9B,UAAI,UAAU,KAAM;AACpB,YAAM,OAAO,QAAQ,KAAK;AAC1B,YAAM,OAAO,QAAQ,KAAK;AAC1B,UAAI,CAAC,UAAU,MAAM,IAAI,GAAG;AAC3B,yBAAiB,KAAK;AAAA,UACrB,UAAU;AAAA,UACV;AAAA,UACA,UAAU;AAAA,UACV,UAAU;AAAA,QACX,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AAGA,aAAW,MAAM,KAAK,KAAK,GAAG;AAC7B,QAAI,CAAC,KAAK,IAAI,EAAE,GAAG;AAClB,iBAAW,KAAK,EAAE;AAAA,IACnB;AAAA,EACD;AAEA,MAAI,WAAW,WAAW,KAAK,WAAW,WAAW,KAAK,iBAAiB,WAAW,GAAG;AACxF,WAAO;AAAA,EACR;AAEA,SAAO;AAAA,IACN;AAAA,IACA,SAAS;AAAA,IACT,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACD;AAKA,SAAS,UAAU,GAAY,GAAqB;AACnD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,MAAM,QAAQ,MAAM,KAAM,QAAO;AACrC,MAAI,MAAM,UAAa,MAAM,OAAW,QAAO;AAC/C,MAAI,OAAO,MAAM,OAAO,EAAG,QAAO;AAElC,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACzC,QAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAO,EAAE,MAAM,CAAC,KAAK,MAAM,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC;AAAA,EAChD;AAEA,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AACnD,UAAM,QAAQ,OAAO,KAAK,CAA4B;AACtD,UAAM,QAAQ,OAAO,KAAK,CAA4B;AACtD,QAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1C,WAAO,MAAM;AAAA,MAAM,CAAC,QACnB,UAAW,EAA8B,GAAG,GAAI,EAA8B,GAAG,CAAC;AAAA,IACnF;AAAA,EACD;AAEA,SAAO;AACR;;;AC/OA,SAAS,iCAAiC;AAe1C,IAAM,sBAAwD,oBAAI,IAAI;AAAA,EACrE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAOD,SAAS,yBACR,SACA,YACc;AACd,MAAI,CAAC,oBAAoB,IAAI,QAAQ,IAAI,GAAG;AAG3C,WAAO,gBAAgB,OAAO;AAAA,EAC/B;AAEA,QAAM,UAAU,WAAW,OAAO,OAAO;AACzC,MAAI,EAAE,mBAAmB,aAAa;AACrC,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACtE;AACA,SAAO,WAAW,OAAO,OAAO;AACjC;AAKA,IAAM,8BAAN,MAA2D;AAAA,EAC1D,YACkB,OACA,YAChB;AAFgB;AACA;AAAA,EACf;AAAA,EAFe;AAAA,EACA;AAAA,EAGlB,QAAQ,KAAa,SAA2C;AAC/D,WAAO,KAAK,MAAM,QAAQ,KAAK,OAAO;AAAA,EACvC;AAAA,EAEA,aAA4B;AAC3B,WAAO,KAAK,MAAM,WAAW;AAAA,EAC9B;AAAA,EAEA,KAAK,SAA4B;AAChC,SAAK,MAAM,KAAK,yBAAyB,SAAS,KAAK,UAAU,CAAC;AAAA,EACnE;AAAA,EAEA,UAAU,SAA+C;AACxD,SAAK,MAAM,UAAU,OAAO;AAAA,EAC7B;AAAA,EAEA,QAAQ,SAAyC;AAChD,SAAK,MAAM,QAAQ,OAAO;AAAA,EAC3B;AAAA,EAEA,QAAQ,SAAuC;AAC9C,SAAK,MAAM,QAAQ,OAAO;AAAA,EAC3B;AAAA,EAEA,cAAuB;AACtB,WAAO,KAAK,MAAM,YAAY;AAAA,EAC/B;AACD;AAKA,IAAM,8BAAN,MAA6D;AAAA,EAC5D,YACkB,OACA,YAChB;AAFgB;AACA;AAAA,EACf;AAAA,EAFe;AAAA,EACA;AAAA,EAGlB,KAAK,SAA4B;AAChC,SAAK,MAAM,KAAK,yBAAyB,SAAS,KAAK,UAAU,CAAC;AAAA,EACnE;AAAA,EAEA,UAAU,SAA+C;AACxD,SAAK,MAAM,UAAU,OAAO;AAAA,EAC7B;AAAA,EAEA,QAAQ,SAAuD;AAC9D,SAAK,MAAM,QAAQ,OAAO;AAAA,EAC3B;AAAA,EAEA,QAAQ,SAAuC;AAC9C,SAAK,MAAM,QAAQ,OAAO;AAAA,EAC3B;AAAA,EAEA,cAAuB;AACtB,WAAO,KAAK,MAAM,YAAY;AAAA,EAC/B;AAAA,EAEA,MAAM,MAAe,QAAuB;AAC3C,SAAK,MAAM,MAAM,MAAM,MAAM;AAAA,EAC9B;AACD;AAaO,SAAS,kCAAkC,MAAoC;AACrF,QAAM,aAAa,IAAI,0BAA0B;AACjD,SAAO;AAAA,IACN,QAAQ,IAAI,4BAA4B,KAAK,QAAQ,UAAU;AAAA,IAC/D,iBAAiB,IAAI,4BAA4B,KAAK,iBAAiB,UAAU;AAAA,EAClF;AACD;;;AC1HA,IAAM,6BAAN,MAA4D;AAAA,EAC3D,YACkB,OACA,WAChB;AAFgB;AACA;AAAA,EACf;AAAA,EAFe;AAAA,EACA;AAAA,EAGlB,KAAK,SAA4B;AAChC,QAAI,QAAQ,SAAS,sBAAsB;AAC1C,WAAK,MAAM,KAAK,EAAE,GAAG,SAAS,YAAY,KAAK,UAAU,EAAE,CAAC;AAC5D;AAAA,IACD;AACA,SAAK,MAAM,KAAK,OAAO;AAAA,EACxB;AAAA,EAEA,UAAU,SAA+C;AACxD,SAAK,MAAM,UAAU,OAAO;AAAA,EAC7B;AAAA,EAEA,QAAQ,SAAuD;AAC9D,SAAK,MAAM,QAAQ,OAAO;AAAA,EAC3B;AAAA,EAEA,QAAQ,SAAuC;AAC9C,SAAK,MAAM,QAAQ,OAAO;AAAA,EAC3B;AAAA,EAEA,cAAuB;AACtB,WAAO,KAAK,MAAM,YAAY;AAAA,EAC/B;AAAA,EAEA,MAAM,MAAe,QAAuB;AAC3C,SAAK,MAAM,MAAM,MAAM,MAAM;AAAA,EAC9B;AACD;AAUO,SAAS,iCACf,MACA,WACgB;AAChB,SAAO;AAAA,IACN,QAAQ,KAAK;AAAA,IACb,iBAAiB,IAAI,2BAA2B,KAAK,iBAAiB,SAAS;AAAA,EAChF;AACD;;;AC9BA,SAAS,kBAAAA,uBAAsB;","names":["ChaosTransport"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korajs/test",
3
- "version": "0.6.0",
3
+ "version": "1.0.0-beta.0",
4
4
  "description": "Testing harness for Kora.js — create virtual device networks and assert convergence",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,14 +23,15 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "better-sqlite3": "^12.8.0",
26
- "@korajs/store": "0.6.0",
27
- "@korajs/server": "0.6.0",
28
- "@korajs/core": "0.6.0",
29
- "@korajs/sync": "0.6.0",
30
- "@korajs/merge": "0.6.0"
26
+ "@korajs/core": "1.0.0-beta.0",
27
+ "@korajs/store": "1.0.0-beta.0",
28
+ "@korajs/merge": "1.0.0-beta.0",
29
+ "@korajs/sync": "1.0.0-beta.0",
30
+ "@korajs/server": "1.0.0-beta.0",
31
+ "korajs": "1.0.0-beta.0"
31
32
  },
32
33
  "peerDependencies": {
33
- "korajs": "0.6.0"
34
+ "korajs": "1.0.0-beta.0"
34
35
  },
35
36
  "peerDependenciesMeta": {
36
37
  "korajs": {