@korajs/test 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,12 +30,14 @@ 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_sync2.ChaosTransport,
33
+ ChaosTransport: () => import_sync3.ChaosTransport,
34
34
  TestDevice: () => TestDevice,
35
35
  TestServer: () => TestServer,
36
36
  checkConvergence: () => checkConvergence,
37
+ createMixedTestNetwork: () => createMixedTestNetwork,
37
38
  createTestNetwork: () => createTestNetwork,
38
- expectConverged: () => expectConverged
39
+ expectConverged: () => expectConverged,
40
+ expectConvergedEventually: () => expectConvergedEventually
39
41
  });
40
42
  module.exports = __toCommonJS(index_exports);
41
43
 
@@ -44,6 +46,7 @@ var import_node_fs = require("fs");
44
46
  var import_node_os = require("os");
45
47
  var import_node_path = require("path");
46
48
  var import_internal2 = require("@korajs/server/internal");
49
+ var import_sync2 = require("@korajs/sync");
47
50
 
48
51
  // src/test-device.ts
49
52
  var import_internal = require("@korajs/core/internal");
@@ -51,6 +54,7 @@ var import_merge = require("@korajs/merge");
51
54
  var import_store = require("@korajs/store");
52
55
  var import_better_sqlite3 = require("@korajs/store/better-sqlite3");
53
56
  var import_sync = require("@korajs/sync");
