@korajs/test 1.0.0-beta.0 → 1.0.0-beta.1
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 +71 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -2
- package/dist/index.d.ts +49 -2
- package/dist/index.js +84 -4
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,8 @@ var TestDevice = class {
|
|
|
61
61
|
name;
|
|
62
62
|
store;
|
|
63
63
|
emitter;
|
|
64
|
+
/** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
|
|
65
|
+
blobStore = new import_store.MemoryBlobStore();
|
|
64
66
|
schema;
|
|
65
67
|
server;
|
|
66
68
|
mergeEngine;
|
|
@@ -71,6 +73,7 @@ var TestDevice = class {
|
|
|
71
73
|
operationTransforms;
|
|
72
74
|
applyPipeline = null;
|
|
73
75
|
syncEngine = null;
|
|
76
|
+
blobChunkProvider = null;
|
|
74
77
|
currentTransport = null;
|
|
75
78
|
unsubscribeSync = null;
|
|
76
79
|
unsubscribeAudit = null;
|
|
@@ -134,6 +137,10 @@ var TestDevice = class {
|
|
|
134
137
|
emitter: this.emitter
|
|
135
138
|
});
|
|
136
139
|
conflictHandler.fn = () => this.syncEngine?.recordConflict();
|
|
140
|
+
const chunkPort = (0, import_testing.createSyncEngineChunkPort)(this.syncEngine);
|
|
141
|
+
(0, import_store.serveBlobChunks)(chunkPort, this.blobStore);
|
|
142
|
+
this.blobChunkProvider = (0, import_store.createRemoteChunkProvider)(chunkPort);
|
|
143
|
+
(0, import_testing.wireBlobUpload)(this.emitter, this.syncEngine, this.blobStore);
|
|
137
144
|
const engine = this.syncEngine;
|
|
138
145
|
this.unsubscribeSync = this.emitter.on("operation:created", (event) => {
|
|
139
146
|
if (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {
|
|
@@ -157,6 +164,7 @@ var TestDevice = class {
|
|
|
157
164
|
await this.syncEngine.stop();
|
|
158
165
|
this.syncEngine = null;
|
|
159
166
|
}
|
|
167
|
+
this.blobChunkProvider = null;
|
|
160
168
|
this.currentTransport = null;
|
|
161
169
|
}
|
|
162
170
|
/**
|
|
@@ -188,6 +196,57 @@ var TestDevice = class {
|
|
|
188
196
|
getSyncEngine() {
|
|
189
197
|
return this.syncEngine;
|
|
190
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Stage a blob's bytes into this device's blob store, splitting them into
|
|
201
|
+
* content-addressed chunks this device can then serve to peers over the sync
|
|
202
|
+
* connection. Returns the manifest (blob hash + ordered chunk hashes) needed
|
|
203
|
+
* to pull the blob elsewhere.
|
|
204
|
+
*/
|
|
205
|
+
async stageBlob(bytes, options) {
|
|
206
|
+
const { manifest } = await (0, import_store.prepareBlobForSend)(bytes, this.blobStore, options);
|
|
207
|
+
return manifest;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Store a blob for transfer the way `app.blobs.put` does: stage chunks, store
|
|
211
|
+
* the full blob, and store the manifest as its own content-addressed object.
|
|
212
|
+
* The returned reference carries a `manifestHash`, so a peer can pull the bytes
|
|
213
|
+
* knowing only the reference.
|
|
214
|
+
*/
|
|
215
|
+
async putBlob(bytes, options) {
|
|
216
|
+
return (0, import_store.putBlobForTransfer)(this.blobStore, bytes, options);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Pull a blob's bytes over the live connection knowing only its reference: the
|
|
220
|
+
* manifest is resolved by `ref.manifestHash` first, then the chunks are fetched.
|
|
221
|
+
*/
|
|
222
|
+
async pullBlobByRef(ref) {
|
|
223
|
+
if (!this.blobChunkProvider) {
|
|
224
|
+
throw new Error("Cannot pull a blob before the device has connected (call sync() first)");
|
|
225
|
+
}
|
|
226
|
+
const manifest = await (0, import_store.resolveBlobManifest)(this.blobChunkProvider, ref);
|
|
227
|
+
return (0, import_store.receiveBlob)(manifest, this.blobChunkProvider, {
|
|
228
|
+
chunkStore: this.blobStore,
|
|
229
|
+
blobStore: this.blobStore
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pull a blob's bytes from peers over the live sync connection. Requests only
|
|
234
|
+
* the chunks this device is missing, reassembles them, and verifies integrity
|
|
235
|
+
* against the manifest's blob hash before storing the full blob locally.
|
|
236
|
+
*/
|
|
237
|
+
async pullBlob(manifest) {
|
|
238
|
+
if (!this.blobChunkProvider) {
|
|
239
|
+
throw new Error("Cannot pull a blob before the device has connected (call sync() first)");
|
|
240
|
+
}
|
|
241
|
+
return (0, import_store.receiveBlob)(manifest, this.blobChunkProvider, {
|
|
242
|
+
chunkStore: this.blobStore,
|
|
243
|
+
blobStore: this.blobStore
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** Read fully-assembled blob bytes from this device's store, or null if absent. */
|
|
247
|
+
async getBlobBytes(hash) {
|
|
248
|
+
return this.blobStore.get(hash);
|
|
249
|
+
}
|
|
191
250
|
/**
|
|
192
251
|
* Get the device's version vector.
|
|
193
252
|
*/
|
|
@@ -240,19 +299,25 @@ var TestDevice = class {
|
|
|
240
299
|
// src/test-server.ts
|
|
241
300
|
var import_server = require("@korajs/server");
|
|
242
301
|
var import_server2 = require("@korajs/server");
|
|
302
|
+
var import_store2 = require("@korajs/store");
|
|
243
303
|
var TestServer = class {
|
|
244
304
|
store;
|
|
305
|
+
/** The server's central blob store, present when `blobStorage` was enabled. */
|
|
306
|
+
blobStore;
|
|
245
307
|
syncServer;
|
|
246
308
|
constructor(schema, options) {
|
|
247
309
|
this.store = new import_server.MemoryServerStore();
|
|
248
310
|
const schemaVersion = options?.schemaVersion ?? schema.version;
|
|
311
|
+
const blob = options?.blobStorage ? (0, import_store2.createMemoryServerBlobStore)() : null;
|
|
312
|
+
this.blobStore = blob?.store ?? null;
|
|
249
313
|
this.syncServer = new import_server2.KoraSyncServer({
|
|
250
314
|
store: this.store,
|
|
251
315
|
schemaVersion,
|
|
252
316
|
supportedSchemaVersions: options?.supportedSchemaVersions ?? {
|
|
253
317
|
min: schemaVersion,
|
|
254
318
|
max: schemaVersion
|
|
255
|
-
}
|
|
319
|
+
},
|
|
320
|
+
...blob ? blob.callbacks : {}
|
|
256
321
|
});
|
|
257
322
|
void this.store.setSchema(schema);
|
|
258
323
|
}
|
|
@@ -275,6 +340,10 @@ var TestServer = class {
|
|
|
275
340
|
getConnectionCount() {
|
|
276
341
|
return this.syncServer.getConnectionCount();
|
|
277
342
|
}
|
|
343
|
+
/** Every blob reference still reachable from live records on the server. */
|
|
344
|
+
getLiveBlobRefs() {
|
|
345
|
+
return this.syncServer.getLiveBlobRefs();
|
|
346
|
+
}
|
|
278
347
|
/**
|
|
279
348
|
* Shut down the server and close all sessions.
|
|
280
349
|
*/
|
|
@@ -289,7 +358,7 @@ async function createTestNetwork(schema, options) {
|
|
|
289
358
|
const deviceCount = options?.devices ?? 2;
|
|
290
359
|
const deviceNames = options?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`);
|
|
291
360
|
const tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "kora-test-"));
|
|
292
|
-
const server = new TestServer(schema);
|
|
361
|
+
const server = new TestServer(schema, { blobStorage: options?.blobStorage });
|
|
293
362
|
const devices = [];
|
|
294
363
|
for (const name of deviceNames) {
|
|
295
364
|
const device = new TestDevice({
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/test-network.ts","../src/test-device.ts","../src/test-server.ts","../src/assertions.ts","../src/protobuf-wire-transport.ts","../src/server-clock-transport.ts"],"sourcesContent":["// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Protobuf wire transport (for testing convergence through the real wire format) ===\nexport { wrapTransportPairWithProtobufWire } from './protobuf-wire-transport'\nexport type { TransportPair } from './protobuf-wire-transport'\n\n// === Server-clock transport (for testing clock-skew / rebase integration) ===\nexport { wrapTransportPairWithServerClock } from './server-clock-transport'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n","import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n\t/**\n\t * Optional wrapper applied to each device's transport pair after the base\n\t * (in-memory, optionally chaos-wrapped) pair is created. Use it to route\n\t * messages through the protobuf wire codec\n\t * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable\n\t * server clock. Called once per device connection.\n\t */\n\twrapTransport?: (pair: TransportPair) => TransportPair\n}\n\n/**\n * A test network with a server and multiple devices.\n */\nexport interface TestNetwork {\n\t/** The test server */\n\tserver: TestServer\n\t/** All devices in the network */\n\tdevices: TestDevice[]\n\t/** Temporary directory for DB files */\n\ttmpDir: string\n\t/** Close all devices and the server, clean up temp files */\n\tclose(): Promise<void>\n}\n\n/**\n * Create a test network with a server and multiple virtual devices.\n *\n * Each device has its own local SQLite store and SyncEngine. Devices\n * communicate with the server via in-memory transports.\n *\n * @param schema - The schema all devices share\n * @param options - Network configuration\n * @returns A test network with server and devices ready for use\n *\n * @example\n * ```typescript\n * const network = await createTestNetwork(schema, { devices: 2 })\n * const [deviceA, deviceB] = network.devices\n *\n * await deviceA.collection('todos').insert({ title: 'Hello' })\n * await deviceA.sync()\n * await deviceB.sync()\n *\n * const todos = await deviceB.getState('todos')\n * expect(todos).toHaveLength(1)\n *\n * await network.close()\n * ```\n */\nexport async function createTestNetwork(\n\tschema: SchemaDefinition,\n\toptions?: TestNetworkOptions,\n): Promise<TestNetwork> {\n\tconst deviceCount = options?.devices ?? 2\n\tconst deviceNames =\n\t\toptions?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`)\n\n\t// Create temp directory for DB files\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\n\t// Create server\n\tconst server = new TestServer(schema)\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\tconst base: TransportPair = {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t\treturn options?.wrapTransport ? options.wrapTransport(base) : base\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport { Store } from '@korajs/store'\nimport type { CollectionAccessor, StorageAdapter } from '@korajs/store'\nimport { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\nimport { SyncEngine } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport {\n\tApplyPipeline,\n\tMergeAwareSyncStore,\n\tStoreQueueStorage,\n\tStoreSyncStatePersistence,\n\twireAuditPersistence,\n} from 'korajs/testing'\nimport type { TestServer } from './test-server'\n\n/**\n * Options for creating a TestDevice.\n */\nexport interface TestDeviceOptions {\n\t/** Unique device name (used for DB file naming) */\n\tname: string\n\t/** Schema definition */\n\tschema: SchemaDefinition\n\t/** Test server to connect to */\n\tserver: TestServer\n\t/** Transport factory — creates a linked client/server transport pair */\n\tcreateTransportPair: () => {\n\t\tclient: SyncTransport\n\t\tserverTransport: import('@korajs/server').ServerTransport\n\t}\n\t/** Optional directory for temp DB files */\n\ttmpDir: string\n\t/** Client handshake schema version. Defaults to `schema.version`. */\n\tsyncSchemaVersion?: number\n\t/** Transforms applied to inbound operations before local apply. */\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * A virtual device in a test network.\n * Each device has its own Store (with real SQLite), SyncEngine, and MergeEngine.\n * Provides high-level methods for syncing, disconnecting, and inspecting state.\n */\nexport class TestDevice {\n\treadonly name: string\n\treadonly store: Store\n\treadonly emitter: KoraEventEmitter & { clear(): void }\n\n\tprivate readonly schema: SchemaDefinition\n\tprivate readonly server: TestServer\n\tprivate readonly mergeEngine: MergeEngine\n\tprivate readonly createTransportPair: TestDeviceOptions['createTransportPair']\n\tprivate readonly adapter: StorageAdapter\n\tprivate readonly dbPath: string\n\tprivate readonly syncSchemaVersion: number\n\tprivate readonly operationTransforms: OperationTransform[]\n\n\tprivate applyPipeline: ApplyPipeline | null = null\n\tprivate syncEngine: SyncEngine | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate unsubscribeAudit: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t\t// Match production wiring (createApp): merge/constraint traces persist to\n\t\t// `_kora_audit_traces`. Without this, harness devices emit merge events\n\t\t// but the durable audit trail every real app has stays empty — a fidelity\n\t\t// gap that hid from tests until Studio's Merges view made it visible.\n\t\tthis.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Wire local mutations to sync outbound queue\n\t\tconst engine = this.syncEngine\n\t\tthis.unsubscribeSync = this.emitter.on('operation:created', (event) => {\n\t\t\tif (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {\n\t\t\t\t// Catch async errors from push racing with disconnect during teardown\n\t\t\t\tthis.syncEngine.pushOperation(event.operation).catch(() => {})\n\t\t\t}\n\t\t})\n\n\t\t// Register server-side connection\n\t\tthis.server.handleConnection(serverTransport)\n\n\t\t// Start sync engine (connects, handshakes, exchanges deltas)\n\t\tawait this.syncEngine.start()\n\n\t\t// Wait for sync messages to propagate (in-memory transport is synchronous\n\t\t// but some processing is async)\n\t\tawait this.waitForSettled()\n\t}\n\n\t/**\n\t * Disconnect from the test server.\n\t */\n\tasync disconnect(): Promise<void> {\n\t\tif (this.unsubscribeSync) {\n\t\t\tthis.unsubscribeSync()\n\t\t\tthis.unsubscribeSync = null\n\t\t}\n\t\tif (this.syncEngine) {\n\t\t\tawait this.syncEngine.stop()\n\t\t\tthis.syncEngine = null\n\t\t}\n\t\tthis.currentTransport = null\n\t}\n\n\t/**\n\t * Reconnect to the test server after a disconnect.\n\t */\n\tasync reconnect(): Promise<void> {\n\t\tawait this.sync()\n\t}\n\n\t/**\n\t * Get a collection accessor for performing CRUD operations.\n\t */\n\tcollection(name: string): CollectionAccessor {\n\t\treturn this.store.collection(name)\n\t}\n\n\t/**\n\t * Get all records from a collection (convenience method).\n\t */\n\tasync getState(collectionName: string): Promise<Record<string, unknown>[]> {\n\t\tconst accessor = this.store.collection(collectionName)\n\t\treturn accessor.where({}).exec()\n\t}\n\n\t/**\n\t * Get the device's node ID.\n\t */\n\tgetNodeId(): string {\n\t\treturn this.store.getNodeId()\n\t}\n\n\t/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */\n\tgetSyncEngine(): SyncEngine | null {\n\t\treturn this.syncEngine\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tif (this.unsubscribeAudit) {\n\t\t\tthis.unsubscribeAudit()\n\t\t\tthis.unsubscribeAudit = null\n\t\t}\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\n\n/**\n * In-memory test server wrapping KoraSyncServer with MemoryServerStore.\n * Handles client connections via memory transports.\n */\nexport interface TestServerOptions {\n\t/** Handshake schema version advertised by the server. Defaults to `schema.version`. */\n\tschemaVersion?: number\n\t/** Inclusive client schema versions accepted at handshake. */\n\tsupportedSchemaVersions?: { min: number; max: number }\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition, options?: TestServerOptions) {\n\t\tthis.store = new MemoryServerStore()\n\t\tconst schemaVersion = options?.schemaVersion ?? schema.version\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion,\n\t\t\tsupportedSchemaVersions: options?.supportedSchemaVersions ?? {\n\t\t\t\tmin: schemaVersion,\n\t\t\t\tmax: schemaVersion,\n\t\t\t},\n\t\t})\n\t\tvoid this.store.setSchema(schema)\n\t}\n\n\t/**\n\t * Register a client connection transport with the server.\n\t * Returns the session ID assigned by the server.\n\t */\n\thandleConnection(transport: ServerTransport): string {\n\t\treturn this.syncServer.handleConnection(transport)\n\t}\n\n\t/**\n\t * Get all operations stored on the server.\n\t */\n\tgetAllOperations(): Operation[] {\n\t\treturn this.store.getAllOperations()\n\t}\n\n\t/**\n\t * Get the number of connected clients.\n\t */\n\tgetConnectionCount(): number {\n\t\treturn this.syncServer.getConnectionCount()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { MessageSerializer, SyncMessage, SyncTransport, TransportOptions } from '@korajs/sync'\nimport { ProtobufMessageSerializer } from '@korajs/sync'\n\n/**\n * A linked client/server transport pair, as consumed by {@link TestDevice}.\n */\nexport interface TransportPair {\n\tclient: SyncTransport\n\tserverTransport: ServerTransport\n}\n\n/**\n * Message types the protobuf serializer models. Ephemeral messages\n * (awareness-update, yjs-doc-update) are JSON-only in the protocol, so they are\n * deep-copied instead of forced through the protobuf envelope.\n */\nconst PROTOBUF_WIRE_TYPES: ReadonlySet<SyncMessage['type']> = new Set([\n\t'handshake',\n\t'handshake-response',\n\t'operation-batch',\n\t'acknowledgment',\n\t'error',\n])\n\n/**\n * Round-trips a message through the real protobuf wire codec: object → protobuf\n * bytes → object. This is what makes a transport pair exercise the true wire\n * format rather than passing live object references between the two ends.\n */\nfunction roundTripThroughProtobuf(\n\tmessage: SyncMessage,\n\tserializer: MessageSerializer,\n): SyncMessage {\n\tif (!PROTOBUF_WIRE_TYPES.has(message.type)) {\n\t\t// Not part of the protobuf schema — still copy so no live reference leaks\n\t\t// across the \"wire\".\n\t\treturn structuredClone(message)\n\t}\n\n\tconst encoded = serializer.encode(message)\n\tif (!(encoded instanceof Uint8Array)) {\n\t\tthrow new Error('Expected the protobuf serializer to encode to bytes')\n\t}\n\treturn serializer.decode(encoded)\n}\n\n/**\n * Wraps a client transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireClientTransport implements SyncTransport {\n\tconstructor(\n\t\tprivate readonly inner: SyncTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tconnect(url: string, options?: TransportOptions): Promise<void> {\n\t\treturn this.inner.connect(url, options)\n\t}\n\n\tdisconnect(): Promise<void> {\n\t\treturn this.inner.disconnect()\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n}\n\n/**\n * Wraps a server transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so that every sync message exchanged between the two\n * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for\n * a real network hop that uses the protobuf wire format.\n *\n * Both directions are wrapped, so operations, handshakes, batches, and acks all\n * travel as bytes rather than shared object references.\n *\n * @param pair - The underlying (in-memory) transport pair to wrap\n * @returns A transport pair that routes messages through the protobuf codec\n */\nexport function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair {\n\tconst serializer = new ProtobufMessageSerializer()\n\treturn {\n\t\tclient: new ProtobufWireClientTransport(pair.client, serializer),\n\t\tserverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer),\n\t}\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { SyncMessage } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\n\n/**\n * Wraps a server transport so its handshake responses advertise a caller-chosen\n * wall-clock time, independent of the process `Date.now()`.\n *\n * The sync engine measures clock skew as `serverTime - Date.now()` at handshake.\n * To simulate a device whose clock is fast while the server's clock is correct —\n * both running in one test process — the client mocks `Date.now()` and this\n * wrapper injects the server's true time into the handshake response, so the\n * skew the engine sees is real.\n */\nclass ServerClockServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serverNow: () => number,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tif (message.type === 'handshake-response') {\n\t\t\tthis.inner.send({ ...message, serverTime: this.serverNow() })\n\t\t\treturn\n\t\t}\n\t\tthis.inner.send(message)\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so the server side reports `serverNow()` as its\n * handshake wall-clock time.\n *\n * @param pair - The underlying transport pair\n * @param serverNow - Returns the server's true current time (ms since epoch)\n * @returns A transport pair whose handshake responses carry the injected time\n */\nexport function wrapTransportPairWithServerClock(\n\tpair: TransportPair,\n\tserverNow: () => number,\n): TransportPair {\n\treturn {\n\t\tclient: pair.client,\n\t\tserverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow),\n\t}\n}\n"],"mappings":"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","names":["import_internal","import_sync","import_server","import_sync","import_sync"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/test-network.ts","../src/test-device.ts","../src/test-server.ts","../src/assertions.ts","../src/protobuf-wire-transport.ts","../src/server-clock-transport.ts"],"sourcesContent":["// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Protobuf wire transport (for testing convergence through the real wire format) ===\nexport { wrapTransportPairWithProtobufWire } from './protobuf-wire-transport'\nexport type { TransportPair } from './protobuf-wire-transport'\n\n// === Server-clock transport (for testing clock-skew / rebase integration) ===\nexport { wrapTransportPairWithServerClock } from './server-clock-transport'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n","import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n\t/**\n\t * Optional wrapper applied to each device's transport pair after the base\n\t * (in-memory, optionally chaos-wrapped) pair is created. Use it to route\n\t * messages through the protobuf wire codec\n\t * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable\n\t * server clock. Called once per device connection.\n\t */\n\twrapTransport?: (pair: TransportPair) => TransportPair\n\t/** Enable central blob storage on the server (persist + serve uploaded blob bytes). */\n\tblobStorage?: boolean\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, { blobStorage: options?.blobStorage })\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\tconst base: TransportPair = {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t\treturn options?.wrapTransport ? options.wrapTransport(base) : base\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport type { BlobRef } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport {\n\tMemoryBlobStore,\n\tStore,\n\tcreateRemoteChunkProvider,\n\tprepareBlobForSend,\n\tputBlobForTransfer,\n\treceiveBlob,\n\tresolveBlobManifest,\n\tserveBlobChunks,\n} from '@korajs/store'\nimport type {\n\tBlobManifest,\n\tChunkProvider,\n\tCollectionAccessor,\n\tReceiveBlobResult,\n\tStorageAdapter,\n} 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\tcreateSyncEngineChunkPort,\n\twireAuditPersistence,\n\twireBlobUpload,\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\t/** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */\n\treadonly blobStore = new MemoryBlobStore()\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 blobChunkProvider: ChunkProvider | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate unsubscribeAudit: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t\t// Match production wiring (createApp): merge/constraint traces persist to\n\t\t// `_kora_audit_traces`. Without this, harness devices emit merge events\n\t\t// but the durable audit trail every real app has stays empty — a fidelity\n\t\t// gap that hid from tests until Studio's Merges view made it visible.\n\t\tthis.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Bind the blob chunk channel to this connection: serve chunks this device\n\t\t// holds, and prepare a provider to pull chunks it needs. Both ride the same\n\t\t// sync socket; the request/response handlers are disjoint by message type.\n\t\tconst chunkPort = createSyncEngineChunkPort(this.syncEngine)\n\t\tserveBlobChunks(chunkPort, this.blobStore)\n\t\tthis.blobChunkProvider = createRemoteChunkProvider(chunkPort)\n\t\t// Mirror createApp: auto-upload blob bytes to the server as ops sync, so\n\t\t// blobs stay available after this device disconnects.\n\t\twireBlobUpload(this.emitter, this.syncEngine, this.blobStore)\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.blobChunkProvider = null\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 * Stage a blob's bytes into this device's blob store, splitting them into\n\t * content-addressed chunks this device can then serve to peers over the sync\n\t * connection. Returns the manifest (blob hash + ordered chunk hashes) needed\n\t * to pull the blob elsewhere.\n\t */\n\tasync stageBlob(bytes: Uint8Array, options?: { chunkSize?: number }): Promise<BlobManifest> {\n\t\tconst { manifest } = await prepareBlobForSend(bytes, this.blobStore, options)\n\t\treturn manifest\n\t}\n\n\t/**\n\t * Store a blob for transfer the way `app.blobs.put` does: stage chunks, store\n\t * the full blob, and store the manifest as its own content-addressed object.\n\t * The returned reference carries a `manifestHash`, so a peer can pull the bytes\n\t * knowing only the reference.\n\t */\n\tasync putBlob(\n\t\tbytes: Uint8Array,\n\t\toptions?: { chunkSize?: number; mimeType?: string; filename?: string },\n\t): Promise<{ ref: BlobRef; manifest: BlobManifest }> {\n\t\treturn putBlobForTransfer(this.blobStore, bytes, options)\n\t}\n\n\t/**\n\t * Pull a blob's bytes over the live connection knowing only its reference: the\n\t * manifest is resolved by `ref.manifestHash` first, then the chunks are fetched.\n\t */\n\tasync pullBlobByRef(ref: BlobRef): Promise<ReceiveBlobResult> {\n\t\tif (!this.blobChunkProvider) {\n\t\t\tthrow new Error('Cannot pull a blob before the device has connected (call sync() first)')\n\t\t}\n\t\tconst manifest = await resolveBlobManifest(this.blobChunkProvider, ref)\n\t\treturn receiveBlob(manifest, this.blobChunkProvider, {\n\t\t\tchunkStore: this.blobStore,\n\t\t\tblobStore: this.blobStore,\n\t\t})\n\t}\n\n\t/**\n\t * Pull a blob's bytes from peers over the live sync connection. Requests only\n\t * the chunks this device is missing, reassembles them, and verifies integrity\n\t * against the manifest's blob hash before storing the full blob locally.\n\t */\n\tasync pullBlob(manifest: BlobManifest): Promise<ReceiveBlobResult> {\n\t\tif (!this.blobChunkProvider) {\n\t\t\tthrow new Error('Cannot pull a blob before the device has connected (call sync() first)')\n\t\t}\n\t\treturn receiveBlob(manifest, this.blobChunkProvider, {\n\t\t\tchunkStore: this.blobStore,\n\t\t\tblobStore: this.blobStore,\n\t\t})\n\t}\n\n\t/** Read fully-assembled blob bytes from this device's store, or null if absent. */\n\tasync getBlobBytes(hash: string): Promise<Uint8Array | null> {\n\t\treturn this.blobStore.get(hash)\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tif (this.unsubscribeAudit) {\n\t\t\tthis.unsubscribeAudit()\n\t\t\tthis.unsubscribeAudit = null\n\t\t}\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { BlobRef, Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\nimport { type ContentAddressedBlobStore, createMemoryServerBlobStore } from '@korajs/store'\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\t/** Enable central blob storage: the server persists and serves uploaded blob bytes. */\n\tblobStorage?: boolean\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\t/** The server's central blob store, present when `blobStorage` was enabled. */\n\treadonly blobStore: ContentAddressedBlobStore | null\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\tconst blob = options?.blobStorage ? createMemoryServerBlobStore() : null\n\t\tthis.blobStore = blob?.store ?? null\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\t...(blob ? blob.callbacks : {}),\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/** Every blob reference still reachable from live records on the server. */\n\tgetLiveBlobRefs(): Promise<BlobRef[]> {\n\t\treturn this.syncServer.getLiveBlobRefs()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { MessageSerializer, SyncMessage, SyncTransport, TransportOptions } from '@korajs/sync'\nimport { ProtobufMessageSerializer } from '@korajs/sync'\n\n/**\n * A linked client/server transport pair, as consumed by {@link TestDevice}.\n */\nexport interface TransportPair {\n\tclient: SyncTransport\n\tserverTransport: ServerTransport\n}\n\n/**\n * Message types the protobuf serializer models. Ephemeral messages\n * (awareness-update, yjs-doc-update) are JSON-only in the protocol, so they are\n * deep-copied instead of forced through the protobuf envelope.\n */\nconst PROTOBUF_WIRE_TYPES: ReadonlySet<SyncMessage['type']> = new Set([\n\t'handshake',\n\t'handshake-response',\n\t'operation-batch',\n\t'acknowledgment',\n\t'error',\n])\n\n/**\n * Round-trips a message through the real protobuf wire codec: object → protobuf\n * bytes → object. This is what makes a transport pair exercise the true wire\n * format rather than passing live object references between the two ends.\n */\nfunction roundTripThroughProtobuf(\n\tmessage: SyncMessage,\n\tserializer: MessageSerializer,\n): SyncMessage {\n\tif (!PROTOBUF_WIRE_TYPES.has(message.type)) {\n\t\t// Not part of the protobuf schema — still copy so no live reference leaks\n\t\t// across the \"wire\".\n\t\treturn structuredClone(message)\n\t}\n\n\tconst encoded = serializer.encode(message)\n\tif (!(encoded instanceof Uint8Array)) {\n\t\tthrow new Error('Expected the protobuf serializer to encode to bytes')\n\t}\n\treturn serializer.decode(encoded)\n}\n\n/**\n * Wraps a client transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireClientTransport implements SyncTransport {\n\tconstructor(\n\t\tprivate readonly inner: SyncTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tconnect(url: string, options?: TransportOptions): Promise<void> {\n\t\treturn this.inner.connect(url, options)\n\t}\n\n\tdisconnect(): Promise<void> {\n\t\treturn this.inner.disconnect()\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n}\n\n/**\n * Wraps a server transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so that every sync message exchanged between the two\n * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for\n * a real network hop that uses the protobuf wire format.\n *\n * Both directions are wrapped, so operations, handshakes, batches, and acks all\n * travel as bytes rather than shared object references.\n *\n * @param pair - The underlying (in-memory) transport pair to wrap\n * @returns A transport pair that routes messages through the protobuf codec\n */\nexport function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair {\n\tconst serializer = new ProtobufMessageSerializer()\n\treturn {\n\t\tclient: new ProtobufWireClientTransport(pair.client, serializer),\n\t\tserverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer),\n\t}\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { SyncMessage } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\n\n/**\n * Wraps a server transport so its handshake responses advertise a caller-chosen\n * wall-clock time, independent of the process `Date.now()`.\n *\n * The sync engine measures clock skew as `serverTime - Date.now()` at handshake.\n * To simulate a device whose clock is fast while the server's clock is correct —\n * both running in one test process — the client mocks `Date.now()` and this\n * wrapper injects the server's true time into the handshake response, so the\n * skew the engine sees is real.\n */\nclass ServerClockServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serverNow: () => number,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tif (message.type === 'handshake-response') {\n\t\t\tthis.inner.send({ ...message, serverTime: this.serverNow() })\n\t\t\treturn\n\t\t}\n\t\tthis.inner.send(message)\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so the server side reports `serverNow()` as its\n * handshake wall-clock time.\n *\n * @param pair - The underlying transport pair\n * @param serverNow - Returns the server's true current time (ms since epoch)\n * @returns A transport pair whose handshake responses carry the injected time\n */\nexport function wrapTransportPairWithServerClock(\n\tpair: TransportPair,\n\tserverNow: () => number,\n): TransportPair {\n\treturn {\n\t\tclient: pair.client,\n\t\tserverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow),\n\t}\n}\n"],"mappings":"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","names":["import_internal","import_sync","import_server","import_store","import_sync","import_sync"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { SchemaDefinition, Operation, KoraEventEmitter, OperationTransform, VersionVector } from '@korajs/core';
|
|
1
|
+
import { SchemaDefinition, Operation, BlobRef, KoraEventEmitter, OperationTransform, VersionVector } from '@korajs/core';
|
|
2
2
|
import { SyncTransport, SyncEngine, ChaosConfig } from '@korajs/sync';
|
|
3
3
|
export { ChaosConfig, ChaosTransport } from '@korajs/sync';
|
|
4
4
|
import * as _korajs_server from '@korajs/server';
|
|
5
5
|
import { ServerTransport, MemoryServerStore } from '@korajs/server';
|
|
6
|
-
import { Store, CollectionAccessor } from '@korajs/store';
|
|
6
|
+
import { ContentAddressedBlobStore, Store, MemoryBlobStore, CollectionAccessor, BlobManifest, ReceiveBlobResult } from '@korajs/store';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A linked client/server transport pair, as consumed by {@link TestDevice}.
|
|
@@ -37,9 +37,13 @@ interface TestServerOptions {
|
|
|
37
37
|
min: number;
|
|
38
38
|
max: number;
|
|
39
39
|
};
|
|
40
|
+
/** Enable central blob storage: the server persists and serves uploaded blob bytes. */
|
|
41
|
+
blobStorage?: boolean;
|
|
40
42
|
}
|
|
41
43
|
declare class TestServer {
|
|
42
44
|
readonly store: MemoryServerStore;
|
|
45
|
+
/** The server's central blob store, present when `blobStorage` was enabled. */
|
|
46
|
+
readonly blobStore: ContentAddressedBlobStore | null;
|
|
43
47
|
private readonly syncServer;
|
|
44
48
|
constructor(schema: SchemaDefinition, options?: TestServerOptions);
|
|
45
49
|
/**
|
|
@@ -55,6 +59,8 @@ declare class TestServer {
|
|
|
55
59
|
* Get the number of connected clients.
|
|
56
60
|
*/
|
|
57
61
|
getConnectionCount(): number;
|
|
62
|
+
/** Every blob reference still reachable from live records on the server. */
|
|
63
|
+
getLiveBlobRefs(): Promise<BlobRef[]>;
|
|
58
64
|
/**
|
|
59
65
|
* Shut down the server and close all sessions.
|
|
60
66
|
*/
|
|
@@ -94,6 +100,8 @@ declare class TestDevice {
|
|
|
94
100
|
readonly emitter: KoraEventEmitter & {
|
|
95
101
|
clear(): void;
|
|
96
102
|
};
|
|
103
|
+
/** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
|
|
104
|
+
readonly blobStore: MemoryBlobStore;
|
|
97
105
|
private readonly schema;
|
|
98
106
|
private readonly server;
|
|
99
107
|
private readonly mergeEngine;
|
|
@@ -104,6 +112,7 @@ declare class TestDevice {
|
|
|
104
112
|
private readonly operationTransforms;
|
|
105
113
|
private applyPipeline;
|
|
106
114
|
private syncEngine;
|
|
115
|
+
private blobChunkProvider;
|
|
107
116
|
private currentTransport;
|
|
108
117
|
private unsubscribeSync;
|
|
109
118
|
private unsubscribeAudit;
|
|
@@ -140,6 +149,42 @@ declare class TestDevice {
|
|
|
140
149
|
getNodeId(): string;
|
|
141
150
|
/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
|
|
142
151
|
getSyncEngine(): SyncEngine | null;
|
|
152
|
+
/**
|
|
153
|
+
* Stage a blob's bytes into this device's blob store, splitting them into
|
|
154
|
+
* content-addressed chunks this device can then serve to peers over the sync
|
|
155
|
+
* connection. Returns the manifest (blob hash + ordered chunk hashes) needed
|
|
156
|
+
* to pull the blob elsewhere.
|
|
157
|
+
*/
|
|
158
|
+
stageBlob(bytes: Uint8Array, options?: {
|
|
159
|
+
chunkSize?: number;
|
|
160
|
+
}): Promise<BlobManifest>;
|
|
161
|
+
/**
|
|
162
|
+
* Store a blob for transfer the way `app.blobs.put` does: stage chunks, store
|
|
163
|
+
* the full blob, and store the manifest as its own content-addressed object.
|
|
164
|
+
* The returned reference carries a `manifestHash`, so a peer can pull the bytes
|
|
165
|
+
* knowing only the reference.
|
|
166
|
+
*/
|
|
167
|
+
putBlob(bytes: Uint8Array, options?: {
|
|
168
|
+
chunkSize?: number;
|
|
169
|
+
mimeType?: string;
|
|
170
|
+
filename?: string;
|
|
171
|
+
}): Promise<{
|
|
172
|
+
ref: BlobRef;
|
|
173
|
+
manifest: BlobManifest;
|
|
174
|
+
}>;
|
|
175
|
+
/**
|
|
176
|
+
* Pull a blob's bytes over the live connection knowing only its reference: the
|
|
177
|
+
* manifest is resolved by `ref.manifestHash` first, then the chunks are fetched.
|
|
178
|
+
*/
|
|
179
|
+
pullBlobByRef(ref: BlobRef): Promise<ReceiveBlobResult>;
|
|
180
|
+
/**
|
|
181
|
+
* Pull a blob's bytes from peers over the live sync connection. Requests only
|
|
182
|
+
* the chunks this device is missing, reassembles them, and verifies integrity
|
|
183
|
+
* against the manifest's blob hash before storing the full blob locally.
|
|
184
|
+
*/
|
|
185
|
+
pullBlob(manifest: BlobManifest): Promise<ReceiveBlobResult>;
|
|
186
|
+
/** Read fully-assembled blob bytes from this device's store, or null if absent. */
|
|
187
|
+
getBlobBytes(hash: string): Promise<Uint8Array | null>;
|
|
143
188
|
/**
|
|
144
189
|
* Get the device's version vector.
|
|
145
190
|
*/
|
|
@@ -181,6 +226,8 @@ interface TestNetworkOptions {
|
|
|
181
226
|
* server clock. Called once per device connection.
|
|
182
227
|
*/
|
|
183
228
|
wrapTransport?: (pair: TransportPair) => TransportPair;
|
|
229
|
+
/** Enable central blob storage on the server (persist + serve uploaded blob bytes). */
|
|
230
|
+
blobStorage?: boolean;
|
|
184
231
|
}
|
|
185
232
|
/**
|
|
186
233
|
* A test network with a server and multiple devices.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { SchemaDefinition, Operation, KoraEventEmitter, OperationTransform, VersionVector } from '@korajs/core';
|
|
1
|
+
import { SchemaDefinition, Operation, BlobRef, KoraEventEmitter, OperationTransform, VersionVector } from '@korajs/core';
|
|
2
2
|
import { SyncTransport, SyncEngine, ChaosConfig } from '@korajs/sync';
|
|
3
3
|
export { ChaosConfig, ChaosTransport } from '@korajs/sync';
|
|
4
4
|
import * as _korajs_server from '@korajs/server';
|
|
5
5
|
import { ServerTransport, MemoryServerStore } from '@korajs/server';
|
|
6
|
-
import { Store, CollectionAccessor } from '@korajs/store';
|
|
6
|
+
import { ContentAddressedBlobStore, Store, MemoryBlobStore, CollectionAccessor, BlobManifest, ReceiveBlobResult } from '@korajs/store';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A linked client/server transport pair, as consumed by {@link TestDevice}.
|
|
@@ -37,9 +37,13 @@ interface TestServerOptions {
|
|
|
37
37
|
min: number;
|
|
38
38
|
max: number;
|
|
39
39
|
};
|
|
40
|
+
/** Enable central blob storage: the server persists and serves uploaded blob bytes. */
|
|
41
|
+
blobStorage?: boolean;
|
|
40
42
|
}
|
|
41
43
|
declare class TestServer {
|
|
42
44
|
readonly store: MemoryServerStore;
|
|
45
|
+
/** The server's central blob store, present when `blobStorage` was enabled. */
|
|
46
|
+
readonly blobStore: ContentAddressedBlobStore | null;
|
|
43
47
|
private readonly syncServer;
|
|
44
48
|
constructor(schema: SchemaDefinition, options?: TestServerOptions);
|
|
45
49
|
/**
|
|
@@ -55,6 +59,8 @@ declare class TestServer {
|
|
|
55
59
|
* Get the number of connected clients.
|
|
56
60
|
*/
|
|
57
61
|
getConnectionCount(): number;
|
|
62
|
+
/** Every blob reference still reachable from live records on the server. */
|
|
63
|
+
getLiveBlobRefs(): Promise<BlobRef[]>;
|
|
58
64
|
/**
|
|
59
65
|
* Shut down the server and close all sessions.
|
|
60
66
|
*/
|
|
@@ -94,6 +100,8 @@ declare class TestDevice {
|
|
|
94
100
|
readonly emitter: KoraEventEmitter & {
|
|
95
101
|
clear(): void;
|
|
96
102
|
};
|
|
103
|
+
/** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
|
|
104
|
+
readonly blobStore: MemoryBlobStore;
|
|
97
105
|
private readonly schema;
|
|
98
106
|
private readonly server;
|
|
99
107
|
private readonly mergeEngine;
|
|
@@ -104,6 +112,7 @@ declare class TestDevice {
|
|
|
104
112
|
private readonly operationTransforms;
|
|
105
113
|
private applyPipeline;
|
|
106
114
|
private syncEngine;
|
|
115
|
+
private blobChunkProvider;
|
|
107
116
|
private currentTransport;
|
|
108
117
|
private unsubscribeSync;
|
|
109
118
|
private unsubscribeAudit;
|
|
@@ -140,6 +149,42 @@ declare class TestDevice {
|
|
|
140
149
|
getNodeId(): string;
|
|
141
150
|
/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
|
|
142
151
|
getSyncEngine(): SyncEngine | null;
|
|
152
|
+
/**
|
|
153
|
+
* Stage a blob's bytes into this device's blob store, splitting them into
|
|
154
|
+
* content-addressed chunks this device can then serve to peers over the sync
|
|
155
|
+
* connection. Returns the manifest (blob hash + ordered chunk hashes) needed
|
|
156
|
+
* to pull the blob elsewhere.
|
|
157
|
+
*/
|
|
158
|
+
stageBlob(bytes: Uint8Array, options?: {
|
|
159
|
+
chunkSize?: number;
|
|
160
|
+
}): Promise<BlobManifest>;
|
|
161
|
+
/**
|
|
162
|
+
* Store a blob for transfer the way `app.blobs.put` does: stage chunks, store
|
|
163
|
+
* the full blob, and store the manifest as its own content-addressed object.
|
|
164
|
+
* The returned reference carries a `manifestHash`, so a peer can pull the bytes
|
|
165
|
+
* knowing only the reference.
|
|
166
|
+
*/
|
|
167
|
+
putBlob(bytes: Uint8Array, options?: {
|
|
168
|
+
chunkSize?: number;
|
|
169
|
+
mimeType?: string;
|
|
170
|
+
filename?: string;
|
|
171
|
+
}): Promise<{
|
|
172
|
+
ref: BlobRef;
|
|
173
|
+
manifest: BlobManifest;
|
|
174
|
+
}>;
|
|
175
|
+
/**
|
|
176
|
+
* Pull a blob's bytes over the live connection knowing only its reference: the
|
|
177
|
+
* manifest is resolved by `ref.manifestHash` first, then the chunks are fetched.
|
|
178
|
+
*/
|
|
179
|
+
pullBlobByRef(ref: BlobRef): Promise<ReceiveBlobResult>;
|
|
180
|
+
/**
|
|
181
|
+
* Pull a blob's bytes from peers over the live sync connection. Requests only
|
|
182
|
+
* the chunks this device is missing, reassembles them, and verifies integrity
|
|
183
|
+
* against the manifest's blob hash before storing the full blob locally.
|
|
184
|
+
*/
|
|
185
|
+
pullBlob(manifest: BlobManifest): Promise<ReceiveBlobResult>;
|
|
186
|
+
/** Read fully-assembled blob bytes from this device's store, or null if absent. */
|
|
187
|
+
getBlobBytes(hash: string): Promise<Uint8Array | null>;
|
|
143
188
|
/**
|
|
144
189
|
* Get the device's version vector.
|
|
145
190
|
*/
|
|
@@ -181,6 +226,8 @@ interface TestNetworkOptions {
|
|
|
181
226
|
* server clock. Called once per device connection.
|
|
182
227
|
*/
|
|
183
228
|
wrapTransport?: (pair: TransportPair) => TransportPair;
|
|
229
|
+
/** Enable central blob storage on the server (persist + serve uploaded blob bytes). */
|
|
230
|
+
blobStorage?: boolean;
|
|
184
231
|
}
|
|
185
232
|
/**
|
|
186
233
|
* A test network with a server and multiple devices.
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,16 @@ import { ChaosTransport } from "@korajs/sync";
|
|
|
8
8
|
// src/test-device.ts
|
|
9
9
|
import { SimpleEventEmitter } from "@korajs/core/internal";
|
|
10
10
|
import { MergeEngine } from "@korajs/merge";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
MemoryBlobStore,
|
|
13
|
+
Store,
|
|
14
|
+
createRemoteChunkProvider,
|
|
15
|
+
prepareBlobForSend,
|
|
16
|
+
putBlobForTransfer,
|
|
17
|
+
receiveBlob,
|
|
18
|
+
resolveBlobManifest,
|
|
19
|
+
serveBlobChunks
|
|
20
|
+
} from "@korajs/store";
|
|
12
21
|
import { BetterSqlite3Adapter } from "@korajs/store/better-sqlite3";
|
|
13
22
|
import { SyncEngine } from "@korajs/sync";
|
|
14
23
|
import {
|
|
@@ -16,12 +25,16 @@ import {
|
|
|
16
25
|
MergeAwareSyncStore,
|
|
17
26
|
StoreQueueStorage,
|
|
18
27
|
StoreSyncStatePersistence,
|
|
19
|
-
|
|
28
|
+
createSyncEngineChunkPort,
|
|
29
|
+
wireAuditPersistence,
|
|
30
|
+
wireBlobUpload
|
|
20
31
|
} from "korajs/testing";
|
|
21
32
|
var TestDevice = class {
|
|
22
33
|
name;
|
|
23
34
|
store;
|
|
24
35
|
emitter;
|
|
36
|
+
/** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
|
|
37
|
+
blobStore = new MemoryBlobStore();
|
|
25
38
|
schema;
|
|
26
39
|
server;
|
|
27
40
|
mergeEngine;
|
|
@@ -32,6 +45,7 @@ var TestDevice = class {
|
|
|
32
45
|
operationTransforms;
|
|
33
46
|
applyPipeline = null;
|
|
34
47
|
syncEngine = null;
|
|
48
|
+
blobChunkProvider = null;
|
|
35
49
|
currentTransport = null;
|
|
36
50
|
unsubscribeSync = null;
|
|
37
51
|
unsubscribeAudit = null;
|
|
@@ -95,6 +109,10 @@ var TestDevice = class {
|
|
|
95
109
|
emitter: this.emitter
|
|
96
110
|
});
|
|
97
111
|
conflictHandler.fn = () => this.syncEngine?.recordConflict();
|
|
112
|
+
const chunkPort = createSyncEngineChunkPort(this.syncEngine);
|
|
113
|
+
serveBlobChunks(chunkPort, this.blobStore);
|
|
114
|
+
this.blobChunkProvider = createRemoteChunkProvider(chunkPort);
|
|
115
|
+
wireBlobUpload(this.emitter, this.syncEngine, this.blobStore);
|
|
98
116
|
const engine = this.syncEngine;
|
|
99
117
|
this.unsubscribeSync = this.emitter.on("operation:created", (event) => {
|
|
100
118
|
if (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {
|
|
@@ -118,6 +136,7 @@ var TestDevice = class {
|
|
|
118
136
|
await this.syncEngine.stop();
|
|
119
137
|
this.syncEngine = null;
|
|
120
138
|
}
|
|
139
|
+
this.blobChunkProvider = null;
|
|
121
140
|
this.currentTransport = null;
|
|
122
141
|
}
|
|
123
142
|
/**
|
|
@@ -149,6 +168,57 @@ var TestDevice = class {
|
|
|
149
168
|
getSyncEngine() {
|
|
150
169
|
return this.syncEngine;
|
|
151
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Stage a blob's bytes into this device's blob store, splitting them into
|
|
173
|
+
* content-addressed chunks this device can then serve to peers over the sync
|
|
174
|
+
* connection. Returns the manifest (blob hash + ordered chunk hashes) needed
|
|
175
|
+
* to pull the blob elsewhere.
|
|
176
|
+
*/
|
|
177
|
+
async stageBlob(bytes, options) {
|
|
178
|
+
const { manifest } = await prepareBlobForSend(bytes, this.blobStore, options);
|
|
179
|
+
return manifest;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Store a blob for transfer the way `app.blobs.put` does: stage chunks, store
|
|
183
|
+
* the full blob, and store the manifest as its own content-addressed object.
|
|
184
|
+
* The returned reference carries a `manifestHash`, so a peer can pull the bytes
|
|
185
|
+
* knowing only the reference.
|
|
186
|
+
*/
|
|
187
|
+
async putBlob(bytes, options) {
|
|
188
|
+
return putBlobForTransfer(this.blobStore, bytes, options);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pull a blob's bytes over the live connection knowing only its reference: the
|
|
192
|
+
* manifest is resolved by `ref.manifestHash` first, then the chunks are fetched.
|
|
193
|
+
*/
|
|
194
|
+
async pullBlobByRef(ref) {
|
|
195
|
+
if (!this.blobChunkProvider) {
|
|
196
|
+
throw new Error("Cannot pull a blob before the device has connected (call sync() first)");
|
|
197
|
+
}
|
|
198
|
+
const manifest = await resolveBlobManifest(this.blobChunkProvider, ref);
|
|
199
|
+
return receiveBlob(manifest, this.blobChunkProvider, {
|
|
200
|
+
chunkStore: this.blobStore,
|
|
201
|
+
blobStore: this.blobStore
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Pull a blob's bytes from peers over the live sync connection. Requests only
|
|
206
|
+
* the chunks this device is missing, reassembles them, and verifies integrity
|
|
207
|
+
* against the manifest's blob hash before storing the full blob locally.
|
|
208
|
+
*/
|
|
209
|
+
async pullBlob(manifest) {
|
|
210
|
+
if (!this.blobChunkProvider) {
|
|
211
|
+
throw new Error("Cannot pull a blob before the device has connected (call sync() first)");
|
|
212
|
+
}
|
|
213
|
+
return receiveBlob(manifest, this.blobChunkProvider, {
|
|
214
|
+
chunkStore: this.blobStore,
|
|
215
|
+
blobStore: this.blobStore
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Read fully-assembled blob bytes from this device's store, or null if absent. */
|
|
219
|
+
async getBlobBytes(hash) {
|
|
220
|
+
return this.blobStore.get(hash);
|
|
221
|
+
}
|
|
152
222
|
/**
|
|
153
223
|
* Get the device's version vector.
|
|
154
224
|
*/
|
|
@@ -201,19 +271,25 @@ var TestDevice = class {
|
|
|
201
271
|
// src/test-server.ts
|
|
202
272
|
import { MemoryServerStore } from "@korajs/server";
|
|
203
273
|
import { KoraSyncServer } from "@korajs/server";
|
|
274
|
+
import { createMemoryServerBlobStore } from "@korajs/store";
|
|
204
275
|
var TestServer = class {
|
|
205
276
|
store;
|
|
277
|
+
/** The server's central blob store, present when `blobStorage` was enabled. */
|
|
278
|
+
blobStore;
|
|
206
279
|
syncServer;
|
|
207
280
|
constructor(schema, options) {
|
|
208
281
|
this.store = new MemoryServerStore();
|
|
209
282
|
const schemaVersion = options?.schemaVersion ?? schema.version;
|
|
283
|
+
const blob = options?.blobStorage ? createMemoryServerBlobStore() : null;
|
|
284
|
+
this.blobStore = blob?.store ?? null;
|
|
210
285
|
this.syncServer = new KoraSyncServer({
|
|
211
286
|
store: this.store,
|
|
212
287
|
schemaVersion,
|
|
213
288
|
supportedSchemaVersions: options?.supportedSchemaVersions ?? {
|
|
214
289
|
min: schemaVersion,
|
|
215
290
|
max: schemaVersion
|
|
216
|
-
}
|
|
291
|
+
},
|
|
292
|
+
...blob ? blob.callbacks : {}
|
|
217
293
|
});
|
|
218
294
|
void this.store.setSchema(schema);
|
|
219
295
|
}
|
|
@@ -236,6 +312,10 @@ var TestServer = class {
|
|
|
236
312
|
getConnectionCount() {
|
|
237
313
|
return this.syncServer.getConnectionCount();
|
|
238
314
|
}
|
|
315
|
+
/** Every blob reference still reachable from live records on the server. */
|
|
316
|
+
getLiveBlobRefs() {
|
|
317
|
+
return this.syncServer.getLiveBlobRefs();
|
|
318
|
+
}
|
|
239
319
|
/**
|
|
240
320
|
* Shut down the server and close all sessions.
|
|
241
321
|
*/
|
|
@@ -250,7 +330,7 @@ async function createTestNetwork(schema, options) {
|
|
|
250
330
|
const deviceCount = options?.devices ?? 2;
|
|
251
331
|
const deviceNames = options?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`);
|
|
252
332
|
const tmpDir = mkdtempSync(join(tmpdir(), "kora-test-"));
|
|
253
|
-
const server = new TestServer(schema);
|
|
333
|
+
const server = new TestServer(schema, { blobStorage: options?.blobStorage });
|
|
254
334
|
const devices = [];
|
|
255
335
|
for (const name of deviceNames) {
|
|
256
336
|
const device = new TestDevice({
|
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/protobuf-wire-transport.ts","../src/server-clock-transport.ts","../src/index.ts"],"sourcesContent":["import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n\t/**\n\t * Optional wrapper applied to each device's transport pair after the base\n\t * (in-memory, optionally chaos-wrapped) pair is created. Use it to route\n\t * messages through the protobuf wire codec\n\t * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable\n\t * server clock. Called once per device connection.\n\t */\n\twrapTransport?: (pair: TransportPair) => TransportPair\n}\n\n/**\n * A test network with a server and multiple devices.\n */\nexport interface TestNetwork {\n\t/** The test server */\n\tserver: TestServer\n\t/** All devices in the network */\n\tdevices: TestDevice[]\n\t/** Temporary directory for DB files */\n\ttmpDir: string\n\t/** Close all devices and the server, clean up temp files */\n\tclose(): Promise<void>\n}\n\n/**\n * Create a test network with a server and multiple virtual devices.\n *\n * Each device has its own local SQLite store and SyncEngine. Devices\n * communicate with the server via in-memory transports.\n *\n * @param schema - The schema all devices share\n * @param options - Network configuration\n * @returns A test network with server and devices ready for use\n *\n * @example\n * ```typescript\n * const network = await createTestNetwork(schema, { devices: 2 })\n * const [deviceA, deviceB] = network.devices\n *\n * await deviceA.collection('todos').insert({ title: 'Hello' })\n * await deviceA.sync()\n * await deviceB.sync()\n *\n * const todos = await deviceB.getState('todos')\n * expect(todos).toHaveLength(1)\n *\n * await network.close()\n * ```\n */\nexport async function createTestNetwork(\n\tschema: SchemaDefinition,\n\toptions?: TestNetworkOptions,\n): Promise<TestNetwork> {\n\tconst deviceCount = options?.devices ?? 2\n\tconst deviceNames =\n\t\toptions?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`)\n\n\t// Create temp directory for DB files\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\n\t// Create server\n\tconst server = new TestServer(schema)\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\tconst base: TransportPair = {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t\treturn options?.wrapTransport ? options.wrapTransport(base) : base\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport { Store } from '@korajs/store'\nimport type { CollectionAccessor, StorageAdapter } from '@korajs/store'\nimport { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\nimport { SyncEngine } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport {\n\tApplyPipeline,\n\tMergeAwareSyncStore,\n\tStoreQueueStorage,\n\tStoreSyncStatePersistence,\n\twireAuditPersistence,\n} from 'korajs/testing'\nimport type { TestServer } from './test-server'\n\n/**\n * Options for creating a TestDevice.\n */\nexport interface TestDeviceOptions {\n\t/** Unique device name (used for DB file naming) */\n\tname: string\n\t/** Schema definition */\n\tschema: SchemaDefinition\n\t/** Test server to connect to */\n\tserver: TestServer\n\t/** Transport factory — creates a linked client/server transport pair */\n\tcreateTransportPair: () => {\n\t\tclient: SyncTransport\n\t\tserverTransport: import('@korajs/server').ServerTransport\n\t}\n\t/** Optional directory for temp DB files */\n\ttmpDir: string\n\t/** Client handshake schema version. Defaults to `schema.version`. */\n\tsyncSchemaVersion?: number\n\t/** Transforms applied to inbound operations before local apply. */\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * A virtual device in a test network.\n * Each device has its own Store (with real SQLite), SyncEngine, and MergeEngine.\n * Provides high-level methods for syncing, disconnecting, and inspecting state.\n */\nexport class TestDevice {\n\treadonly name: string\n\treadonly store: Store\n\treadonly emitter: KoraEventEmitter & { clear(): void }\n\n\tprivate readonly schema: SchemaDefinition\n\tprivate readonly server: TestServer\n\tprivate readonly mergeEngine: MergeEngine\n\tprivate readonly createTransportPair: TestDeviceOptions['createTransportPair']\n\tprivate readonly adapter: StorageAdapter\n\tprivate readonly dbPath: string\n\tprivate readonly syncSchemaVersion: number\n\tprivate readonly operationTransforms: OperationTransform[]\n\n\tprivate applyPipeline: ApplyPipeline | null = null\n\tprivate syncEngine: SyncEngine | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate unsubscribeAudit: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t\t// Match production wiring (createApp): merge/constraint traces persist to\n\t\t// `_kora_audit_traces`. Without this, harness devices emit merge events\n\t\t// but the durable audit trail every real app has stays empty — a fidelity\n\t\t// gap that hid from tests until Studio's Merges view made it visible.\n\t\tthis.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Wire local mutations to sync outbound queue\n\t\tconst engine = this.syncEngine\n\t\tthis.unsubscribeSync = this.emitter.on('operation:created', (event) => {\n\t\t\tif (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {\n\t\t\t\t// Catch async errors from push racing with disconnect during teardown\n\t\t\t\tthis.syncEngine.pushOperation(event.operation).catch(() => {})\n\t\t\t}\n\t\t})\n\n\t\t// Register server-side connection\n\t\tthis.server.handleConnection(serverTransport)\n\n\t\t// Start sync engine (connects, handshakes, exchanges deltas)\n\t\tawait this.syncEngine.start()\n\n\t\t// Wait for sync messages to propagate (in-memory transport is synchronous\n\t\t// but some processing is async)\n\t\tawait this.waitForSettled()\n\t}\n\n\t/**\n\t * Disconnect from the test server.\n\t */\n\tasync disconnect(): Promise<void> {\n\t\tif (this.unsubscribeSync) {\n\t\t\tthis.unsubscribeSync()\n\t\t\tthis.unsubscribeSync = null\n\t\t}\n\t\tif (this.syncEngine) {\n\t\t\tawait this.syncEngine.stop()\n\t\t\tthis.syncEngine = null\n\t\t}\n\t\tthis.currentTransport = null\n\t}\n\n\t/**\n\t * Reconnect to the test server after a disconnect.\n\t */\n\tasync reconnect(): Promise<void> {\n\t\tawait this.sync()\n\t}\n\n\t/**\n\t * Get a collection accessor for performing CRUD operations.\n\t */\n\tcollection(name: string): CollectionAccessor {\n\t\treturn this.store.collection(name)\n\t}\n\n\t/**\n\t * Get all records from a collection (convenience method).\n\t */\n\tasync getState(collectionName: string): Promise<Record<string, unknown>[]> {\n\t\tconst accessor = this.store.collection(collectionName)\n\t\treturn accessor.where({}).exec()\n\t}\n\n\t/**\n\t * Get the device's node ID.\n\t */\n\tgetNodeId(): string {\n\t\treturn this.store.getNodeId()\n\t}\n\n\t/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */\n\tgetSyncEngine(): SyncEngine | null {\n\t\treturn this.syncEngine\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tif (this.unsubscribeAudit) {\n\t\t\tthis.unsubscribeAudit()\n\t\t\tthis.unsubscribeAudit = null\n\t\t}\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\n\n/**\n * In-memory test server wrapping KoraSyncServer with MemoryServerStore.\n * Handles client connections via memory transports.\n */\nexport interface TestServerOptions {\n\t/** Handshake schema version advertised by the server. Defaults to `schema.version`. */\n\tschemaVersion?: number\n\t/** Inclusive client schema versions accepted at handshake. */\n\tsupportedSchemaVersions?: { min: number; max: number }\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\tprivate readonly syncServer: KoraSyncServer\n\n\tconstructor(schema: SchemaDefinition, options?: TestServerOptions) {\n\t\tthis.store = new MemoryServerStore()\n\t\tconst schemaVersion = options?.schemaVersion ?? schema.version\n\t\tthis.syncServer = new KoraSyncServer({\n\t\t\tstore: this.store,\n\t\t\tschemaVersion,\n\t\t\tsupportedSchemaVersions: options?.supportedSchemaVersions ?? {\n\t\t\t\tmin: schemaVersion,\n\t\t\t\tmax: schemaVersion,\n\t\t\t},\n\t\t})\n\t\tvoid this.store.setSchema(schema)\n\t}\n\n\t/**\n\t * Register a client connection transport with the server.\n\t * Returns the session ID assigned by the server.\n\t */\n\thandleConnection(transport: ServerTransport): string {\n\t\treturn this.syncServer.handleConnection(transport)\n\t}\n\n\t/**\n\t * Get all operations stored on the server.\n\t */\n\tgetAllOperations(): Operation[] {\n\t\treturn this.store.getAllOperations()\n\t}\n\n\t/**\n\t * Get the number of connected clients.\n\t */\n\tgetConnectionCount(): number {\n\t\treturn this.syncServer.getConnectionCount()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { MessageSerializer, SyncMessage, SyncTransport, TransportOptions } from '@korajs/sync'\nimport { ProtobufMessageSerializer } from '@korajs/sync'\n\n/**\n * A linked client/server transport pair, as consumed by {@link TestDevice}.\n */\nexport interface TransportPair {\n\tclient: SyncTransport\n\tserverTransport: ServerTransport\n}\n\n/**\n * Message types the protobuf serializer models. Ephemeral messages\n * (awareness-update, yjs-doc-update) are JSON-only in the protocol, so they are\n * deep-copied instead of forced through the protobuf envelope.\n */\nconst PROTOBUF_WIRE_TYPES: ReadonlySet<SyncMessage['type']> = new Set([\n\t'handshake',\n\t'handshake-response',\n\t'operation-batch',\n\t'acknowledgment',\n\t'error',\n])\n\n/**\n * Round-trips a message through the real protobuf wire codec: object → protobuf\n * bytes → object. This is what makes a transport pair exercise the true wire\n * format rather than passing live object references between the two ends.\n */\nfunction roundTripThroughProtobuf(\n\tmessage: SyncMessage,\n\tserializer: MessageSerializer,\n): SyncMessage {\n\tif (!PROTOBUF_WIRE_TYPES.has(message.type)) {\n\t\t// Not part of the protobuf schema — still copy so no live reference leaks\n\t\t// across the \"wire\".\n\t\treturn structuredClone(message)\n\t}\n\n\tconst encoded = serializer.encode(message)\n\tif (!(encoded instanceof Uint8Array)) {\n\t\tthrow new Error('Expected the protobuf serializer to encode to bytes')\n\t}\n\treturn serializer.decode(encoded)\n}\n\n/**\n * Wraps a client transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireClientTransport implements SyncTransport {\n\tconstructor(\n\t\tprivate readonly inner: SyncTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tconnect(url: string, options?: TransportOptions): Promise<void> {\n\t\treturn this.inner.connect(url, options)\n\t}\n\n\tdisconnect(): Promise<void> {\n\t\treturn this.inner.disconnect()\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n}\n\n/**\n * Wraps a server transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so that every sync message exchanged between the two\n * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for\n * a real network hop that uses the protobuf wire format.\n *\n * Both directions are wrapped, so operations, handshakes, batches, and acks all\n * travel as bytes rather than shared object references.\n *\n * @param pair - The underlying (in-memory) transport pair to wrap\n * @returns A transport pair that routes messages through the protobuf codec\n */\nexport function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair {\n\tconst serializer = new ProtobufMessageSerializer()\n\treturn {\n\t\tclient: new ProtobufWireClientTransport(pair.client, serializer),\n\t\tserverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer),\n\t}\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { SyncMessage } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\n\n/**\n * Wraps a server transport so its handshake responses advertise a caller-chosen\n * wall-clock time, independent of the process `Date.now()`.\n *\n * The sync engine measures clock skew as `serverTime - Date.now()` at handshake.\n * To simulate a device whose clock is fast while the server's clock is correct —\n * both running in one test process — the client mocks `Date.now()` and this\n * wrapper injects the server's true time into the handshake response, so the\n * skew the engine sees is real.\n */\nclass ServerClockServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serverNow: () => number,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tif (message.type === 'handshake-response') {\n\t\t\tthis.inner.send({ ...message, serverTime: this.serverNow() })\n\t\t\treturn\n\t\t}\n\t\tthis.inner.send(message)\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so the server side reports `serverNow()` as its\n * handshake wall-clock time.\n *\n * @param pair - The underlying transport pair\n * @param serverNow - Returns the server's true current time (ms since epoch)\n * @returns A transport pair whose handshake responses carry the injected time\n */\nexport function wrapTransportPairWithServerClock(\n\tpair: TransportPair,\n\tserverNow: () => number,\n): TransportPair {\n\treturn {\n\t\tclient: pair.client,\n\t\tserverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow),\n\t}\n}\n","// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Protobuf wire transport (for testing convergence through the real wire format) ===\nexport { wrapTransportPairWithProtobufWire } from './protobuf-wire-transport'\nexport type { TransportPair } from './protobuf-wire-transport'\n\n// === Server-clock transport (for testing clock-skew / rebase integration) ===\nexport { wrapTransportPairWithServerClock } from './server-clock-transport'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n"],"mappings":"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","names":["ChaosTransport"]}
|
|
1
|
+
{"version":3,"sources":["../src/test-network.ts","../src/test-device.ts","../src/test-server.ts","../src/assertions.ts","../src/protobuf-wire-transport.ts","../src/server-clock-transport.ts","../src/index.ts"],"sourcesContent":["import { mkdtempSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport { createServerTransportPair } from '@korajs/server/internal'\nimport type { ChaosConfig } from '@korajs/sync'\nimport { ChaosTransport } from '@korajs/sync'\nimport type { SyncTransport } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\nimport { TestDevice } from './test-device'\nimport { TestServer, type TestServerOptions } from './test-server'\n\n/**\n * Options for creating a test network.\n */\nexport interface TestNetworkOptions {\n\t/** Number of devices to create. Defaults to 2. */\n\tdevices?: number\n\t/** Custom device names. If not provided, uses 'device-0', 'device-1', etc. */\n\tdeviceNames?: string[]\n\t/** Optional chaos transport settings applied to each device link. */\n\tchaos?: ChaosConfig\n\t/**\n\t * Optional wrapper applied to each device's transport pair after the base\n\t * (in-memory, optionally chaos-wrapped) pair is created. Use it to route\n\t * messages through the protobuf wire codec\n\t * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable\n\t * server clock. Called once per device connection.\n\t */\n\twrapTransport?: (pair: TransportPair) => TransportPair\n\t/** Enable central blob storage on the server (persist + serve uploaded blob bytes). */\n\tblobStorage?: boolean\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, { blobStorage: options?.blobStorage })\n\n\t// Create devices\n\tconst devices: TestDevice[] = []\n\tfor (const name of deviceNames) {\n\t\tconst device = new TestDevice({\n\t\t\tname,\n\t\t\tschema,\n\t\t\tserver,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\tconst client = pair.client as unknown as SyncTransport\n\t\t\t\tconst chaos = options?.chaos\n\t\t\t\tconst base: TransportPair = {\n\t\t\t\t\tclient: chaos ? new ChaosTransport(client, chaos) : client,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t\treturn options?.wrapTransport ? options.wrapTransport(base) : base\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\t// Clean up temp DB files\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * Per-device configuration when devices use different local schemas or sync settings.\n */\nexport interface MixedTestDeviceConfig {\n\tname: string\n\tschema: SchemaDefinition\n\tsyncSchemaVersion?: number\n\toperationTransforms?: OperationTransform[]\n}\n\n/**\n * Create a test network where devices may use different schema versions and transforms.\n * The server uses `serverSchema` for materialization and handshake bounds.\n */\nexport async function createMixedTestNetwork(\n\tserverSchema: SchemaDefinition,\n\tserverOptions: TestServerOptions,\n\tdeviceConfigs: MixedTestDeviceConfig[],\n): Promise<TestNetwork> {\n\tconst tmpDir = mkdtempSync(join(tmpdir(), 'kora-test-'))\n\tconst server = new TestServer(serverSchema, serverOptions)\n\n\tconst devices: TestDevice[] = []\n\tfor (const config of deviceConfigs) {\n\t\tconst device = new TestDevice({\n\t\t\tname: config.name,\n\t\t\tschema: config.schema,\n\t\t\tserver,\n\t\t\tsyncSchemaVersion: config.syncSchemaVersion,\n\t\t\toperationTransforms: config.operationTransforms,\n\t\t\tcreateTransportPair: () => {\n\t\t\t\tconst pair = createServerTransportPair()\n\t\t\t\treturn {\n\t\t\t\t\tclient: pair.client as unknown as SyncTransport,\n\t\t\t\t\tserverTransport: pair.server,\n\t\t\t\t}\n\t\t\t},\n\t\t\ttmpDir,\n\t\t})\n\t\tawait device.open()\n\t\tdevices.push(device)\n\t}\n\n\treturn {\n\t\tserver,\n\t\tdevices,\n\t\ttmpDir,\n\t\tasync close(): Promise<void> {\n\t\t\tfor (const device of devices) {\n\t\t\t\tawait device.close()\n\t\t\t}\n\t\t\tawait server.close()\n\t\t\ttry {\n\t\t\t\tconst { rmSync } = await import('node:fs')\n\t\t\t\trmSync(tmpDir, { recursive: true, force: true })\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t},\n\t}\n}\n","import type { OperationTransform, SchemaDefinition } from '@korajs/core'\nimport type { VersionVector } from '@korajs/core'\nimport type { KoraEventEmitter } from '@korajs/core'\nimport type { BlobRef } from '@korajs/core'\nimport { SimpleEventEmitter } from '@korajs/core/internal'\nimport { MergeEngine } from '@korajs/merge'\nimport {\n\tMemoryBlobStore,\n\tStore,\n\tcreateRemoteChunkProvider,\n\tprepareBlobForSend,\n\tputBlobForTransfer,\n\treceiveBlob,\n\tresolveBlobManifest,\n\tserveBlobChunks,\n} from '@korajs/store'\nimport type {\n\tBlobManifest,\n\tChunkProvider,\n\tCollectionAccessor,\n\tReceiveBlobResult,\n\tStorageAdapter,\n} 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\tcreateSyncEngineChunkPort,\n\twireAuditPersistence,\n\twireBlobUpload,\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\t/** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */\n\treadonly blobStore = new MemoryBlobStore()\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 blobChunkProvider: ChunkProvider | null = null\n\tprivate currentTransport: SyncTransport | null = null\n\tprivate unsubscribeSync: (() => void) | null = null\n\tprivate unsubscribeAudit: (() => void) | null = null\n\tprivate closing = false\n\n\tconstructor(options: TestDeviceOptions) {\n\t\tthis.name = options.name\n\t\tthis.schema = options.schema\n\t\tthis.server = options.server\n\t\tthis.createTransportPair = options.createTransportPair\n\t\tthis.dbPath = `${options.tmpDir}/test-device-${options.name}.db`\n\t\tthis.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version\n\t\tthis.operationTransforms = options.operationTransforms ?? []\n\n\t\tthis.emitter = new SimpleEventEmitter()\n\t\tthis.mergeEngine = new MergeEngine()\n\t\tthis.adapter = new BetterSqlite3Adapter(this.dbPath)\n\t\tthis.store = new Store({\n\t\t\tschema: options.schema,\n\t\t\tadapter: this.adapter,\n\t\t\temitter: this.emitter,\n\t\t})\n\t}\n\n\t/**\n\t * Open the store (must be called before sync or collection operations).\n\t */\n\tasync open(): Promise<void> {\n\t\tawait this.store.open()\n\t\tthis.applyPipeline = new ApplyPipeline({\n\t\t\tstore: this.store,\n\t\t\tmergeEngine: this.mergeEngine,\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tthis.store.setLocalMutationHandler(this.applyPipeline)\n\t\t// Match production wiring (createApp): merge/constraint traces persist to\n\t\t// `_kora_audit_traces`. Without this, harness devices emit merge events\n\t\t// but the durable audit trail every real app has stays empty — a fidelity\n\t\t// gap that hid from tests until Studio's Merges view made it visible.\n\t\tthis.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter)\n\t}\n\n\t/**\n\t * Connect to the test server and perform initial sync.\n\t * If already connected, flushes any pending operations.\n\t */\n\tasync sync(): Promise<void> {\n\t\tif (this.syncEngine && this.currentTransport?.isConnected()) {\n\t\t\t// Already connected — flush outbound ops, then allow inbound relay to settle\n\t\t\tawait this.waitForPendingOps()\n\t\t\tawait this.waitForSettled()\n\t\t\treturn\n\t\t}\n\n\t\t// Create a new transport pair and connect\n\t\tconst { client, serverTransport } = this.createTransportPair()\n\t\tthis.currentTransport = client\n\n\t\tconst conflictHandler: { fn?: () => void } = {}\n\t\tconst syncStore = new MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {\n\t\t\tonMergeConflict: () => conflictHandler.fn?.(),\n\t\t})\n\n\t\tthis.syncEngine = new SyncEngine({\n\t\t\ttransport: client,\n\t\t\tstore: syncStore,\n\t\t\tqueueStorage: new StoreQueueStorage(this.adapter),\n\t\t\tsyncState: new StoreSyncStatePersistence(this.store),\n\t\t\tconfig: {\n\t\t\t\turl: 'ws://test-network',\n\t\t\t\tschemaVersion: this.syncSchemaVersion,\n\t\t\t\toperationTransforms:\n\t\t\t\t\tthis.operationTransforms.length > 0 ? this.operationTransforms : undefined,\n\t\t\t},\n\t\t\temitter: this.emitter,\n\t\t})\n\t\tconflictHandler.fn = () => this.syncEngine?.recordConflict()\n\n\t\t// Bind the blob chunk channel to this connection: serve chunks this device\n\t\t// holds, and prepare a provider to pull chunks it needs. Both ride the same\n\t\t// sync socket; the request/response handlers are disjoint by message type.\n\t\tconst chunkPort = createSyncEngineChunkPort(this.syncEngine)\n\t\tserveBlobChunks(chunkPort, this.blobStore)\n\t\tthis.blobChunkProvider = createRemoteChunkProvider(chunkPort)\n\t\t// Mirror createApp: auto-upload blob bytes to the server as ops sync, so\n\t\t// blobs stay available after this device disconnects.\n\t\twireBlobUpload(this.emitter, this.syncEngine, this.blobStore)\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.blobChunkProvider = null\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 * Stage a blob's bytes into this device's blob store, splitting them into\n\t * content-addressed chunks this device can then serve to peers over the sync\n\t * connection. Returns the manifest (blob hash + ordered chunk hashes) needed\n\t * to pull the blob elsewhere.\n\t */\n\tasync stageBlob(bytes: Uint8Array, options?: { chunkSize?: number }): Promise<BlobManifest> {\n\t\tconst { manifest } = await prepareBlobForSend(bytes, this.blobStore, options)\n\t\treturn manifest\n\t}\n\n\t/**\n\t * Store a blob for transfer the way `app.blobs.put` does: stage chunks, store\n\t * the full blob, and store the manifest as its own content-addressed object.\n\t * The returned reference carries a `manifestHash`, so a peer can pull the bytes\n\t * knowing only the reference.\n\t */\n\tasync putBlob(\n\t\tbytes: Uint8Array,\n\t\toptions?: { chunkSize?: number; mimeType?: string; filename?: string },\n\t): Promise<{ ref: BlobRef; manifest: BlobManifest }> {\n\t\treturn putBlobForTransfer(this.blobStore, bytes, options)\n\t}\n\n\t/**\n\t * Pull a blob's bytes over the live connection knowing only its reference: the\n\t * manifest is resolved by `ref.manifestHash` first, then the chunks are fetched.\n\t */\n\tasync pullBlobByRef(ref: BlobRef): Promise<ReceiveBlobResult> {\n\t\tif (!this.blobChunkProvider) {\n\t\t\tthrow new Error('Cannot pull a blob before the device has connected (call sync() first)')\n\t\t}\n\t\tconst manifest = await resolveBlobManifest(this.blobChunkProvider, ref)\n\t\treturn receiveBlob(manifest, this.blobChunkProvider, {\n\t\t\tchunkStore: this.blobStore,\n\t\t\tblobStore: this.blobStore,\n\t\t})\n\t}\n\n\t/**\n\t * Pull a blob's bytes from peers over the live sync connection. Requests only\n\t * the chunks this device is missing, reassembles them, and verifies integrity\n\t * against the manifest's blob hash before storing the full blob locally.\n\t */\n\tasync pullBlob(manifest: BlobManifest): Promise<ReceiveBlobResult> {\n\t\tif (!this.blobChunkProvider) {\n\t\t\tthrow new Error('Cannot pull a blob before the device has connected (call sync() first)')\n\t\t}\n\t\treturn receiveBlob(manifest, this.blobChunkProvider, {\n\t\t\tchunkStore: this.blobStore,\n\t\t\tblobStore: this.blobStore,\n\t\t})\n\t}\n\n\t/** Read fully-assembled blob bytes from this device's store, or null if absent. */\n\tasync getBlobBytes(hash: string): Promise<Uint8Array | null> {\n\t\treturn this.blobStore.get(hash)\n\t}\n\n\t/**\n\t * Get the device's version vector.\n\t */\n\tgetVersionVector(): VersionVector {\n\t\treturn this.store.getVersionVector()\n\t}\n\n\t/**\n\t * Check if the device is currently connected to the server.\n\t */\n\tisConnected(): boolean {\n\t\treturn this.currentTransport?.isConnected() ?? false\n\t}\n\n\t/**\n\t * Close the device, releasing all resources.\n\t */\n\tasync close(): Promise<void> {\n\t\tthis.closing = true\n\t\tif (this.unsubscribeAudit) {\n\t\t\tthis.unsubscribeAudit()\n\t\t\tthis.unsubscribeAudit = null\n\t\t}\n\t\tawait this.disconnect()\n\t\tawait this.store.close()\n\t\tthis.emitter.clear()\n\t}\n\n\t/**\n\t * Wait for in-flight sync operations to settle.\n\t * In-memory transports are near-synchronous, but apply/relay work is async.\n\t */\n\tprivate async waitForSettled(): Promise<void> {\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 20))\n\t\t}\n\t}\n\n\t/**\n\t * Wait for all pending outbound operations to be acknowledged.\n\t */\n\tprivate async waitForPendingOps(): Promise<void> {\n\t\tif (!this.syncEngine) return\n\t\tconst maxWait = 2000\n\t\tconst start = Date.now()\n\t\twhile (Date.now() - start < maxWait) {\n\t\t\tconst status = this.syncEngine.getStatus()\n\t\t\tif (status.pendingOperations === 0) return\n\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, 10))\n\t\t}\n\t}\n}\n","import type { BlobRef, Operation, SchemaDefinition } from '@korajs/core'\nimport { MemoryServerStore } from '@korajs/server'\nimport { KoraSyncServer } from '@korajs/server'\nimport type { ServerTransport } from '@korajs/server'\nimport { type ContentAddressedBlobStore, createMemoryServerBlobStore } from '@korajs/store'\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\t/** Enable central blob storage: the server persists and serves uploaded blob bytes. */\n\tblobStorage?: boolean\n}\n\nexport class TestServer {\n\treadonly store: MemoryServerStore\n\t/** The server's central blob store, present when `blobStorage` was enabled. */\n\treadonly blobStore: ContentAddressedBlobStore | null\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\tconst blob = options?.blobStorage ? createMemoryServerBlobStore() : null\n\t\tthis.blobStore = blob?.store ?? null\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\t...(blob ? blob.callbacks : {}),\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/** Every blob reference still reachable from live records on the server. */\n\tgetLiveBlobRefs(): Promise<BlobRef[]> {\n\t\treturn this.syncServer.getLiveBlobRefs()\n\t}\n\n\t/**\n\t * Shut down the server and close all sessions.\n\t */\n\tasync close(): Promise<void> {\n\t\tawait this.syncServer.stop()\n\t\tawait this.store.close()\n\t}\n}\n","import type { SchemaDefinition } from '@korajs/core'\nimport type { TestDevice } from './test-device'\n\n/**\n * Result of a convergence check.\n */\nexport interface ConvergenceResult {\n\t/** Whether all devices have converged to the same state */\n\tconverged: boolean\n\t/** Per-collection comparison details (only populated on failure) */\n\tdifferences: CollectionDifference[]\n}\n\n/**\n * Describes a difference in a collection between devices.\n */\nexport interface CollectionDifference {\n\tcollection: string\n\tdeviceA: string\n\tdeviceB: string\n\t/** Records present in deviceA but not deviceB */\n\tmissingInB: string[]\n\t/** Records present in deviceB but not deviceA */\n\tmissingInA: string[]\n\t/** Records present in both but with different values */\n\tfieldDifferences: FieldDifference[]\n}\n\n/**\n * A specific field-level difference between two devices.\n */\nexport interface FieldDifference {\n\trecordId: string\n\tfield: string\n\tvalueInA: unknown\n\tvalueInB: unknown\n}\n\n/**\n * Assert that all devices have converged to identical collection states.\n *\n * Compares every collection across all device pairs. Throws an error\n * with detailed diagnostics if any differences are found.\n *\n * @param devices - The devices to check for convergence\n * @param schema - The schema (used to enumerate collections)\n *\n * @example\n * ```typescript\n * await deviceA.sync()\n * await deviceB.sync()\n * await expectConverged([deviceA, deviceB], schema)\n * ```\n */\nexport async function expectConverged(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst result = await checkConvergence(devices, schema)\n\tif (!result.converged) {\n\t\tconst details = result.differences\n\t\t\t.map((d) => {\n\t\t\t\tconst parts: string[] = [\n\t\t\t\t\t` Collection \"${d.collection}\" differs between ${d.deviceA} and ${d.deviceB}:`,\n\t\t\t\t]\n\t\t\t\tif (d.missingInB.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tif (d.missingInA.length > 0) {\n\t\t\t\t\tparts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(', ')}`)\n\t\t\t\t}\n\t\t\t\tfor (const fd of d.fieldDifferences) {\n\t\t\t\t\tparts.push(\n\t\t\t\t\t\t` Record \"${fd.recordId}\" field \"${fd.field}\": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\treturn parts.join('\\n')\n\t\t\t})\n\t\t\t.join('\\n')\n\n\t\tthrow new Error(`Devices have not converged:\\n${details}`)\n\t}\n}\n\n/**\n * Poll until all devices converge or timeout. Use after sync in integration tests\n * where relay/apply work may still be in flight under parallel CI load.\n */\nexport async function expectConvergedEventually(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n\toptions?: { timeoutMs?: number; intervalMs?: number },\n): Promise<void> {\n\tif (devices.length < 2) return\n\n\tconst timeoutMs = options?.timeoutMs ?? 5000\n\tconst intervalMs = options?.intervalMs ?? 25\n\tconst deadline = Date.now() + timeoutMs\n\n\twhile (Date.now() < deadline) {\n\t\tconst result = await checkConvergence(devices, schema)\n\t\tif (result.converged) {\n\t\t\treturn\n\t\t}\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, intervalMs))\n\t}\n\n\tawait expectConverged(devices, schema)\n}\n\n/**\n * Check whether all devices have converged without throwing.\n *\n * @param devices - The devices to check\n * @param schema - The schema (for collection enumeration)\n * @returns Convergence result with details\n */\nexport async function checkConvergence(\n\tdevices: TestDevice[],\n\tschema: SchemaDefinition,\n): Promise<ConvergenceResult> {\n\tconst differences: CollectionDifference[] = []\n\tconst collectionNames = Object.keys(schema.collections)\n\n\tfor (let i = 0; i < devices.length - 1; i++) {\n\t\tfor (let j = i + 1; j < devices.length; j++) {\n\t\t\tconst deviceA = devices[i] as TestDevice\n\t\t\tconst deviceB = devices[j] as TestDevice\n\n\t\t\tfor (const collection of collectionNames) {\n\t\t\t\tconst stateA = await deviceA.getState(collection)\n\t\t\t\tconst stateB = await deviceB.getState(collection)\n\n\t\t\t\tconst diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB)\n\n\t\t\t\tif (diff) {\n\t\t\t\t\tdifferences.push(diff)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tconverged: differences.length === 0,\n\t\tdifferences,\n\t}\n}\n\n/**\n * Compare two collection states and return differences if any.\n */\nfunction compareCollectionStates(\n\tcollection: string,\n\tnameA: string,\n\tnameB: string,\n\tstateA: Record<string, unknown>[],\n\tstateB: Record<string, unknown>[],\n): CollectionDifference | null {\n\tconst mapA = new Map(stateA.map((r) => [r.id as string, r]))\n\tconst mapB = new Map(stateB.map((r) => [r.id as string, r]))\n\n\tconst missingInB: string[] = []\n\tconst missingInA: string[] = []\n\tconst fieldDifferences: FieldDifference[] = []\n\n\t// Check records in A\n\tfor (const [id, recordA] of mapA) {\n\t\tconst recordB = mapB.get(id)\n\t\tif (!recordB) {\n\t\t\tmissingInB.push(id)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Compare fields (skip internal fields like _created_at, _updated_at)\n\t\tconst allFields = new Set([\n\t\t\t...Object.keys(recordA).filter((k) => !k.startsWith('_')),\n\t\t\t...Object.keys(recordB).filter((k) => !k.startsWith('_')),\n\t\t])\n\n\t\tfor (const field of allFields) {\n\t\t\tif (field === 'id') continue\n\t\t\tconst valA = recordA[field]\n\t\t\tconst valB = recordB[field]\n\t\t\tif (!deepEqual(valA, valB)) {\n\t\t\t\tfieldDifferences.push({\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tfield,\n\t\t\t\t\tvalueInA: valA,\n\t\t\t\t\tvalueInB: valB,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check records only in B\n\tfor (const id of mapB.keys()) {\n\t\tif (!mapA.has(id)) {\n\t\t\tmissingInA.push(id)\n\t\t}\n\t}\n\n\tif (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {\n\t\treturn null\n\t}\n\n\treturn {\n\t\tcollection,\n\t\tdeviceA: nameA,\n\t\tdeviceB: nameB,\n\t\tmissingInB,\n\t\tmissingInA,\n\t\tfieldDifferences,\n\t}\n}\n\n/**\n * Deep equality check for comparing field values.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n\tif (a === b) return true\n\tif (a === null || b === null) return false\n\tif (a === undefined || b === undefined) return false\n\tif (typeof a !== typeof b) return false\n\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((val, i) => deepEqual(val, b[i]))\n\t}\n\n\tif (typeof a === 'object' && typeof b === 'object') {\n\t\tconst keysA = Object.keys(a as Record<string, unknown>)\n\t\tconst keysB = Object.keys(b as Record<string, unknown>)\n\t\tif (keysA.length !== keysB.length) return false\n\t\treturn keysA.every((key) =>\n\t\t\tdeepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n\t\t)\n\t}\n\n\treturn false\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { MessageSerializer, SyncMessage, SyncTransport, TransportOptions } from '@korajs/sync'\nimport { ProtobufMessageSerializer } from '@korajs/sync'\n\n/**\n * A linked client/server transport pair, as consumed by {@link TestDevice}.\n */\nexport interface TransportPair {\n\tclient: SyncTransport\n\tserverTransport: ServerTransport\n}\n\n/**\n * Message types the protobuf serializer models. Ephemeral messages\n * (awareness-update, yjs-doc-update) are JSON-only in the protocol, so they are\n * deep-copied instead of forced through the protobuf envelope.\n */\nconst PROTOBUF_WIRE_TYPES: ReadonlySet<SyncMessage['type']> = new Set([\n\t'handshake',\n\t'handshake-response',\n\t'operation-batch',\n\t'acknowledgment',\n\t'error',\n])\n\n/**\n * Round-trips a message through the real protobuf wire codec: object → protobuf\n * bytes → object. This is what makes a transport pair exercise the true wire\n * format rather than passing live object references between the two ends.\n */\nfunction roundTripThroughProtobuf(\n\tmessage: SyncMessage,\n\tserializer: MessageSerializer,\n): SyncMessage {\n\tif (!PROTOBUF_WIRE_TYPES.has(message.type)) {\n\t\t// Not part of the protobuf schema — still copy so no live reference leaks\n\t\t// across the \"wire\".\n\t\treturn structuredClone(message)\n\t}\n\n\tconst encoded = serializer.encode(message)\n\tif (!(encoded instanceof Uint8Array)) {\n\t\tthrow new Error('Expected the protobuf serializer to encode to bytes')\n\t}\n\treturn serializer.decode(encoded)\n}\n\n/**\n * Wraps a client transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireClientTransport implements SyncTransport {\n\tconstructor(\n\t\tprivate readonly inner: SyncTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tconnect(url: string, options?: TransportOptions): Promise<void> {\n\t\treturn this.inner.connect(url, options)\n\t}\n\n\tdisconnect(): Promise<void> {\n\t\treturn this.inner.disconnect()\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n}\n\n/**\n * Wraps a server transport so every outbound message crosses the protobuf wire.\n */\nclass ProtobufWireServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serializer: MessageSerializer,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tthis.inner.send(roundTripThroughProtobuf(message, this.serializer))\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so that every sync message exchanged between the two\n * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for\n * a real network hop that uses the protobuf wire format.\n *\n * Both directions are wrapped, so operations, handshakes, batches, and acks all\n * travel as bytes rather than shared object references.\n *\n * @param pair - The underlying (in-memory) transport pair to wrap\n * @returns A transport pair that routes messages through the protobuf codec\n */\nexport function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair {\n\tconst serializer = new ProtobufMessageSerializer()\n\treturn {\n\t\tclient: new ProtobufWireClientTransport(pair.client, serializer),\n\t\tserverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer),\n\t}\n}\n","import type { ServerTransport } from '@korajs/server'\nimport type { SyncMessage } from '@korajs/sync'\nimport type { TransportPair } from './protobuf-wire-transport'\n\n/**\n * Wraps a server transport so its handshake responses advertise a caller-chosen\n * wall-clock time, independent of the process `Date.now()`.\n *\n * The sync engine measures clock skew as `serverTime - Date.now()` at handshake.\n * To simulate a device whose clock is fast while the server's clock is correct —\n * both running in one test process — the client mocks `Date.now()` and this\n * wrapper injects the server's true time into the handshake response, so the\n * skew the engine sees is real.\n */\nclass ServerClockServerTransport implements ServerTransport {\n\tconstructor(\n\t\tprivate readonly inner: ServerTransport,\n\t\tprivate readonly serverNow: () => number,\n\t) {}\n\n\tsend(message: SyncMessage): void {\n\t\tif (message.type === 'handshake-response') {\n\t\t\tthis.inner.send({ ...message, serverTime: this.serverNow() })\n\t\t\treturn\n\t\t}\n\t\tthis.inner.send(message)\n\t}\n\n\tonMessage(handler: (message: SyncMessage) => void): void {\n\t\tthis.inner.onMessage(handler)\n\t}\n\n\tonClose(handler: (code: number, reason: string) => void): void {\n\t\tthis.inner.onClose(handler)\n\t}\n\n\tonError(handler: (error: Error) => void): void {\n\t\tthis.inner.onError(handler)\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.inner.isConnected()\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tthis.inner.close(code, reason)\n\t}\n}\n\n/**\n * Wraps a transport pair so the server side reports `serverNow()` as its\n * handshake wall-clock time.\n *\n * @param pair - The underlying transport pair\n * @param serverNow - Returns the server's true current time (ms since epoch)\n * @returns A transport pair whose handshake responses carry the injected time\n */\nexport function wrapTransportPairWithServerClock(\n\tpair: TransportPair,\n\tserverNow: () => number,\n): TransportPair {\n\treturn {\n\t\tclient: pair.client,\n\t\tserverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow),\n\t}\n}\n","// @korajs/test — testing harness for Kora.js\n// Creates virtual device networks for testing sync convergence and conflicts.\n\n// === Factory ===\nexport { createTestNetwork, createMixedTestNetwork } from './test-network'\nexport type {\n\tTestNetwork,\n\tTestNetworkOptions,\n\tMixedTestDeviceConfig,\n} from './test-network'\n\n// === Device ===\nexport { TestDevice } from './test-device'\nexport type { TestDeviceOptions } from './test-device'\n\n// === Server ===\nexport { TestServer } from './test-server'\nexport type { TestServerOptions } from './test-server'\n\n// === Assertions ===\nexport { checkConvergence, expectConverged, expectConvergedEventually } from './assertions'\nexport type {\n\tCollectionDifference,\n\tConvergenceResult,\n\tFieldDifference,\n} from './assertions'\n\n// === Protobuf wire transport (for testing convergence through the real wire format) ===\nexport { wrapTransportPairWithProtobufWire } from './protobuf-wire-transport'\nexport type { TransportPair } from './protobuf-wire-transport'\n\n// === Server-clock transport (for testing clock-skew / rebase integration) ===\nexport { wrapTransportPairWithServerClock } from './server-clock-transport'\n\n// === Re-export ChaosTransport for convenience ===\nexport { ChaosTransport } from '@korajs/sync'\nexport type { ChaosConfig } from '@korajs/sync'\n"],"mappings":"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","names":["ChaosTransport"]}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@korajs/test",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
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",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
|
+
"./package.json": "./package.json",
|
|
10
11
|
".": {
|
|
11
12
|
"import": {
|
|
12
13
|
"types": "./dist/index.d.ts",
|
|
@@ -23,15 +24,15 @@
|
|
|
23
24
|
],
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"better-sqlite3": "^12.8.0",
|
|
26
|
-
"@korajs/core": "1.0.0-beta.
|
|
27
|
-
"@korajs/
|
|
28
|
-
"@korajs/
|
|
29
|
-
"@korajs/sync": "1.0.0-beta.
|
|
30
|
-
"
|
|
31
|
-
"korajs": "1.0.0-beta.
|
|
27
|
+
"@korajs/core": "1.0.0-beta.1",
|
|
28
|
+
"@korajs/merge": "1.0.0-beta.1",
|
|
29
|
+
"@korajs/store": "1.0.0-beta.1",
|
|
30
|
+
"@korajs/sync": "1.0.0-beta.1",
|
|
31
|
+
"korajs": "1.0.0-beta.1",
|
|
32
|
+
"@korajs/server": "1.0.0-beta.1"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"korajs": "1.0.0-beta.
|
|
35
|
+
"korajs": "1.0.0-beta.1"
|
|
35
36
|
},
|
|
36
37
|
"peerDependenciesMeta": {
|
|
37
38
|
"korajs": {
|