@korajs/test 0.6.1 → 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.d.cts CHANGED
@@ -1,9 +1,29 @@
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
- import { MemoryServerStore, ServerTransport } from '@korajs/server';
6
- import { Store, CollectionAccessor } from '@korajs/store';
5
+ import { ServerTransport, MemoryServerStore } from '@korajs/server';
6
+ import { ContentAddressedBlobStore, Store, MemoryBlobStore, CollectionAccessor, BlobManifest, ReceiveBlobResult } from '@korajs/store';
7
+
8
+ /**
9
+ * A linked client/server transport pair, as consumed by {@link TestDevice}.
10
+ */
11
+ interface TransportPair {
12
+ client: SyncTransport;
13
+ serverTransport: ServerTransport;
14
+ }
15
+ /**
16
+ * Wraps a transport pair so that every sync message exchanged between the two
17
+ * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for
18
+ * a real network hop that uses the protobuf wire format.
19
+ *
20
+ * Both directions are wrapped, so operations, handshakes, batches, and acks all
21
+ * travel as bytes rather than shared object references.
22
+ *
23
+ * @param pair - The underlying (in-memory) transport pair to wrap
24
+ * @returns A transport pair that routes messages through the protobuf codec
25
+ */
26
+ declare function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair;
7
27
 
8
28
  /**
9
29
  * In-memory test server wrapping KoraSyncServer with MemoryServerStore.
@@ -17,9 +37,13 @@ interface TestServerOptions {
17
37
  min: number;
18
38
  max: number;
19
39
  };
40
+ /** Enable central blob storage: the server persists and serves uploaded blob bytes. */
41
+ blobStorage?: boolean;
20
42
  }
21
43
  declare class TestServer {
22
44
  readonly store: MemoryServerStore;
45
+ /** The server's central blob store, present when `blobStorage` was enabled. */
46
+ readonly blobStore: ContentAddressedBlobStore | null;
23
47
  private readonly syncServer;
24
48
  constructor(schema: SchemaDefinition, options?: TestServerOptions);
25
49
  /**
@@ -35,6 +59,8 @@ declare class TestServer {
35
59
  * Get the number of connected clients.
36
60
  */
37
61
  getConnectionCount(): number;
62
+ /** Every blob reference still reachable from live records on the server. */
63
+ getLiveBlobRefs(): Promise<BlobRef[]>;
38
64
  /**
39
65
  * Shut down the server and close all sessions.
40
66
  */
@@ -74,6 +100,8 @@ declare class TestDevice {
74
100
  readonly emitter: KoraEventEmitter & {
75
101
  clear(): void;
76
102
  };
103
+ /** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
104
+ readonly blobStore: MemoryBlobStore;
77
105
  private readonly schema;
78
106
  private readonly server;
79
107
  private readonly mergeEngine;
@@ -84,8 +112,10 @@ declare class TestDevice {
84
112
  private readonly operationTransforms;
85
113
  private applyPipeline;
86
114
  private syncEngine;
115
+ private blobChunkProvider;
87
116
  private currentTransport;
88
117
  private unsubscribeSync;
118
+ private unsubscribeAudit;
89
119
  private closing;
90
120
  constructor(options: TestDeviceOptions);
91
121
  /**
@@ -119,6 +149,42 @@ declare class TestDevice {
119
149
  getNodeId(): string;
120
150
  /** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
121
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>;
122
188
  /**
123
189
  * Get the device's version vector.
124
190
  */
@@ -152,6 +218,16 @@ interface TestNetworkOptions {
152
218
  deviceNames?: string[];
153
219
  /** Optional chaos transport settings applied to each device link. */
154
220
  chaos?: ChaosConfig;
221
+ /**
222
+ * Optional wrapper applied to each device's transport pair after the base
223
+ * (in-memory, optionally chaos-wrapped) pair is created. Use it to route
224
+ * messages through the protobuf wire codec
225
+ * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable
226
+ * server clock. Called once per device connection.
227
+ */
228
+ wrapTransport?: (pair: TransportPair) => TransportPair;
229
+ /** Enable central blob storage on the server (persist + serve uploaded blob bytes). */
230
+ blobStorage?: boolean;
155
231
  }
156
232
  /**
157
233
  * A test network with a server and multiple devices.
@@ -273,4 +349,14 @@ declare function expectConvergedEventually(devices: TestDevice[], schema: Schema
273
349
  */
274
350
  declare function checkConvergence(devices: TestDevice[], schema: SchemaDefinition): Promise<ConvergenceResult>;
275
351
 
276
- export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually };
352
+ /**
353
+ * Wraps a transport pair so the server side reports `serverNow()` as its
354
+ * handshake wall-clock time.
355
+ *
356
+ * @param pair - The underlying transport pair
357
+ * @param serverNow - Returns the server's true current time (ms since epoch)
358
+ * @returns A transport pair whose handshake responses carry the injected time
359
+ */
360
+ declare function wrapTransportPairWithServerClock(pair: TransportPair, serverNow: () => number): TransportPair;
361
+
362
+ export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, type TransportPair, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually, wrapTransportPairWithProtobufWire, wrapTransportPairWithServerClock };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,29 @@
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
- import { MemoryServerStore, ServerTransport } from '@korajs/server';
6
- import { Store, CollectionAccessor } from '@korajs/store';
5
+ import { ServerTransport, MemoryServerStore } from '@korajs/server';
6
+ import { ContentAddressedBlobStore, Store, MemoryBlobStore, CollectionAccessor, BlobManifest, ReceiveBlobResult } from '@korajs/store';
7
+
8
+ /**
9
+ * A linked client/server transport pair, as consumed by {@link TestDevice}.
10
+ */
11
+ interface TransportPair {
12
+ client: SyncTransport;
13
+ serverTransport: ServerTransport;
14
+ }
15
+ /**
16
+ * Wraps a transport pair so that every sync message exchanged between the two
17
+ * ends is encoded to protobuf bytes and decoded back — a faithful stand-in for
18
+ * a real network hop that uses the protobuf wire format.
19
+ *
20
+ * Both directions are wrapped, so operations, handshakes, batches, and acks all
21
+ * travel as bytes rather than shared object references.
22
+ *
23
+ * @param pair - The underlying (in-memory) transport pair to wrap
24
+ * @returns A transport pair that routes messages through the protobuf codec
25
+ */
26
+ declare function wrapTransportPairWithProtobufWire(pair: TransportPair): TransportPair;
7
27
 