57
+ var import_testing = require("korajs/testing");
54
58
  var TestDevice = class {
55
59
  name;
56
60
  store;
@@ -61,6 +65,9 @@ var TestDevice = class {
61
65
  createTransportPair;
62
66
  adapter;
63
67
  dbPath;
68
+ syncSchemaVersion;
69
+ operationTransforms;
70
+ applyPipeline = null;
64
71
  syncEngine = null;
65
72
  currentTransport = null;
66
73
  unsubscribeSync = null;
@@ -71,6 +78,8 @@ var TestDevice = class {
71
78
  this.server = options.server;
72
79
  this.createTransportPair = options.createTransportPair;
73
80
  this.dbPath = `${options.tmpDir}/test-device-${options.name}.db`;
81
+ this.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version;
82
+ this.operationTransforms = options.operationTransforms ?? [];
74
83
  this.emitter = new import_internal.SimpleEventEmitter();
75
84
  this.mergeEngine = new import_merge.MergeEngine();
76
85
  this.adapter = new import_better_sqlite3.BetterSqlite3Adapter(this.dbPath);
@@ -85,6 +94,12 @@ var TestDevice = class {
85
94
  */
86
95
  async open() {
87
96
  await this.store.open();
97
+ this.applyPipeline = new import_testing.ApplyPipeline({
98
+ store: this.store,
99
+ mergeEngine: this.mergeEngine,
100
+ emitter: this.emitter
101
+ });
102
+ this.store.setLocalMutationHandler(this.applyPipeline);
88
103
  }
89
104
  /**
90
105
  * Connect to the test server and perform initial sync.
@@ -93,17 +108,28 @@ var TestDevice = class {
93
108
  async sync() {
94
109
  if (this.syncEngine && this.currentTransport?.isConnected()) {
95
110
  await this.waitForPendingOps();
111
+ await this.waitForSettled();
96
112
  return;
97
113
  }
98
114
  const { client, serverTransport } = this.createTransportPair();
99
115
  this.currentTransport = client;
100
- const syncStore = this.createMergeAwareSyncStore();
116
+ const conflictHandler = {};
117
+ const syncStore = new import_testing.MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {
118
+ onMergeConflict: () => conflictHandler.fn?.()
119
+ });
101
120
  this.syncEngine = new import_sync.SyncEngine({
102
121
  transport: client,
103
122
  store: syncStore,
104
- config: { url: "ws://test-network" },
123
+ queueStorage: new import_testing.StoreQueueStorage(this.adapter),
124
+ syncState: new import_testing.StoreSyncStatePersistence(this.store),
125
+ config: {
126
+ url: "ws://test-network",
127
+ schemaVersion: this.syncSchemaVersion,
128
+ operationTransforms: this.operationTransforms.length > 0 ? this.operationTransforms : void 0
129
+ },
105
130
  emitter: this.emitter
106
131
  });
132
+ conflictHandler.fn = () => this.syncEngine?.recordConflict();
107
133
  const engine = this.syncEngine;
108
134
  this.unsubscribeSync = this.emitter.on("operation:created", (event) => {
109
135
  if (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {
@@ -154,6 +180,10 @@ var TestDevice = class {
154
180
  getNodeId() {
155
181
  return this.store.getNodeId();
156
182
  }
183
+ /** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
184
+ getSyncEngine() {
185
+ return this.syncEngine;
186
+ }
157
187
  /**
158
188
  * Get the device's version vector.
159
189
  */
@@ -175,36 +205,13 @@ var TestDevice = class {
175
205
  await this.store.close();
176
206
  this.emitter.clear();
177
207
  }
178
- /**
179
- * Create a SyncStore wrapper that interposes merge resolution.
180
- * Simplified version of MergeAwareSyncStore from the kora meta-package.
181
- */
182
- createMergeAwareSyncStore() {
183
- const store = this.store;
184
- const mergeEngine = this.mergeEngine;
185
- const emitter = this.emitter;
186
- return {
187
- getVersionVector() {
188
- return store.getVersionVector();
189
- },
190
- getNodeId() {
191
- return store.getNodeId();
192
- },
193
- async getOperationRange(nodeId, fromSeq, toSeq) {
194
- return store.getOperationRange(nodeId, fromSeq, toSeq);
195
- },
196
- async applyRemoteOperation(op) {
197
- return store.applyRemoteOperation(op);
198
- }
199
- };
200
- }
201
208
  /**
202
209
  * Wait for in-flight sync operations to settle.
203
- * In-memory transports are near-synchronous, so a microtask flush suffices.
210
+ * In-memory transports are near-synchronous, but apply/relay work is async.
204
211
  */
205
212
  async waitForSettled() {
206
- for (let i = 0; i < 5; i++) {
207
- await new Promise((resolve) => setTimeout(resolve, 10));
213
+ for (let i = 0; i < 15; i++) {
214
+ await new Promise((resolve) => setTimeout(resolve, 20));
208
215
  }
209
216
  }
210
217
  /**
@@ -228,12 +235,18 @@ var import_server2 = require("@korajs/server");
228
235
  var TestServer = class {
229
236
  store;
230
237
  syncServer;
231
- constructor(schema) {
238
+ constructor(schema, options) {
232
239
  this.store = new import_server.MemoryServerStore();
240
+ const schemaVersion = options?.schemaVersion ?? schema.version;
233
241
  this.syncServer = new import_server2.KoraSyncServer({
234
242
  store: this.store,
235
- schemaVersion: schema.version
243
+ schemaVersion,
244
+ supportedSchemaVersions: options?.supportedSchemaVersions ?? {
245
+ min: schemaVersion,
246
+ max: schemaVersion
247
+ }
236
248
  });
249
+ void this.store.setSchema(schema);
237
250
  }
238
251
  /**
239
252
  * Register a client connection transport with the server.
@@ -275,6 +288,48 @@ async function createTestNetwork(schema, options) {
275
288
  name,
276
289
  schema,
277
290
  server,
291
+ createTransportPair: () => {
292
+ const pair = (0, import_internal2.createServerTransportPair)();
293
+ const client = pair.client;
294
+ const chaos = options?.chaos;
295
+ return {
296
+ client: chaos ? new import_sync2.ChaosTransport(client, chaos) : client,
297
+ serverTransport: pair.server
298
+ };
299
+ },
300
+ tmpDir
301
+ });
302
+ await device.open();
303
+ devices.push(device);
304
+ }
305
+ return {
306
+ server,
307
+ devices,
308
+ tmpDir,
309
+ async close() {
310
+ for (const device of devices) {
311
+ await device.close();
312
+ }
313
+ await server.close();
314
+ try {
315
+ const { rmSync } = await import("fs");
316
+ rmSync(tmpDir, { recursive: true, force: true });
317
+ } catch {
318
+ }
319
+ }
320
+ };
321
+ }
322
+ async function createMixedTestNetwork(serverSchema, serverOptions, deviceConfigs) {
323
+ const tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "kora-test-"));
324
+ const server = new TestServer(serverSchema, serverOptions);
325
+ const devices = [];
326
+ for (const config of deviceConfigs) {
327
+ const device = new TestDevice({
328
+ name: config.name,
329
+ schema: config.schema,
330
+ server,
331
+ syncSchemaVersion: config.syncSchemaVersion,
332
+ operationTransforms: config.operationTransforms,
278
333
  createTransportPair: () => {
279
334
  const pair = (0, import_internal2.createServerTransportPair)();
280
335
  return {
@@ -331,6 +386,20 @@ async function expectConverged(devices, schema) {
331
386
  ${details}`);
332
387
  }
333
388
  }
389
+ async function expectConvergedEventually(devices, schema, options) {
390
+ if (devices.length < 2) return;
391
+ const timeoutMs = options?.timeoutMs ?? 5e3;
392
+ const intervalMs = options?.intervalMs ?? 25;
393
+ const deadline = Date.now() + timeoutMs;
394
+ while (Date.now() < deadline) {
395
+ const result = await checkConvergence(devices, schema);
396
+ if (result.converged) {
397
+ return;
398
+ }
399
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
400
+ }
401
+ await expectConverged(devices, schema);
402
+ }
334
403
  async function checkConvergence(devices, schema) {
335
404
  const differences = [];
336
405
  const collectionNames = Object.keys(schema.collections);
@@ -421,14 +490,16 @@ function deepEqual(a, b) {
421
490
  }
422
491
 
423
492
  // src/index.ts
424
- var import_sync2 = require("@korajs/sync");
493
+ var import_sync3 = require("@korajs/sync");
425
494
  // Annotate the CommonJS export names for ESM import in node:
426
495
  0 && (module.exports = {
427
496
  ChaosTransport,
428
497
  TestDevice,
429
498
  TestServer,
430
499
  checkConvergence,
500
+ createMixedTestNetwork,
431
501
  createTestNetwork,
432
- expectConverged
502
+ expectConverged,
503
+ expectConvergedEventually
433
504
  });
434
505
  //# 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 } from './test-network'\nexport type { TestNetwork, TestNetworkOptions } 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'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged } 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 { SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { SyncTransport } from '@korajs/sync'\nimport { TestDevice } from './test-device'\nimport { TestServer } 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}\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\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\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","import type { Operation, SchemaDefinition, 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 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}\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\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\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}\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 — wait for pending operations to flush\n\t\t\tawait this.waitForPendingOps()\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\t// Create a MergeAwareSyncStore wrapper\n\t\tconst syncStore = this.createMergeAwareSyncStore()\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tconfig: { url: 'ws://test-network' },\n\t\t\temitter: this.emitter,\n\t\t})\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/**\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 * Create a SyncStore wrapper that interposes merge resolution.\n\t * Simplified version of MergeAwareSyncStore from the kora meta-package.\n\t */\n\tprivate createMergeAwareSyncStore(): import('@korajs/sync').SyncStore {\n\t\tconst store = this.store\n\t\tconst mergeEngine = this.mergeEngine\n\t\tconst emitter = this.emitter\n\n\t\treturn {\n\t\t\tgetVersionVector(): VersionVector {\n\t\t\t\treturn store.getVersionVector()\n\t\t\t},\n\t\t\tgetNodeId(): string {\n\t\t\t\treturn store.getNodeId()\n\t\t\t},\n\t\t\tasync getOperationRange(\n\t\t\t\tnodeId: string,\n\t\t\t\tfromSeq: number,\n\t\t\t\ttoSeq: number,\n\t\t\t): Promise<Operation[]> {\n\t\t\t\treturn store.getOperationRange(nodeId, fromSeq, toSeq)\n\t\t\t},\n\t\t\tasync applyRemoteOperation(op: Operation): Promise<import('@korajs/sync').ApplyResult> {\n\t\t\t\t// For the test harness, delegate directly to store.\n\t\t\t\t// Merge resolution happens inside the store's applyRemoteOperation.\n\t\t\t\treturn store.applyRemoteOperation(op)\n\t\t\t},\n\t\t}\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, so a microtask flush suffices.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\t// Multiple microtask flushes to allow async message processing to complete\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\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 class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition) {\n\t\tthis.store = new MemoryServerStore()\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion: schema.version,\n\t\t})\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 * 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_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"],"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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAA4B;AAC5B,qBAAuB;AACvB,uBAAqB;AAErB,IAAAA,mBAA0C;AAE1C,IAAAC,eAA+B;;;ACH/B,sBAAmC;AACnC,mBAA4B;AAC5B,mBAAsB;AAEtB,4BAAqC;AACrC,kBAA2B;AAE3B,qBAKO;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,mCAAmB;AACtC,SAAK,cAAc,IAAI,yBAAY;AACnC,SAAK,UAAU,IAAI,2CAAqB,KAAK,MAAM;AACnD,SAAK,QAAQ,IAAI,mBAAM;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,6BAAc;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,mCAAoB,KAAK,OAAO,KAAK,aAAa,KAAK,SAAS;AAAA,MACrF,iBAAiB,MAAM,gBAAgB,KAAK;AAAA,IAC7C,CAAC;AAED,SAAK,aAAa,IAAI,uBAAW;AAAA,MAChC,WAAW;AAAA,MACX,OAAO;AAAA,MACP,cAAc,IAAI,iCAAkB,KAAK,OAAO;AAAA,MAChD,WAAW,IAAI,yCAA0B,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,oBAAkC;AAClC,IAAAC,iBAA+B;AAcxB,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACQ;AAAA,EAEjB,YAAY,QAA0B,SAA6B;AAClE,SAAK,QAAQ,IAAI,gCAAkB;AACnC,UAAM,gBAAgB,SAAS,iBAAiB,OAAO;AACvD,SAAK,aAAa,IAAI,8BAAe;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,aAAS,gCAAY,2BAAK,uBAAO,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,WAAO,4CAA0B;AACvC,cAAM,SAAS,KAAK;AACpB,cAAM,QAAQ,SAAS;AACvB,eAAO;AAAA,UACN,QAAQ,QAAQ,IAAI,4BAAe,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,aAAS,gCAAY,2BAAK,uBAAO,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,WAAO,4CAA0B;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;;;AJrNA,IAAAC,eAA+B;","names":["import_internal","import_sync","import_server","import_sync"]}
package/dist/index.d.cts CHANGED
@@ -1,18 +1,27 @@
1
- import { SchemaDefinition, Operation, KoraEventEmitter, VersionVector } from '@korajs/core';
1
+ import { SchemaDefinition, Operation, KoraEventEmitter, OperationTransform, VersionVector } from '@korajs/core';
2
+ import { SyncTransport, SyncEngine, ChaosConfig } from '@korajs/sync';
3
+ export { ChaosConfig, ChaosTransport } from '@korajs/sync';
2
4
  import * as _korajs_server from '@korajs/server';
3
5
  import { MemoryServerStore, ServerTransport } from '@korajs/server';
4
6
  import { Store, CollectionAccessor } from '@korajs/store';
5
- import { SyncTransport } from '@korajs/sync';
6
- export { ChaosConfig, ChaosTransport } from '@korajs/sync';
7
7
 
8
8
  /**
9
9
  * In-memory test server wrapping KoraSyncServer with MemoryServerStore.
10
10
  * Handles client connections via memory transports.
11
11
  */
12
+ interface TestServerOptions {
13
+ /** Handshake schema version advertised by the server. Defaults to `schema.version`. */
14
+ schemaVersion?: number;
15
+ /** Inclusive client schema versions accepted at handshake. */
16
+ supportedSchemaVersions?: {
17
+ min: number;
18
+ max: number;
19
+ };
20
+ }
12
21
  declare class TestServer {
13
22
  readonly store: MemoryServerStore;
14
23
  private readonly syncServer;
15
- constructor(schema: SchemaDefinition);
24
+ constructor(schema: SchemaDefinition, options?: TestServerOptions);
16
25
  /**
17
26
  * Register a client connection transport with the server.
18
27
  * Returns the session ID assigned by the server.
@@ -49,6 +58,10 @@ interface TestDeviceOptions {
49
58
  };
50
59
  /** Optional directory for temp DB files */
51
60
  tmpDir: string;
61
+ /** Client handshake schema version. Defaults to `schema.version`. */
62
+ syncSchemaVersion?: number;
63
+ /** Transforms applied to inbound operations before local apply. */
64
+ operationTransforms?: OperationTransform[];
52
65
  }
53
66
  /**
54
67
  * A virtual device in a test network.
@@ -67,6 +80,9 @@ declare class TestDevice {
67
80
  private readonly createTransportPair;
68
81
  private readonly adapter;
69
82
  private readonly dbPath;
83
+ private readonly syncSchemaVersion;
84
+ private readonly operationTransforms;
85
+ private applyPipeline;
70
86
  private syncEngine;
71
87
  private currentTransport;
72
88
  private unsubscribeSync;
@@ -101,6 +117,8 @@ declare class TestDevice {
101
117
  * Get the device's node ID.
102
118
  */
103
119
  getNodeId(): string;
120
+ /** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
121
+ getSyncEngine(): SyncEngine | null;
104
122
  /**
105
123
  * Get the device's version vector.
106
124
  */
@@ -113,14 +131,9 @@ declare class TestDevice {
113
131
  * Close the device, releasing all resources.
114
132
  */
115
133
  close(): Promise<void>;
116
- /**
117
- * Create a SyncStore wrapper that interposes merge resolution.
118
- * Simplified version of MergeAwareSyncStore from the kora meta-package.
119
- */
120
- private createMergeAwareSyncStore;
121
134
  /**
122
135
  * Wait for in-flight sync operations to settle.
123
- * In-memory transports are near-synchronous, so a microtask flush suffices.
136
+ * In-memory transports are near-synchronous, but apply/relay work is async.
124
137
  */
125
138
  private waitForSettled;
126
139
  /**
@@ -137,6 +150,8 @@ interface TestNetworkOptions {
137
150
  devices?: number;
138
151
  /** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */
139
152
  deviceNames?: string[];
153
+ /** Optional chaos transport settings applied to each device link. */
154
+ chaos?: ChaosConfig;
140
155
  }
141
156
  /**
142
157
  * A test network with a server and multiple devices.
@@ -177,6 +192,20 @@ interface TestNetwork {
177
192
  * ```
178
193
  */
179
194
  declare function createTestNetwork(schema: SchemaDefinition, options?: TestNetworkOptions): Promise<TestNetwork>;
195
+ /**
196
+ * Per-device configuration when devices use different local schemas or sync settings.
197
+ */
198
+ interface MixedTestDeviceConfig {
199
+ name: string;
200
+ schema: SchemaDefinition;
201
+ syncSchemaVersion?: number;
202
+ operationTransforms?: OperationTransform[];
203
+ }
204
+ /**
205
+ * Create a test network where devices may use different schema versions and transforms.
206
+ * The server uses `serverSchema` for materialization and handshake bounds.
207
+ */
208
+ declare function createMixedTestNetwork(serverSchema: SchemaDefinition, serverOptions: TestServerOptions, deviceConfigs: MixedTestDeviceConfig[]): Promise<TestNetwork>;
180
209
 
181
210
  /**
182
211
  * Result of a convergence check.
@@ -227,6 +256,14 @@ interface FieldDifference {
227
256
  * ```
228
257
  */
229
258
  declare function expectConverged(devices: TestDevice[], schema: SchemaDefinition): Promise<void>;
259
+ /**
260
+ * Poll until all devices converge or timeout. Use after sync in integration tests
261
+ * where relay/apply work may still be in flight under parallel CI load.
262
+ */
263
+ declare function expectConvergedEventually(devices: TestDevice[], schema: SchemaDefinition, options?: {
264
+ timeoutMs?: number;
265
+ intervalMs?: number;
266
+ }): Promise<void>;
230
267
  /**
231
268
  * Check whether all devices have converged without throwing.
232
269
  *
@@ -236,4 +273,4 @@ declare function expectConverged(devices: TestDevice[], schema: SchemaDefinition
236
273
  */
237
274
  declare function checkConvergence(devices: TestDevice[], schema: SchemaDefinition): Promise<ConvergenceResult>;
238
275
 
239
- export { type CollectionDifference, type ConvergenceResult, type FieldDifference, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, checkConvergence, createTestNetwork, expectConverged };
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 };
package/dist/index.d.ts CHANGED
@@ -1,18 +1,27 @@
1
- import { SchemaDefinition, Operation, KoraEventEmitter, VersionVector } from '@korajs/core';
1
+ import { SchemaDefinition, Operation, KoraEventEmitter, OperationTransform, VersionVector } from '@korajs/core';
2
+ import { SyncTransport, SyncEngine, ChaosConfig } from '@korajs/sync';
3
+ export { ChaosConfig, ChaosTransport } from '@korajs/sync';
2
4
  import * as _korajs_server from '@korajs/server';
3
5
  import { MemoryServerStore, ServerTransport } from '@korajs/server';
4
6
  import { Store, CollectionAccessor } from '@korajs/store';
5
- import { SyncTransport } from '@korajs/sync';
6
- export { ChaosConfig, ChaosTransport } from '@korajs/sync';
7
7
 
8
8
  /**
9
9
  * In-memory test server wrapping KoraSyncServer with MemoryServerStore.
10
10
  * Handles client connections via memory transports.
11
11
  */
12
+ interface TestServerOptions {
13
+ /** Handshake schema version advertised by the server. Defaults to `schema.version`. */
14
+ schemaVersion?: number;
15
+ /** Inclusive client schema versions accepted at handshake. */
16
+ supportedSchemaVersions?: {
17
+ min: number;
18
+ max: number;
19
+ };
20
+ }
12
21
  declare class TestServer {
13
22
  readonly store: MemoryServerStore;
14
23
  private readonly syncServer;
15
- constructor(schema: SchemaDefinition);
24
+ constructor(schema: SchemaDefinition, options?: TestServerOptions);
16
25
  /**
17
26
  * Register a client connection transport with the server.
18
27
  * Returns the session ID assigned by the server.
@@ -49,6 +58,10 @@ interface TestDeviceOptions {
49
58
  };
50
59
  /** Optional directory for temp DB files */
51
60
  tmpDir: string;
61
+ /** Client handshake schema version. Defaults to `schema.version`. */
62
+ syncSchemaVersion?: number;
63
+ /** Transforms applied to inbound operations before local apply. */
64
+ operationTransforms?: OperationTransform[];
52
65
  }
53
66
  /**
54
67
  * A virtual device in a test network.
@@ -67,6 +80,9 @@ declare class TestDevice {
67
80
  private readonly createTransportPair;
68
81
  private readonly adapter;
69
82
  private readonly dbPath;
83
+ private readonly syncSchemaVersion;
84
+ private readonly operationTransforms;
85
+ private applyPipeline;
70
86
  private syncEngine;
71
87
  private currentTransport;
72
88
  private unsubscribeSync;
@@ -101,6 +117,8 @@ declare class TestDevice {
101
117
  * Get the device's node ID.
102
118
  */
103
119
  getNodeId(): string;
120
+ /** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
121
+ getSyncEngine(): SyncEngine | null;
104
122
  /**
105
123
  * Get the device's version vector.
106
124
  */
@@ -113,14 +131,9 @@ declare class TestDevice {
113
131
  * Close the device, releasing all resources.
114
132
  */
115
133
  close(): Promise<void>;
116
- /**
117
- * Create a SyncStore wrapper that interposes merge resolution.
118
- * Simplified version of MergeAwareSyncStore from the kora meta-package.
119
- */
120
- private createMergeAwareSyncStore;
121
134
  /**
122
135
  * Wait for in-flight sync operations to settle.
123
- * In-memory transports are near-synchronous, so a microtask flush suffices.
136
+ * In-memory transports are near-synchronous, but apply/relay work is async.
124
137
  */
125
138
  private waitForSettled;
126
139
  /**
@@ -137,6 +150,8 @@ interface TestNetworkOptions {
137
150
  devices?: number;
138
151
  /** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */
139
152
  deviceNames?: string[];
153
+ /** Optional chaos transport settings applied to each device link. */
154
+ chaos?: ChaosConfig;
140
155
  }
141
156
  /**
142
157
  * A test network with a server and multiple devices.
@@ -177,6 +192,20 @@ interface TestNetwork {
177
192
  * ```
178
193
  */
179
194
  declare function createTestNetwork(schema: SchemaDefinition, options?: TestNetworkOptions): Promise<TestNetwork>;
195
+ /**
196
+ * Per-device configuration when devices use different local schemas or sync settings.
197
+ */
198
+ interface MixedTestDeviceConfig {
199
+ name: string;
200
+ schema: SchemaDefinition;
201
+ syncSchemaVersion?: number;
202
+ operationTransforms?: OperationTransform[];
203
+ }
204
+ /**
205
+ * Create a test network where devices may use different schema versions and transforms.
206
+ * The server uses `serverSchema` for materialization and handshake bounds.
207
+ */
208
+ declare function createMixedTestNetwork(serverSchema: SchemaDefinition, serverOptions: TestServerOptions, deviceConfigs: MixedTestDeviceConfig[]): Promise<TestNetwork>;
180
209
 
181
210
  /**
182
211
  * Result of a convergence check.
@@ -227,6 +256,14 @@ interface FieldDifference {
227
256
  * ```
228
257
  */
229
258
  declare function expectConverged(devices: TestDevice[], schema: SchemaDefinition): Promise<void>;
259
+ /**
260
+ * Poll until all devices converge or timeout. Use after sync in integration tests
261
+ * where relay/apply work may still be in flight under parallel CI load.
262
+ */
263
+ declare function expectConvergedEventually(devices: TestDevice[], schema: SchemaDefinition, options?: {
264
+ timeoutMs?: number;
265
+ intervalMs?: number;
266
+ }): Promise<void>;
230
267
  /**
231
268
  * Check whether all devices have converged without throwing.
232
269
  *
@@ -236,4 +273,4 @@ declare function expectConverged(devices: TestDevice[], schema: SchemaDefinition
236
273
  */
237
274
  declare function checkConvergence(devices: TestDevice[], schema: SchemaDefinition): Promise<ConvergenceResult>;
238
275
 
239
- export { type CollectionDifference, type ConvergenceResult, type FieldDifference, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, checkConvergence, createTestNetwork, expectConverged };
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 };
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { mkdtempSync } from "fs";
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
5
  import { createServerTransportPair } from "@korajs/server/internal";
6
+ import { ChaosTransport } from "@korajs/sync";
6
7
 
7
8
  // src/test-device.ts
8
9
  import { SimpleEventEmitter } from "@korajs/core/internal";
@@ -10,6 +11,12 @@ import { MergeEngine } from "@korajs/merge";
10
11
  import { Store } from "@korajs/store";
11
12
  import { BetterSqlite3Adapter } from "@korajs/store/better-sqlite3";
12
13
  import { SyncEngine } from "@korajs/sync";
14
+ import {
15
+ ApplyPipeline,
16
+ MergeAwareSyncStore,
17
+ StoreQueueStorage,
18
+ StoreSyncStatePersistence
19
+ } from "korajs/testing";
13
20
  var TestDevice = class {
14
21
  name;
15
22
  store;
@@ -20,6 +27,9 @@ var TestDevice = class {
20
27
  createTransportPair;
21
28
  adapter;
22
29
  dbPath;
30
+ syncSchemaVersion;
31
+ operationTransforms;
32
+ applyPipeline = null;
23
33
  syncEngine = null;
24
34
  currentTransport = null;
25
35
  unsubscribeSync = null;
@@ -30,6 +40,8 @@ var TestDevice = class {
30
40
  this.server = options.server;
31
41
  this.createTransportPair = options.createTransportPair;
32
42
  this.dbPath = `${options.tmpDir}/test-device-${options.name}.db`;
43
+ this.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version;
44
+ this.operationTransforms = options.operationTransforms ?? [];
33
45
  this.emitter = new SimpleEventEmitter();
34
46
  this.mergeEngine = new MergeEngine();
35
47
  this.adapter = new BetterSqlite3Adapter(this.dbPath);
@@ -44,6 +56,12 @@ var TestDevice = class {
44
56
  */
45
57
  async open() {
46
58
  await this.store.open();
59
+ this.applyPipeline = new ApplyPipeline({
60
+ store: this.store,
61
+ mergeEngine: this.mergeEngine,
62
+ emitter: this.emitter
63
+ });
64
+ this.store.setLocalMutationHandler(this.applyPipeline);
47
65
  }
48
66
  /**
49
67
  * Connect to the test server and perform initial sync.
@@ -52,17 +70,28 @@ var TestDevice = class {
52
70
  async sync() {
53
71
  if (this.syncEngine && this.currentTransport?.isConnected()) {
54
72
  await this.waitForPendingOps();
73
+ await this.waitForSettled();
55
74
  return;
56
75
  }
57
76
  const { client, serverTransport } = this.createTransportPair();
58
77
  this.currentTransport = client;
59
- const syncStore = this.createMergeAwareSyncStore();
78
+ const conflictHandler = {};
79
+ const syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {
80
+ onMergeConflict: () => conflictHandler.fn?.()
81
+ });
60
82
  this.syncEngine = new SyncEngine({
61
83
  transport: client,
62
84
  store: syncStore,
63
- config: { url: "ws://test-network" },
85
+ queueStorage: new StoreQueueStorage(this.adapter),
86
+ syncState: new StoreSyncStatePersistence(this.store),
87
+ config: {
88
+ url: "ws://test-network",
89
+ schemaVersion: this.syncSchemaVersion,
90
+ operationTransforms: this.operationTransforms.length > 0 ? this.operationTransforms : void 0
91
+ },
64
92
  emitter: this.emitter
65
93
  });
94
+ conflictHandler.fn = () => this.syncEngine?.recordConflict();
66
95
  const engine = this.syncEngine;
67
96
  this.unsubscribeSync = this.emitter.on("operation:created", (event) => {
68
97
  if (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {
@@ -113,6 +142,10 @@ var TestDevice = class {
113
142
  getNodeId() {
114
143
  return this.store.getNodeId();
115
144
  }
145
+ /** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
146
+ getSyncEngine() {
147
+ return this.syncEngine;
148
+ }
116
149
  /**
117
150
  * Get the device's version vector.
118
151
  */
@@ -134,36 +167,13 @@ var TestDevice = class {
134
167
  await this.store.close();
135
168
  this.emitter.clear();
136
169
  }
137
- /**
138
- * Create a SyncStore wrapper that interposes merge resolution.
139
- * Simplified version of MergeAwareSyncStore from the kora meta-package.
140
- */
141
- createMergeAwareSyncStore() {
142
- const store = this.store;
143
- const mergeEngine = this.mergeEngine;
144
- const emitter = this.emitter;
145
- return {
146
- getVersionVector() {
147
- return store.getVersionVector();
148
- },
149
- getNodeId() {
150
- return store.getNodeId();
151
- },
152
- async getOperationRange(nodeId, fromSeq, toSeq) {
153
- return store.getOperationRange(nodeId, fromSeq, toSeq);
154
- },
155
- async applyRemoteOperation(op) {
156
- return store.applyRemoteOperation(op);
157
- }
158
- };
159
- }
160
170
  /**
161
171
  * Wait for in-flight sync operations to settle.
162
- * In-memory transports are near-synchronous, so a microtask flush suffices.
172
+ * In-memory transports are near-synchronous, but apply/relay work is async.
163
173
  */
164
174
  async waitForSettled() {
165
- for (let i = 0; i < 5; i++) {
166
- await new Promise((resolve) => setTimeout(resolve, 10));
175
+ for (let i = 0; i < 15; i++) {
176
+ await new Promise((resolve) => setTimeout(resolve, 20));
167
177
  }
168
178
  }
169
179
  /**
@@ -187,12 +197,18 @@ import { KoraSyncServer } from "@korajs/server";
187
197
  var TestServer = class {
188
198
  store;
189
199
  syncServer;
190
- constructor(schema) {
200
+ constructor(schema, options) {
191
201
  this.store = new MemoryServerStore();
202
+ const schemaVersion = options?.schemaVersion ?? schema.version;
192
203
  this.syncServer = new KoraSyncServer({
193
204
  store: this.store,
194
- schemaVersion: schema.version
205
+ schemaVersion,
206
+ supportedSchemaVersions: options?.supportedSchemaVersions ?? {
207
+ min: schemaVersion,
208
+ max: schemaVersion
209
+ }
195
210
  });
211
+ void this.store.setSchema(schema);
196
212
  }
197
213
  /**
198
214
  * Register a client connection transport with the server.
@@ -234,6 +250,48 @@ async function createTestNetwork(schema, options) {
234
250
  name,
235
251
  schema,
236
252
  server,
253
+ createTransportPair: () => {
254
+ const pair = createServerTransportPair();
255
+ const client = pair.client;
256
+ const chaos = options?.chaos;
257
+ return {
258
+ client: chaos ? new ChaosTransport(client, chaos) : client,
259
+ serverTransport: pair.server
260
+ };
261
+ },
262
+ tmpDir
263
+ });
264
+ await device.open();
265
+ devices.push(device);
266
+ }
267
+ return {
268
+ server,
269
+ devices,
270
+ tmpDir,
271
+ async close() {
272
+ for (const device of devices) {
273
+ await device.close();
274
+ }
275
+ await server.close();
276
+ try {
277
+ const { rmSync } = await import("fs");
278
+ rmSync(tmpDir, { recursive: true, force: true });
279
+ } catch {
280
+ }
281
+ }
282
+ };
283
+ }
284
+ async function createMixedTestNetwork(serverSchema, serverOptions, deviceConfigs) {
285
+ const tmpDir = mkdtempSync(join(tmpdir(), "kora-test-"));
286
+ const server = new TestServer(serverSchema, serverOptions);
287
+ const devices = [];
288
+ for (const config of deviceConfigs) {
289
+ const device = new TestDevice({
290
+ name: config.name,
291
+ schema: config.schema,
292
+ server,
293
+ syncSchemaVersion: config.syncSchemaVersion,
294
+ operationTransforms: config.operationTransforms,
237
295
  createTransportPair: () => {
238
296
  const pair = createServerTransportPair();
239
297
  return {
@@ -290,6 +348,20 @@ async function expectConverged(devices, schema) {
290
348
  ${details}`);
291
349
  }
292
350
  }
351
+ async function expectConvergedEventually(devices, schema, options) {
352
+ if (devices.length < 2) return;
353
+ const timeoutMs = options?.timeoutMs ?? 5e3;
354
+ const intervalMs = options?.intervalMs ?? 25;
355
+ const deadline = Date.now() + timeoutMs;
356
+ while (Date.now() < deadline) {
357
+ const result = await checkConvergence(devices, schema);
358
+ if (result.converged) {
359
+ return;
360
+ }
361
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
362
+ }
363
+ await expectConverged(devices, schema);
364
+ }
293
365
  async function checkConvergence(devices, schema) {
294
366
  const differences = [];
295
367
  const collectionNames = Object.keys(schema.collections);
@@ -380,13 +452,15 @@ function deepEqual(a, b) {
380
452
  }
381
453
 
382
454
  // src/index.ts
383
- import { ChaosTransport } from "@korajs/sync";
455
+ import { ChaosTransport as ChaosTransport2 } from "@korajs/sync";
384
456
  export {
385
- ChaosTransport,
457
+ ChaosTransport2 as ChaosTransport,
386
458
  TestDevice,
387
459
  TestServer,
388
460
  checkConvergence,
461
+ createMixedTestNetwork,
389
462
  createTestNetwork,
390
- expectConverged
463
+ expectConverged,
464
+ expectConvergedEventually
391
465
  };
392
466
  //# 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 { SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { SyncTransport } from '@korajs/sync'\nimport { TestDevice } from './test-device'\nimport { TestServer } 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}\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\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\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","import type { Operation, SchemaDefinition, 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 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}\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\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\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}\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 — wait for pending operations to flush\n\t\t\tawait this.waitForPendingOps()\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\t// Create a MergeAwareSyncStore wrapper\n\t\tconst syncStore = this.createMergeAwareSyncStore()\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tconfig: { url: 'ws://test-network' },\n\t\t\temitter: this.emitter,\n\t\t})\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/**\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 * Create a SyncStore wrapper that interposes merge resolution.\n\t * Simplified version of MergeAwareSyncStore from the kora meta-package.\n\t */\n\tprivate createMergeAwareSyncStore(): import('@korajs/sync').SyncStore {\n\t\tconst store = this.store\n\t\tconst mergeEngine = this.mergeEngine\n\t\tconst emitter = this.emitter\n\n\t\treturn {\n\t\t\tgetVersionVector(): VersionVector {\n\t\t\t\treturn store.getVersionVector()\n\t\t\t},\n\t\t\tgetNodeId(): string {\n\t\t\t\treturn store.getNodeId()\n\t\t\t},\n\t\t\tasync getOperationRange(\n\t\t\t\tnodeId: string,\n\t\t\t\tfromSeq: number,\n\t\t\t\ttoSeq: number,\n\t\t\t): Promise<Operation[]> {\n\t\t\t\treturn store.getOperationRange(nodeId, fromSeq, toSeq)\n\t\t\t},\n\t\t\tasync applyRemoteOperation(op: Operation): Promise<import('@korajs/sync').ApplyResult> {\n\t\t\t\t// For the test harness, delegate directly to store.\n\t\t\t\t// Merge resolution happens inside the store's applyRemoteOperation.\n\t\t\t\treturn store.applyRemoteOperation(op)\n\t\t\t},\n\t\t}\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, so a microtask flush suffices.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\t// Multiple microtask flushes to allow async message processing to complete\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\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 class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition) {\n\t\tthis.store = new MemoryServerStore()\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion: schema.version,\n\t\t})\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 * 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 } from './test-network'\nexport type { TestNetwork, TestNetworkOptions } 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'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged } 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":"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","names":[]}
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":"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","names":["ChaosTransport"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korajs/test",
3
- "version": "0.4.0",
3
+ "version": "0.5.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,17 +23,26 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "better-sqlite3": "^12.8.0",
26
- "@korajs/core": "0.4.0",
27
- "@korajs/merge": "0.4.0",
28
- "@korajs/store": "0.4.0",
29
- "@korajs/sync": "0.4.0",
30
- "@korajs/server": "0.4.0"
26
+ "@korajs/core": "0.5.0",
27
+ "@korajs/merge": "0.5.0",
28
+ "@korajs/store": "0.5.0",
29
+ "@korajs/sync": "0.5.0",
30
+ "@korajs/server": "0.5.0"
31
+ },
32
+ "peerDependencies": {
33
+ "korajs": "0.5.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "korajs": {
37
+ "optional": true
38
+ }
31
39
  },
32
40
  "devDependencies": {
33
41
  "@types/better-sqlite3": "^7.6.13",
34
42
  "tsup": "^8.3.6",
35
43
  "typescript": "^5.7.3",
36
- "vitest": "^3.0.4"
44
+ "vitest": "^3.0.4",
45
+ "yjs": "^13.6.30"
37
46
  },
38
47
  "license": "MIT",
39
48
  "scripts": {