8
28
  /**
9
29
  * In-memory test server wrapping KoraSyncServer with MemoryServerStore.
@@ -17,9 +37,13 @@ interface TestServerOptions {
17
37
  min: number;
18
38
  max: number;
19
39
  };
40
+ /** Enable central blob storage: the server persists and serves uploaded blob bytes. */
41
+ blobStorage?: boolean;
20
42
  }
21
43
  declare class TestServer {
22
44
  readonly store: MemoryServerStore;
45
+ /** The server's central blob store, present when `blobStorage` was enabled. */
46
+ readonly blobStore: ContentAddressedBlobStore | null;
23
47
  private readonly syncServer;
24
48
  constructor(schema: SchemaDefinition, options?: TestServerOptions);
25
49
  /**
@@ -35,6 +59,8 @@ declare class TestServer {
35
59
  * Get the number of connected clients.
36
60
  */
37
61
  getConnectionCount(): number;
62
+ /** Every blob reference still reachable from live records on the server. */
63
+ getLiveBlobRefs(): Promise<BlobRef[]>;
38
64
  /**
39
65
  * Shut down the server and close all sessions.
40
66
  */
@@ -74,6 +100,8 @@ declare class TestDevice {
74
100
  readonly emitter: KoraEventEmitter & {
75
101
  clear(): void;
76
102
  };
103
+ /** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
104
+ readonly blobStore: MemoryBlobStore;
77
105
  private readonly schema;
78
106
  private readonly server;
79
107
  private readonly mergeEngine;
@@ -84,8 +112,10 @@ declare class TestDevice {
84
112
  private readonly operationTransforms;
85
113
  private applyPipeline;
86
114
  private syncEngine;
115
+ private blobChunkProvider;
87
116
  private currentTransport;
88
117
  private unsubscribeSync;
118
+ private unsubscribeAudit;
89
119
  private closing;
90
120
  constructor(options: TestDeviceOptions);
91
121
  /**
@@ -119,6 +149,42 @@ declare class TestDevice {
119
149
  getNodeId(): string;
120
150
  /** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
121
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>;
122
188
  /**
123
189
  * Get the device's version vector.
124
190
  */
@@ -152,6 +218,16 @@ interface TestNetworkOptions {
152
218
  deviceNames?: string[];
153
219
  /** Optional chaos transport settings applied to each device link. */
154
220
  chaos?: ChaosConfig;
221
+ /**
222
+ * Optional wrapper applied to each device's transport pair after the base
223
+ * (in-memory, optionally chaos-wrapped) pair is created. Use it to route
224
+ * messages through the protobuf wire codec
225
+ * ({@link wrapTransportPairWithProtobufWire}) or to inject a controllable
226
+ * server clock. Called once per device connection.
227
+ */
228
+ wrapTransport?: (pair: TransportPair) => TransportPair;
229
+ /** Enable central blob storage on the server (persist + serve uploaded blob bytes). */
230
+ blobStorage?: boolean;
155
231
  }
156
232
  /**
157
233
  * A test network with a server and multiple devices.
@@ -273,4 +349,14 @@ declare function expectConvergedEventually(devices: TestDevice[], schema: Schema
273
349
  */
274
350
  declare function checkConvergence(devices: TestDevice[], schema: SchemaDefinition): Promise<ConvergenceResult>;
275
351
 
276
- export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually };
352
+ /**
353
+ * Wraps a transport pair so the server side reports `serverNow()` as its
354
+ * handshake wall-clock time.
355
+ *
356
+ * @param pair - The underlying transport pair
357
+ * @param serverNow - Returns the server's true current time (ms since epoch)
358
+ * @returns A transport pair whose handshake responses carry the injected time
359
+ */
360
+ declare function wrapTransportPairWithServerClock(pair: TransportPair, serverNow: () => number): TransportPair;
361
+
362
+ export { type CollectionDifference, type ConvergenceResult, type FieldDifference, type MixedTestDeviceConfig, TestDevice, type TestDeviceOptions, type TestNetwork, type TestNetworkOptions, TestServer, type TestServerOptions, type TransportPair, checkConvergence, createMixedTestNetwork, createTestNetwork, expectConverged, expectConvergedEventually, wrapTransportPairWithProtobufWire, wrapTransportPairWithServerClock };
package/dist/index.js CHANGED
@@ -8,19 +8,33 @@ 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 { Store } from "@korajs/store";
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 {
15
24
  ApplyPipeline,
16
25
  MergeAwareSyncStore,
17
26
  StoreQueueStorage,
18
- StoreSyncStatePersistence
27
+ StoreSyncStatePersistence,
28
+ createSyncEngineChunkPort,
29
+ wireAuditPersistence,
30
+ wireBlobUpload
19
31
  } from "korajs/testing";
20
32
  var TestDevice = class {
21
33
  name;
22
34
  store;
23
35
  emitter;
36
+ /** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
37
+ blobStore = new MemoryBlobStore();
24
38
  schema;
25
39
  server;
26
40
  mergeEngine;
@@ -31,8 +45,10 @@ var TestDevice = class {
31
45
  operationTransforms;
32
46
  applyPipeline = null;
33
47
  syncEngine = null;
48
+ blobChunkProvider = null;
34
49
  currentTransport = null;
35
50
  unsubscribeSync = null;
51
+ unsubscribeAudit = null;
36
52
  closing = false;
37
53
  constructor(options) {
38
54
  this.name = options.name;
@@ -62,6 +78,7 @@ var TestDevice = class {
62
78
  emitter: this.emitter
63
79
  });
64
80
  this.store.setLocalMutationHandler(this.applyPipeline);
81
+ this.unsubscribeAudit = wireAuditPersistence(this.store, this.emitter);
65
82
  }
66
83
  /**
67
84
  * Connect to the test server and perform initial sync.
@@ -92,6 +109,10 @@ var TestDevice = class {
92
109
  emitter: this.emitter
93
110
  });
94
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);
95
116
  const engine = this.syncEngine;
96
117
  this.unsubscribeSync = this.emitter.on("operation:created", (event) => {
97
118
  if (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {
@@ -115,6 +136,7 @@ var TestDevice = class {
115
136
  await this.syncEngine.stop();
116
137
  this.syncEngine = null;
117
138
  }
139
+ this.blobChunkProvider = null;
118
140
  this.currentTransport = null;
119
141
  }
120
142
  /**
@@ -146,6 +168,57 @@ var TestDevice = class {
146
168
  getSyncEngine() {
147
169
  return this.syncEngine;
148
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
+ }
149
222
  /**
150
223
  * Get the device's version vector.
151
224
  */
@@ -163,6 +236,10 @@ var TestDevice = class {
163
236
  */
164
237
  async close() {
165
238
  this.closing = true;
239
+ if (this.unsubscribeAudit) {
240
+ this.unsubscribeAudit();
241
+ this.unsubscribeAudit = null;
242
+ }
166
243
  await this.disconnect();
167
244
  await this.store.close();
168
245
  this.emitter.clear();
@@ -194,19 +271,25 @@ var TestDevice = class {
194
271
  // src/test-server.ts
195
272
  import { MemoryServerStore } from "@korajs/server";
196
273
  import { KoraSyncServer } from "@korajs/server";
274
+ import { createMemoryServerBlobStore } from "@korajs/store";
197
275
  var TestServer = class {
198
276
  store;
277
+ /** The server's central blob store, present when `blobStorage` was enabled. */
278
+ blobStore;
199
279
  syncServer;
200
280
  constructor(schema, options) {
201
281
  this.store = new MemoryServerStore();
202
282
  const schemaVersion = options?.schemaVersion ?? schema.version;
283
+ const blob = options?.blobStorage ? createMemoryServerBlobStore() : null;
284
+ this.blobStore = blob?.store ?? null;
203
285
  this.syncServer = new KoraSyncServer({
204
286
  store: this.store,
205
287
  schemaVersion,
206
288
  supportedSchemaVersions: options?.supportedSchemaVersions ?? {
207
289
  min: schemaVersion,
208
290
  max: schemaVersion
209
- }
291
+ },
292
+ ...blob ? blob.callbacks : {}
210
293
  });
211
294
  void this.store.setSchema(schema);
212
295
  }
@@ -229,6 +312,10 @@ var TestServer = class {
229
312
  getConnectionCount() {
230
313
  return this.syncServer.getConnectionCount();
231
314
  }
315
+ /** Every blob reference still reachable from live records on the server. */
316
+ getLiveBlobRefs() {
317
+ return this.syncServer.getLiveBlobRefs();
318
+ }
232
319
  /**
233
320
  * Shut down the server and close all sessions.
234
321
  */
@@ -243,7 +330,7 @@ async function createTestNetwork(schema, options) {
243
330
  const deviceCount = options?.devices ?? 2;
244
331
  const deviceNames = options?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`);
245
332
  const tmpDir = mkdtempSync(join(tmpdir(), "kora-test-"));
246
- const server = new TestServer(schema);
333
+ const server = new TestServer(schema, { blobStorage: options?.blobStorage });
247
334
  const devices = [];
248
335
  for (const name of deviceNames) {
249
336
  const device = new TestDevice({
@@ -254,10 +341,11 @@ async function createTestNetwork(schema, options) {
254
341
  const pair = createServerTransportPair();
255
342
  const client = pair.client;
256
343
  const chaos = options?.chaos;
257
- return {
344
+ const base = {
258
345
  client: chaos ? new ChaosTransport(client, chaos) : client,
259
346
  serverTransport: pair.server
260
347
  };
348
+ return options?.wrapTransport ? options.wrapTransport(base) : base;
261
349
  },
262
350
  tmpDir
263
351
  });
@@ -451,6 +539,126 @@ function deepEqual(a, b) {
451
539
  return false;
452
540
  }
453
541
 
542
+ // src/protobuf-wire-transport.ts
543
+ import { ProtobufMessageSerializer } from "@korajs/sync";
544
+ var PROTOBUF_WIRE_TYPES = /* @__PURE__ */ new Set([
545
+ "handshake",
546
+ "handshake-response",
547
+ "operation-batch",
548
+ "acknowledgment",
549
+ "error"
550
+ ]);
551
+ function roundTripThroughProtobuf(message, serializer) {
552
+ if (!PROTOBUF_WIRE_TYPES.has(message.type)) {
553
+ return structuredClone(message);
554
+ }
555
+ const encoded = serializer.encode(message);
556
+ if (!(encoded instanceof Uint8Array)) {
557
+ throw new Error("Expected the protobuf serializer to encode to bytes");
558
+ }
559
+ return serializer.decode(encoded);
560
+ }
561
+ var ProtobufWireClientTransport = class {
562
+ constructor(inner, serializer) {
563
+ this.inner = inner;
564
+ this.serializer = serializer;
565
+ }
566
+ inner;
567
+ serializer;
568
+ connect(url, options) {
569
+ return this.inner.connect(url, options);
570
+ }
571
+ disconnect() {
572
+ return this.inner.disconnect();
573
+ }
574
+ send(message) {
575
+ this.inner.send(roundTripThroughProtobuf(message, this.serializer));
576
+ }
577
+ onMessage(handler) {
578
+ this.inner.onMessage(handler);
579
+ }
580
+ onClose(handler) {
581
+ this.inner.onClose(handler);
582
+ }
583
+ onError(handler) {
584
+ this.inner.onError(handler);
585
+ }
586
+ isConnected() {
587
+ return this.inner.isConnected();
588
+ }
589
+ };
590
+ var ProtobufWireServerTransport = class {
591
+ constructor(inner, serializer) {
592
+ this.inner = inner;
593
+ this.serializer = serializer;
594
+ }
595
+ inner;
596
+ serializer;
597
+ send(message) {
598
+ this.inner.send(roundTripThroughProtobuf(message, this.serializer));
599
+ }
600
+ onMessage(handler) {
601
+ this.inner.onMessage(handler);
602
+ }
603
+ onClose(handler) {
604
+ this.inner.onClose(handler);
605
+ }
606
+ onError(handler) {
607
+ this.inner.onError(handler);
608
+ }
609
+ isConnected() {
610
+ return this.inner.isConnected();
611
+ }
612
+ close(code, reason) {
613
+ this.inner.close(code, reason);
614
+ }
615
+ };
616
+ function wrapTransportPairWithProtobufWire(pair) {
617
+ const serializer = new ProtobufMessageSerializer();
618
+ return {
619
+ client: new ProtobufWireClientTransport(pair.client, serializer),
620
+ serverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer)
621
+ };
622
+ }
623
+
624
+ // src/server-clock-transport.ts
625
+ var ServerClockServerTransport = class {
626
+ constructor(inner, serverNow) {
627
+ this.inner = inner;
628
+ this.serverNow = serverNow;
629
+ }
630
+ inner;
631
+ serverNow;
632
+ send(message) {
633
+ if (message.type === "handshake-response") {
634
+ this.inner.send({ ...message, serverTime: this.serverNow() });
635
+ return;
636
+ }
637
+ this.inner.send(message);
638
+ }
639
+ onMessage(handler) {
640
+ this.inner.onMessage(handler);
641
+ }
642
+ onClose(handler) {
643
+ this.inner.onClose(handler);
644
+ }
645
+ onError(handler) {
646
+ this.inner.onError(handler);
647
+ }
648
+ isConnected() {
649
+ return this.inner.isConnected();
650
+ }
651
+ close(code, reason) {
652
+ this.inner.close(code, reason);
653
+ }
654
+ };
655
+ function wrapTransportPairWithServerClock(pair, serverNow) {
656
+ return {
657
+ client: pair.client,
658
+ serverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow)
659
+ };
660
+ }
661
+
454
662
  // src/index.ts
455
663
  import { ChaosTransport as ChaosTransport2 } from "@korajs/sync";
456
664
  export {
@@ -461,6 +669,8 @@ export {
461
669
  createMixedTestNetwork,
462
670
  createTestNetwork,
463
671
  expectConverged,
464
- expectConvergedEventually
672
+ expectConvergedEventually,
673
+ wrapTransportPairWithProtobufWire,
674
+ wrapTransportPairWithServerClock
465
675
  };
466
676
  //# sourceMappingURL=index.js.map