@korajs/test 0.0.0-canary-20261008174129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +973 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +493 -0
- package/dist/index.d.ts +493 -0
- package/dist/index.js +942 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ChaosTransport: () => import_sync4.ChaosTransport,
|
|
34
|
+
TestDevice: () => TestDevice,
|
|
35
|
+
TestServer: () => TestServer,
|
|
36
|
+
checkConvergence: () => checkConvergence,
|
|
37
|
+
createMixedTestNetwork: () => createMixedTestNetwork,
|
|
38
|
+
createTestAuthBinding: () => createTestAuthBinding,
|
|
39
|
+
createTestNetwork: () => createTestNetwork,
|
|
40
|
+
createTransportMetrics: () => createTransportMetrics,
|
|
41
|
+
expectConverged: () => expectConverged,
|
|
42
|
+
expectConvergedEventually: () => expectConvergedEventually,
|
|
43
|
+
wrapTransportPairWithProtobufWire: () => wrapTransportPairWithProtobufWire,
|
|
44
|
+
wrapTransportPairWithServerClock: () => wrapTransportPairWithServerClock
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(index_exports);
|
|
47
|
+
|
|
48
|
+
// src/test-network.ts
|
|
49
|
+
var import_node_fs = require("fs");
|
|
50
|
+
var import_node_os = require("os");
|
|
51
|
+
var import_node_path = require("path");
|
|
52
|
+
var import_internal2 = require("@korajs/server/internal");
|
|
53
|
+
var import_sync2 = require("@korajs/sync");
|
|
54
|
+
|
|
55
|
+
// src/test-device.ts
|
|
56
|
+
var import_internal = require("@korajs/core/internal");
|
|
57
|
+
var import_merge = require("@korajs/merge");
|
|
58
|
+
var import_store = require("@korajs/store");
|
|
59
|
+
var import_better_sqlite3 = require("@korajs/store/better-sqlite3");
|
|
60
|
+
var import_sync = require("@korajs/sync");
|
|
61
|
+
var import_testing = require("korajs/testing");
|
|
62
|
+
var ReconnectingClientTransport = class {
|
|
63
|
+
constructor(inner, createPair, server) {
|
|
64
|
+
this.inner = inner;
|
|
65
|
+
this.createPair = createPair;
|
|
66
|
+
this.server = server;
|
|
67
|
+
}
|
|
68
|
+
inner;
|
|
69
|
+
createPair;
|
|
70
|
+
server;
|
|
71
|
+
messageHandler = null;
|
|
72
|
+
closeHandler = null;
|
|
73
|
+
errorHandler = null;
|
|
74
|
+
connectedOnce = false;
|
|
75
|
+
async connect(url, options) {
|
|
76
|
+
if (this.connectedOnce && !this.inner.isConnected()) {
|
|
77
|
+
const previous = this.inner;
|
|
78
|
+
previous.onMessage(() => {
|
|
79
|
+
});
|
|
80
|
+
previous.onClose(() => {
|
|
81
|
+
});
|
|
82
|
+
previous.onError(() => {
|
|
83
|
+
});
|
|
84
|
+
const { client, serverTransport } = this.createPair();
|
|
85
|
+
this.inner = client;
|
|
86
|
+
this.wire();
|
|
87
|
+
this.server.handleConnection(serverTransport);
|
|
88
|
+
}
|
|
89
|
+
this.connectedOnce = true;
|
|
90
|
+
await this.inner.connect(url, options);
|
|
91
|
+
}
|
|
92
|
+
disconnect() {
|
|
93
|
+
return this.inner.disconnect();
|
|
94
|
+
}
|
|
95
|
+
send(message) {
|
|
96
|
+
this.inner.send(message);
|
|
97
|
+
}
|
|
98
|
+
onMessage(handler) {
|
|
99
|
+
this.messageHandler = handler;
|
|
100
|
+
this.wire();
|
|
101
|
+
}
|
|
102
|
+
onClose(handler) {
|
|
103
|
+
this.closeHandler = handler;
|
|
104
|
+
this.wire();
|
|
105
|
+
}
|
|
106
|
+
onError(handler) {
|
|
107
|
+
this.errorHandler = handler;
|
|
108
|
+
this.wire();
|
|
109
|
+
}
|
|
110
|
+
isConnected() {
|
|
111
|
+
return this.inner.isConnected();
|
|
112
|
+
}
|
|
113
|
+
wire() {
|
|
114
|
+
this.inner.onMessage((message) => this.messageHandler?.(message));
|
|
115
|
+
this.inner.onClose((reason) => this.closeHandler?.(reason));
|
|
116
|
+
this.inner.onError((error) => this.errorHandler?.(error));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var TestDevice = class {
|
|
120
|
+
name;
|
|
121
|
+
store;
|
|
122
|
+
emitter;
|
|
123
|
+
/** Content-addressed blob store for out-of-band blob bytes (chunks + full blobs). */
|
|
124
|
+
blobStore = new import_store.MemoryBlobStore();
|
|
125
|
+
schema;
|
|
126
|
+
server;
|
|
127
|
+
encryption;
|
|
128
|
+
batchSize;
|
|
129
|
+
encryptor = null;
|
|
130
|
+
mergeEngine;
|
|
131
|
+
createTransportPair;
|
|
132
|
+
adapter;
|
|
133
|
+
dbPath;
|
|
134
|
+
syncSchemaVersion;
|
|
135
|
+
operationTransforms;
|
|
136
|
+
scopeExit;
|
|
137
|
+
principal;
|
|
138
|
+
reconnectable;
|
|
139
|
+
applyPipeline = null;
|
|
140
|
+
syncEngine = null;
|
|
141
|
+
blobChunkProvider = null;
|
|
142
|
+
currentTransport = null;
|
|
143
|
+
unsubscribeSync = null;
|
|
144
|
+
unsubscribeAudit = null;
|
|
145
|
+
closing = false;
|
|
146
|
+
constructor(options) {
|
|
147
|
+
this.name = options.name;
|
|
148
|
+
this.schema = options.schema;
|
|
149
|
+
this.server = options.server;
|
|
150
|
+
this.createTransportPair = options.createTransportPair;
|
|
151
|
+
this.dbPath = `${options.tmpDir}/test-device-${options.name}.db`;
|
|
152
|
+
this.syncSchemaVersion = options.syncSchemaVersion ?? options.schema.version;
|
|
153
|
+
this.operationTransforms = options.operationTransforms ?? [];
|
|
154
|
+
this.scopeExit = options.scopeExit;
|
|
155
|
+
this.principal = options.principal;
|
|
156
|
+
this.reconnectable = options.reconnectable === true;
|
|
157
|
+
this.encryption = options.encryption;
|
|
158
|
+
this.batchSize = options.batchSize;
|
|
159
|
+
(0, import_sync.validateEncryptedRelations)(options.schema, options.encryption?.config);
|
|
160
|
+
this.emitter = new import_internal.SimpleEventEmitter();
|
|
161
|
+
this.mergeEngine = new import_merge.MergeEngine();
|
|
162
|
+
this.adapter = new import_better_sqlite3.BetterSqlite3Adapter(this.dbPath);
|
|
163
|
+
this.store = new import_store.Store({
|
|
164
|
+
schema: options.schema,
|
|
165
|
+
adapter: this.adapter,
|
|
166
|
+
emitter: this.emitter,
|
|
167
|
+
materialization: options.legacyMerge === true ? "legacy" : "fold",
|
|
168
|
+
...options.operationTransforms && options.operationTransforms.length > 0 ? { operationTransforms: options.operationTransforms } : {},
|
|
169
|
+
...options.maxOperationBytes !== void 0 ? { maxOperationBytes: options.maxOperationBytes } : {},
|
|
170
|
+
...options.nodeId !== void 0 ? { nodeId: options.nodeId } : {}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Open the store (must be called before sync or collection operations).
|
|
175
|
+
*/
|
|
176
|
+
async open() {
|
|
177
|
+
await this.store.open();
|
|
178
|
+
this.applyPipeline = new import_testing.ApplyPipeline({
|
|
179
|
+
store: this.store,
|
|
180
|
+
mergeEngine: this.mergeEngine,
|
|
181
|
+
emitter: this.emitter
|
|
182
|
+
});
|
|
183
|
+
this.store.setLocalMutationHandler(this.applyPipeline);
|
|
184
|
+
this.unsubscribeAudit = (0, import_testing.wireAuditPersistence)(this.store, this.emitter);
|
|
185
|
+
const principal = this.principal?.();
|
|
186
|
+
if (principal) await this.store.bindPrincipal(principal);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The signed-in user changed (RT-42), as an auth binding reports it: like createApp,
|
|
190
|
+
* end the live session and bind the store to the new user before the next write.
|
|
191
|
+
*/
|
|
192
|
+
async authChanged() {
|
|
193
|
+
if (this.syncEngine) {
|
|
194
|
+
await this.syncEngine.refreshPrincipal();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const principal = this.principal?.();
|
|
198
|
+
if (principal) await this.store.bindPrincipal(principal);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Connect to the test server and perform initial sync.
|
|
202
|
+
* If already connected, flushes any pending operations.
|
|
203
|
+
*/
|
|
204
|
+
async sync() {
|
|
205
|
+
if (this.syncEngine && this.currentTransport?.isConnected()) {
|
|
206
|
+
await this.waitForPendingOps();
|
|
207
|
+
await this.waitForSettled();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const created = this.createTransportPair();
|
|
211
|
+
const { serverTransport } = created;
|
|
212
|
+
const client = this.reconnectable ? new ReconnectingClientTransport(created.client, this.createTransportPair, this.server) : created.client;
|
|
213
|
+
this.currentTransport = client;
|
|
214
|
+
if (this.encryption && !this.encryptor) {
|
|
215
|
+
this.encryptor = await import_sync.SyncEncryptor.create(
|
|
216
|
+
this.encryption.config,
|
|
217
|
+
this.encryption.salt,
|
|
218
|
+
this.encryption.iterations
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const conflictHandler = {};
|
|
222
|
+
const syncStore = new import_testing.MergeAwareSyncStore(this.store, this.mergeEngine, this.emitter, {
|
|
223
|
+
onMergeConflict: () => conflictHandler.fn?.()
|
|
224
|
+
});
|
|
225
|
+
this.syncEngine = new import_sync.SyncEngine({
|
|
226
|
+
transport: client,
|
|
227
|
+
store: syncStore,
|
|
228
|
+
queueStorage: new import_testing.StoreQueueStorage(this.adapter),
|
|
229
|
+
syncState: new import_testing.StoreSyncStatePersistence(this.store),
|
|
230
|
+
config: {
|
|
231
|
+
url: "ws://test-network",
|
|
232
|
+
schemaVersion: this.syncSchemaVersion,
|
|
233
|
+
operationTransforms: this.operationTransforms.length > 0 ? this.operationTransforms : void 0,
|
|
234
|
+
...this.scopeExit ? { scopeExit: this.scopeExit } : {},
|
|
235
|
+
...this.principal ? { principal: async () => this.principal ? this.principal() : null } : {},
|
|
236
|
+
...this.encryption ? { encryption: this.encryption.config } : {},
|
|
237
|
+
...this.batchSize !== void 0 ? { batchSize: this.batchSize } : {}
|
|
238
|
+
},
|
|
239
|
+
emitter: this.emitter,
|
|
240
|
+
...this.encryptor ? { encryptor: this.encryptor } : {}
|
|
241
|
+
});
|
|
242
|
+
conflictHandler.fn = () => this.syncEngine?.recordConflict();
|
|
243
|
+
const chunkPort = (0, import_testing.createSyncEngineChunkPort)(this.syncEngine);
|
|
244
|
+
(0, import_store.serveBlobChunks)(chunkPort, this.blobStore);
|
|
245
|
+
this.blobChunkProvider = (0, import_store.createRemoteChunkProvider)(chunkPort);
|
|
246
|
+
(0, import_testing.wireBlobUpload)(this.emitter, this.syncEngine, this.blobStore);
|
|
247
|
+
const engine = this.syncEngine;
|
|
248
|
+
this.unsubscribeSync = this.emitter.on("operation:created", (event) => {
|
|
249
|
+
if (!this.closing && this.syncEngine === engine && this.currentTransport?.isConnected()) {
|
|
250
|
+
this.syncEngine.pushOperation(event.operation).catch(() => {
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
this.server.handleConnection(serverTransport);
|
|
255
|
+
await this.syncEngine.start();
|
|
256
|
+
await this.waitForSettled();
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Disconnect from the test server.
|
|
260
|
+
*/
|
|
261
|
+
async disconnect() {
|
|
262
|
+
if (this.unsubscribeSync) {
|
|
263
|
+
this.unsubscribeSync();
|
|
264
|
+
this.unsubscribeSync = null;
|
|
265
|
+
}
|
|
266
|
+
if (this.syncEngine) {
|
|
267
|
+
await this.syncEngine.stop();
|
|
268
|
+
this.syncEngine = null;
|
|
269
|
+
}
|
|
270
|
+
this.blobChunkProvider = null;
|
|
271
|
+
this.currentTransport = null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Reconnect to the test server after a disconnect.
|
|
275
|
+
*/
|
|
276
|
+
async reconnect() {
|
|
277
|
+
await this.sync();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get a collection accessor for performing CRUD operations.
|
|
281
|
+
*/
|
|
282
|
+
collection(name) {
|
|
283
|
+
return this.store.collection(name);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get all records from a collection (convenience method).
|
|
287
|
+
*/
|
|
288
|
+
async getState(collectionName) {
|
|
289
|
+
const accessor = this.store.collection(collectionName);
|
|
290
|
+
return accessor.where({}).exec();
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get the device's node ID.
|
|
294
|
+
*/
|
|
295
|
+
getNodeId() {
|
|
296
|
+
return this.store.getNodeId();
|
|
297
|
+
}
|
|
298
|
+
/** Exposes the sync engine for integration tests (e.g. doc channel, chaos). */
|
|
299
|
+
getSyncEngine() {
|
|
300
|
+
return this.syncEngine;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Operations the server rejected for this device that have not been reconciled.
|
|
304
|
+
* Empty when the device has never connected.
|
|
305
|
+
*/
|
|
306
|
+
async getRejectedOperations() {
|
|
307
|
+
if (!this.syncEngine) return [];
|
|
308
|
+
return this.syncEngine.getRejectedOperations();
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Stage a blob's bytes into this device's blob store, splitting them into
|
|
312
|
+
* content-addressed chunks this device can then serve to peers over the sync
|
|
313
|
+
* connection. Returns the manifest (blob hash + ordered chunk hashes) needed
|
|
314
|
+
* to pull the blob elsewhere.
|
|
315
|
+
*/
|
|
316
|
+
async stageBlob(bytes, options) {
|
|
317
|
+
const { manifest } = await (0, import_store.prepareBlobForSend)(bytes, this.blobStore, options);
|
|
318
|
+
return manifest;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Store a blob for transfer the way `app.blobs.put` does: stage chunks, store
|
|
322
|
+
* the full blob, and store the manifest as its own content-addressed object.
|
|
323
|
+
* The returned reference carries a `manifestHash`, so a peer can pull the bytes
|
|
324
|
+
* knowing only the reference.
|
|
325
|
+
*/
|
|
326
|
+
async putBlob(bytes, options) {
|
|
327
|
+
return (0, import_store.putBlobForTransfer)(this.blobStore, bytes, options);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Pull a blob's bytes over the live connection knowing only its reference: the
|
|
331
|
+
* manifest is resolved by `ref.manifestHash` first, then the chunks are fetched.
|
|
332
|
+
*/
|
|
333
|
+
async pullBlobByRef(ref) {
|
|
334
|
+
if (!this.blobChunkProvider) {
|
|
335
|
+
throw new Error("Cannot pull a blob before the device has connected (call sync() first)");
|
|
336
|
+
}
|
|
337
|
+
const manifest = await (0, import_store.resolveBlobManifest)(this.blobChunkProvider, ref);
|
|
338
|
+
return (0, import_store.receiveBlob)(manifest, this.blobChunkProvider, {
|
|
339
|
+
chunkStore: this.blobStore,
|
|
340
|
+
blobStore: this.blobStore
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Pull a blob's bytes from peers over the live sync connection. Requests only
|
|
345
|
+
* the chunks this device is missing, reassembles them, and verifies integrity
|
|
346
|
+
* against the manifest's blob hash before storing the full blob locally.
|
|
347
|
+
*/
|
|
348
|
+
async pullBlob(manifest) {
|
|
349
|
+
if (!this.blobChunkProvider) {
|
|
350
|
+
throw new Error("Cannot pull a blob before the device has connected (call sync() first)");
|
|
351
|
+
}
|
|
352
|
+
return (0, import_store.receiveBlob)(manifest, this.blobChunkProvider, {
|
|
353
|
+
chunkStore: this.blobStore,
|
|
354
|
+
blobStore: this.blobStore
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/** Read fully-assembled blob bytes from this device's store, or null if absent. */
|
|
358
|
+
async getBlobBytes(hash) {
|
|
359
|
+
return this.blobStore.get(hash);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Get the device's version vector.
|
|
363
|
+
*/
|
|
364
|
+
getVersionVector() {
|
|
365
|
+
return this.store.getVersionVector();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Check if the device is currently connected to the server.
|
|
369
|
+
*/
|
|
370
|
+
isConnected() {
|
|
371
|
+
return this.currentTransport?.isConnected() ?? false;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Close the device, releasing all resources.
|
|
375
|
+
*/
|
|
376
|
+
async close() {
|
|
377
|
+
this.closing = true;
|
|
378
|
+
if (this.unsubscribeAudit) {
|
|
379
|
+
this.unsubscribeAudit();
|
|
380
|
+
this.unsubscribeAudit = null;
|
|
381
|
+
}
|
|
382
|
+
await this.disconnect();
|
|
383
|
+
await this.store.close();
|
|
384
|
+
this.emitter.clear();
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Wait for in-flight sync operations to settle.
|
|
388
|
+
* In-memory transports are near-synchronous, but apply/relay work is async.
|
|
389
|
+
*/
|
|
390
|
+
async waitForSettled() {
|
|
391
|
+
for (let i = 0; i < 15; i++) {
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Wait for all pending outbound operations to be acknowledged.
|
|
397
|
+
*/
|
|
398
|
+
async waitForPendingOps() {
|
|
399
|
+
if (!this.syncEngine) return;
|
|
400
|
+
const maxWait = 2e3;
|
|
401
|
+
const start = Date.now();
|
|
402
|
+
while (Date.now() - start < maxWait) {
|
|
403
|
+
const status = this.syncEngine.getStatus();
|
|
404
|
+
if (status.pendingOperations === 0) return;
|
|
405
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// src/test-server.ts
|
|
411
|
+
var import_server = require("@korajs/server");
|
|
412
|
+
var import_server2 = require("@korajs/server");
|
|
413
|
+
var import_store2 = require("@korajs/store");
|
|
414
|
+
var TestServer = class {
|
|
415
|
+
store;
|
|
416
|
+
/** Resolves once the store has the schema (needed before a SQL store is used). */
|
|
417
|
+
ready;
|
|
418
|
+
/** The server's central blob store, present when `blobStorage` was enabled. */
|
|
419
|
+
blobStore;
|
|
420
|
+
syncServer;
|
|
421
|
+
constructor(schema, options) {
|
|
422
|
+
this.store = options?.store ?? new import_server.MemoryServerStore();
|
|
423
|
+
const schemaVersion = options?.schemaVersion ?? schema.version;
|
|
424
|
+
const blob = options?.blobStorage ? (0, import_store2.createMemoryServerBlobStore)() : null;
|
|
425
|
+
this.blobStore = blob?.store ?? null;
|
|
426
|
+
this.syncServer = new import_server2.KoraSyncServer({
|
|
427
|
+
store: this.store,
|
|
428
|
+
schemaVersion,
|
|
429
|
+
supportedSchemaVersions: options?.supportedSchemaVersions ?? {
|
|
430
|
+
min: schemaVersion,
|
|
431
|
+
max: schemaVersion
|
|
432
|
+
},
|
|
433
|
+
...options?.operationTransforms ? { operationTransforms: options.operationTransforms } : {},
|
|
434
|
+
...blob ? blob.callbacks : {},
|
|
435
|
+
...options?.validateOperation ? { validateOperation: options.validateOperation } : {},
|
|
436
|
+
...options?.encryption ? { encryption: options.encryption } : {}
|
|
437
|
+
});
|
|
438
|
+
this.ready = this.store.setSchema(
|
|
439
|
+
schema,
|
|
440
|
+
options?.operationTransforms ? { operationTransforms: options.operationTransforms } : {}
|
|
441
|
+
);
|
|
442
|
+
this.ready.catch(() => {
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Register a client connection transport with the server.
|
|
447
|
+
* Returns the session ID assigned by the server.
|
|
448
|
+
*/
|
|
449
|
+
handleConnection(transport) {
|
|
450
|
+
return this.syncServer.handleConnection(transport);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get all operations stored on the server.
|
|
454
|
+
*/
|
|
455
|
+
getAllOperations() {
|
|
456
|
+
if (!(this.store instanceof import_server.MemoryServerStore)) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
"TestServer.getAllOperations() reads the memory store synchronously; use store.getOperationsAfterDelivery() with a SQL store"
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return this.store.getAllOperations();
|
|
462
|
+
}
|
|
463
|
+
/** The trusted server-side write API (route context): writes authored by the server node. */
|
|
464
|
+
getKoraContext() {
|
|
465
|
+
return this.syncServer.getKoraContext();
|
|
466
|
+
}
|
|
467
|
+
/** The node ids the server's stores fold with and advertise in the handshake. */
|
|
468
|
+
get authoritativeNodeIds() {
|
|
469
|
+
return this.syncServer.authoritativeNodeIds;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Get the number of connected clients.
|
|
473
|
+
*/
|
|
474
|
+
getConnectionCount() {
|
|
475
|
+
return this.syncServer.getConnectionCount();
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Retransmit relay batches connected clients have not acknowledged. Lets a test
|
|
479
|
+
* deterministically trigger the redelivery that the server also runs on a timer,
|
|
480
|
+
* without depending on wall-clock time. `staleMs` of 0 resends every pending relay.
|
|
481
|
+
*/
|
|
482
|
+
retransmitPendingRelays(staleMs = 0) {
|
|
483
|
+
this.syncServer.retransmitPendingRelays(staleMs);
|
|
484
|
+
}
|
|
485
|
+
/** Every blob reference still reachable from live records on the server. */
|
|
486
|
+
getLiveBlobRefs() {
|
|
487
|
+
return this.syncServer.getLiveBlobRefs();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Shut down the server and close all sessions.
|
|
491
|
+
*/
|
|
492
|
+
async close() {
|
|
493
|
+
await this.syncServer.stop();
|
|
494
|
+
await this.store.close();
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/test-network.ts
|
|
499
|
+
async function createTestNetwork(schema, options) {
|
|
500
|
+
const deviceCount = options?.devices ?? 2;
|
|
501
|
+
const deviceNames = options?.deviceNames ?? Array.from({ length: deviceCount }, (_, i) => `device-${i}`);
|
|
502
|
+
const tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "kora-test-"));
|
|
503
|
+
const server = new TestServer(schema, {
|
|
504
|
+
blobStorage: options?.blobStorage,
|
|
505
|
+
validateOperation: options?.validateOperation,
|
|
506
|
+
...options?.serverStore ? { store: options.serverStore } : {},
|
|
507
|
+
...options?.serverEncryption ? { encryption: options.serverEncryption } : {}
|
|
508
|
+
});
|
|
509
|
+
await server.ready;
|
|
510
|
+
const devices = [];
|
|
511
|
+
for (const name of deviceNames) {
|
|
512
|
+
const device = new TestDevice({
|
|
513
|
+
name,
|
|
514
|
+
schema,
|
|
515
|
+
server,
|
|
516
|
+
createTransportPair: () => {
|
|
517
|
+
const pair = (0, import_internal2.createServerTransportPair)();
|
|
518
|
+
const client = pair.client;
|
|
519
|
+
const chaos = options?.chaos;
|
|
520
|
+
const base = {
|
|
521
|
+
client: chaos ? new import_sync2.ChaosTransport(client, chaos) : client,
|
|
522
|
+
serverTransport: pair.server
|
|
523
|
+
};
|
|
524
|
+
return options?.wrapTransport ? options.wrapTransport(base) : base;
|
|
525
|
+
},
|
|
526
|
+
tmpDir,
|
|
527
|
+
...options?.legacyMerge ? { legacyMerge: true } : {},
|
|
528
|
+
...options?.encryption ? { encryption: options.encryption } : {},
|
|
529
|
+
...options?.deviceMaxOperationBytes !== void 0 ? { maxOperationBytes: options.deviceMaxOperationBytes } : {}
|
|
530
|
+
});
|
|
531
|
+
await device.open();
|
|
532
|
+
devices.push(device);
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
server,
|
|
536
|
+
devices,
|
|
537
|
+
tmpDir,
|
|
538
|
+
async close() {
|
|
539
|
+
for (const device of devices) {
|
|
540
|
+
await device.close();
|
|
541
|
+
}
|
|
542
|
+
await server.close();
|
|
543
|
+
try {
|
|
544
|
+
const { rmSync } = await import("fs");
|
|
545
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async function createMixedTestNetwork(serverSchema, serverOptions, deviceConfigs) {
|
|
552
|
+
const tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "kora-test-"));
|
|
553
|
+
const server = new TestServer(serverSchema, serverOptions);
|
|
554
|
+
const devices = [];
|
|
555
|
+
for (const config of deviceConfigs) {
|
|
556
|
+
const device = new TestDevice({
|
|
557
|
+
name: config.name,
|
|
558
|
+
schema: config.schema,
|
|
559
|
+
server,
|
|
560
|
+
syncSchemaVersion: config.syncSchemaVersion,
|
|
561
|
+
operationTransforms: config.operationTransforms,
|
|
562
|
+
createTransportPair: () => {
|
|
563
|
+
const pair = (0, import_internal2.createServerTransportPair)();
|
|
564
|
+
return {
|
|
565
|
+
client: pair.client,
|
|
566
|
+
serverTransport: pair.server
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
tmpDir
|
|
570
|
+
});
|
|
571
|
+
await device.open();
|
|
572
|
+
devices.push(device);
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
server,
|
|
576
|
+
devices,
|
|
577
|
+
tmpDir,
|
|
578
|
+
async close() {
|
|
579
|
+
for (const device of devices) {
|
|
580
|
+
await device.close();
|
|
581
|
+
}
|
|
582
|
+
await server.close();
|
|
583
|
+
try {
|
|
584
|
+
const { rmSync } = await import("fs");
|
|
585
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/assertions.ts
|
|
593
|
+
async function expectConverged(devices, schema) {
|
|
594
|
+
if (devices.length < 2) return;
|
|
595
|
+
const result = await checkConvergence(devices, schema);
|
|
596
|
+
if (!result.converged) {
|
|
597
|
+
const details = result.differences.map((d) => {
|
|
598
|
+
const parts = [
|
|
599
|
+
` Collection "${d.collection}" differs between ${d.deviceA} and ${d.deviceB}:`
|
|
600
|
+
];
|
|
601
|
+
if (d.missingInB.length > 0) {
|
|
602
|
+
parts.push(` Missing in ${d.deviceB}: ${d.missingInB.join(", ")}`);
|
|
603
|
+
}
|
|
604
|
+
if (d.missingInA.length > 0) {
|
|
605
|
+
parts.push(` Missing in ${d.deviceA}: ${d.missingInA.join(", ")}`);
|
|
606
|
+
}
|
|
607
|
+
for (const fd of d.fieldDifferences) {
|
|
608
|
+
parts.push(
|
|
609
|
+
` Record "${fd.recordId}" field "${fd.field}": ${JSON.stringify(fd.valueInA)} vs ${JSON.stringify(fd.valueInB)}`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return parts.join("\n");
|
|
613
|
+
}).join("\n");
|
|
614
|
+
throw new Error(`Devices have not converged:
|
|
615
|
+
${details}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function expectConvergedEventually(devices, schema, options) {
|
|
619
|
+
if (devices.length < 2) return;
|
|
620
|
+
const timeoutMs = options?.timeoutMs ?? 5e3;
|
|
621
|
+
const intervalMs = options?.intervalMs ?? 25;
|
|
622
|
+
const deadline = Date.now() + timeoutMs;
|
|
623
|
+
while (Date.now() < deadline) {
|
|
624
|
+
const result = await checkConvergence(devices, schema);
|
|
625
|
+
if (result.converged) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
629
|
+
}
|
|
630
|
+
await expectConverged(devices, schema);
|
|
631
|
+
}
|
|
632
|
+
async function checkConvergence(devices, schema) {
|
|
633
|
+
const differences = [];
|
|
634
|
+
const collectionNames = Object.keys(schema.collections);
|
|
635
|
+
for (let i = 0; i < devices.length - 1; i++) {
|
|
636
|
+
for (let j = i + 1; j < devices.length; j++) {
|
|
637
|
+
const deviceA = devices[i];
|
|
638
|
+
const deviceB = devices[j];
|
|
639
|
+
for (const collection of collectionNames) {
|
|
640
|
+
const stateA = await deviceA.getState(collection);
|
|
641
|
+
const stateB = await deviceB.getState(collection);
|
|
642
|
+
const diff = compareCollectionStates(collection, deviceA.name, deviceB.name, stateA, stateB);
|
|
643
|
+
if (diff) {
|
|
644
|
+
differences.push(diff);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
converged: differences.length === 0,
|
|
651
|
+
differences
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function compareCollectionStates(collection, nameA, nameB, stateA, stateB) {
|
|
655
|
+
const mapA = new Map(stateA.map((r) => [r.id, r]));
|
|
656
|
+
const mapB = new Map(stateB.map((r) => [r.id, r]));
|
|
657
|
+
const missingInB = [];
|
|
658
|
+
const missingInA = [];
|
|
659
|
+
const fieldDifferences = [];
|
|
660
|
+
for (const [id, recordA] of mapA) {
|
|
661
|
+
const recordB = mapB.get(id);
|
|
662
|
+
if (!recordB) {
|
|
663
|
+
missingInB.push(id);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const allFields = /* @__PURE__ */ new Set([
|
|
667
|
+
...Object.keys(recordA).filter((k) => !k.startsWith("_")),
|
|
668
|
+
...Object.keys(recordB).filter((k) => !k.startsWith("_"))
|
|
669
|
+
]);
|
|
670
|
+
for (const field of allFields) {
|
|
671
|
+
if (field === "id") continue;
|
|
672
|
+
const valA = recordA[field];
|
|
673
|
+
const valB = recordB[field];
|
|
674
|
+
if (!deepEqual(valA, valB)) {
|
|
675
|
+
fieldDifferences.push({
|
|
676
|
+
recordId: id,
|
|
677
|
+
field,
|
|
678
|
+
valueInA: valA,
|
|
679
|
+
valueInB: valB
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const id of mapB.keys()) {
|
|
685
|
+
if (!mapA.has(id)) {
|
|
686
|
+
missingInA.push(id);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (missingInB.length === 0 && missingInA.length === 0 && fieldDifferences.length === 0) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
collection,
|
|
694
|
+
deviceA: nameA,
|
|
695
|
+
deviceB: nameB,
|
|
696
|
+
missingInB,
|
|
697
|
+
missingInA,
|
|
698
|
+
fieldDifferences
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function deepEqual(a, b) {
|
|
702
|
+
if (a === b) return true;
|
|
703
|
+
if (a === null || b === null) return false;
|
|
704
|
+
if (a === void 0 || b === void 0) return false;
|
|
705
|
+
if (typeof a !== typeof b) return false;
|
|
706
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
707
|
+
if (a.length !== b.length) return false;
|
|
708
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
709
|
+
}
|
|
710
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
711
|
+
const keysA = Object.keys(a);
|
|
712
|
+
const keysB = Object.keys(b);
|
|
713
|
+
if (keysA.length !== keysB.length) return false;
|
|
714
|
+
return keysA.every(
|
|
715
|
+
(key) => deepEqual(a[key], b[key])
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/protobuf-wire-transport.ts
|
|
722
|
+
var import_sync3 = require("@korajs/sync");
|
|
723
|
+
var PROTOBUF_WIRE_TYPES = /* @__PURE__ */ new Set([
|
|
724
|
+
"handshake",
|
|
725
|
+
"handshake-response",
|
|
726
|
+
"operation-batch",
|
|
727
|
+
"acknowledgment",
|
|
728
|
+
"error"
|
|
729
|
+
]);
|
|
730
|
+
function roundTripThroughProtobuf(message, serializer) {
|
|
731
|
+
if (!PROTOBUF_WIRE_TYPES.has(message.type)) {
|
|
732
|
+
return structuredClone(message);
|
|
733
|
+
}
|
|
734
|
+
const encoded = serializer.encode(message);
|
|
735
|
+
if (!(encoded instanceof Uint8Array)) {
|
|
736
|
+
throw new Error("Expected the protobuf serializer to encode to bytes");
|
|
737
|
+
}
|
|
738
|
+
return serializer.decode(encoded);
|
|
739
|
+
}
|
|
740
|
+
var ProtobufWireClientTransport = class {
|
|
741
|
+
constructor(inner, serializer) {
|
|
742
|
+
this.inner = inner;
|
|
743
|
+
this.serializer = serializer;
|
|
744
|
+
}
|
|
745
|
+
inner;
|
|
746
|
+
serializer;
|
|
747
|
+
connect(url, options) {
|
|
748
|
+
return this.inner.connect(url, options);
|
|
749
|
+
}
|
|
750
|
+
disconnect() {
|
|
751
|
+
return this.inner.disconnect();
|
|
752
|
+
}
|
|
753
|
+
send(message) {
|
|
754
|
+
this.inner.send(roundTripThroughProtobuf(message, this.serializer));
|
|
755
|
+
}
|
|
756
|
+
onMessage(handler) {
|
|
757
|
+
this.inner.onMessage(handler);
|
|
758
|
+
}
|
|
759
|
+
onClose(handler) {
|
|
760
|
+
this.inner.onClose(handler);
|
|
761
|
+
}
|
|
762
|
+
onError(handler) {
|
|
763
|
+
this.inner.onError(handler);
|
|
764
|
+
}
|
|
765
|
+
isConnected() {
|
|
766
|
+
return this.inner.isConnected();
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
var ProtobufWireServerTransport = class {
|
|
770
|
+
constructor(inner, serializer) {
|
|
771
|
+
this.inner = inner;
|
|
772
|
+
this.serializer = serializer;
|
|
773
|
+
}
|
|
774
|
+
inner;
|
|
775
|
+
serializer;
|
|
776
|
+
send(message) {
|
|
777
|
+
this.inner.send(roundTripThroughProtobuf(message, this.serializer));
|
|
778
|
+
}
|
|
779
|
+
onMessage(handler) {
|
|
780
|
+
this.inner.onMessage(handler);
|
|
781
|
+
}
|
|
782
|
+
onClose(handler) {
|
|
783
|
+
this.inner.onClose(handler);
|
|
784
|
+
}
|
|
785
|
+
onError(handler) {
|
|
786
|
+
this.inner.onError(handler);
|
|
787
|
+
}
|
|
788
|
+
isConnected() {
|
|
789
|
+
return this.inner.isConnected();
|
|
790
|
+
}
|
|
791
|
+
close(code, reason) {
|
|
792
|
+
this.inner.close(code, reason);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
function wrapTransportPairWithProtobufWire(pair) {
|
|
796
|
+
const serializer = new import_sync3.ProtobufMessageSerializer();
|
|
797
|
+
return {
|
|
798
|
+
client: new ProtobufWireClientTransport(pair.client, serializer),
|
|
799
|
+
serverTransport: new ProtobufWireServerTransport(pair.serverTransport, serializer)
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/server-clock-transport.ts
|
|
804
|
+
var ServerClockServerTransport = class {
|
|
805
|
+
constructor(inner, serverNow) {
|
|
806
|
+
this.inner = inner;
|
|
807
|
+
this.serverNow = serverNow;
|
|
808
|
+
}
|
|
809
|
+
inner;
|
|
810
|
+
serverNow;
|
|
811
|
+
send(message) {
|
|
812
|
+
if (message.type === "handshake-response") {
|
|
813
|
+
this.inner.send({ ...message, serverTime: this.serverNow() });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
this.inner.send(message);
|
|
817
|
+
}
|
|
818
|
+
onMessage(handler) {
|
|
819
|
+
this.inner.onMessage(handler);
|
|
820
|
+
}
|
|
821
|
+
onClose(handler) {
|
|
822
|
+
this.inner.onClose(handler);
|
|
823
|
+
}
|
|
824
|
+
onError(handler) {
|
|
825
|
+
this.inner.onError(handler);
|
|
826
|
+
}
|
|
827
|
+
isConnected() {
|
|
828
|
+
return this.inner.isConnected();
|
|
829
|
+
}
|
|
830
|
+
close(code, reason) {
|
|
831
|
+
this.inner.close(code, reason);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
function wrapTransportPairWithServerClock(pair, serverNow) {
|
|
835
|
+
return {
|
|
836
|
+
client: pair.client,
|
|
837
|
+
serverTransport: new ServerClockServerTransport(pair.serverTransport, serverNow)
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/index.ts
|
|
842
|
+
var import_sync4 = require("@korajs/sync");
|
|
843
|
+
|
|
844
|
+
// src/test-auth.ts
|
|
845
|
+
function testJwt(userId, generation) {
|
|
846
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
847
|
+
return `${encode({ alg: "none", typ: "JWT" })}.${encode({ sub: userId, dev: "test-device", generation })}.test`;
|
|
848
|
+
}
|
|
849
|
+
function createTestAuthBinding(options) {
|
|
850
|
+
let generation = 0;
|
|
851
|
+
let state = options?.initialUserId ? {
|
|
852
|
+
state: "authenticated",
|
|
853
|
+
userId: options.initialUserId,
|
|
854
|
+
token: testJwt(options.initialUserId, generation)
|
|
855
|
+
} : { state: "loading" };
|
|
856
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
857
|
+
const notify = () => {
|
|
858
|
+
for (const listener of listeners) listener();
|
|
859
|
+
};
|
|
860
|
+
return {
|
|
861
|
+
get state() {
|
|
862
|
+
return state;
|
|
863
|
+
},
|
|
864
|
+
auth: async () => ({ token: state.state === "authenticated" ? state.token ?? "" : "" }),
|
|
865
|
+
resolveSyncState: async () => state,
|
|
866
|
+
resolveUserId: async () => state.state === "authenticated" ? state.userId : void 0,
|
|
867
|
+
resolveNodeId: async () => state.state === "authenticated" ? "test-device" : void 0,
|
|
868
|
+
subscribe(listener) {
|
|
869
|
+
listeners.add(listener);
|
|
870
|
+
return () => listeners.delete(listener);
|
|
871
|
+
},
|
|
872
|
+
setLoading() {
|
|
873
|
+
state = { state: "loading" };
|
|
874
|
+
notify();
|
|
875
|
+
},
|
|
876
|
+
signIn(userId, token) {
|
|
877
|
+
generation++;
|
|
878
|
+
state = { state: "authenticated", userId, token: token ?? testJwt(userId, generation) };
|
|
879
|
+
notify();
|
|
880
|
+
},
|
|
881
|
+
refreshToken(token) {
|
|
882
|
+
if (state.state !== "authenticated") throw new Error("Cannot refresh while signed out");
|
|
883
|
+
generation++;
|
|
884
|
+
state = {
|
|
885
|
+
...state,
|
|
886
|
+
token: token ?? testJwt(state.userId, generation)
|
|
887
|
+
};
|
|
888
|
+
notify();
|
|
889
|
+
},
|
|
890
|
+
signOut() {
|
|
891
|
+
state = options?.anonymous ? { state: "anonymous", mayConnectAnonymously: true } : { state: "signed-out", mayConnectAnonymously: false };
|
|
892
|
+
notify();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/transport-metrics.ts
|
|
898
|
+
function wireBytes(message) {
|
|
899
|
+
return new TextEncoder().encode(JSON.stringify(message)).byteLength;
|
|
900
|
+
}
|
|
901
|
+
function createTransportMetrics() {
|
|
902
|
+
const metrics = {
|
|
903
|
+
connectionAttempts: 0,
|
|
904
|
+
disconnections: 0,
|
|
905
|
+
clientMessages: 0,
|
|
906
|
+
serverMessages: 0,
|
|
907
|
+
clientBytes: 0,
|
|
908
|
+
serverBytes: 0,
|
|
909
|
+
reset() {
|
|
910
|
+
this.connectionAttempts = 0;
|
|
911
|
+
this.disconnections = 0;
|
|
912
|
+
this.clientMessages = 0;
|
|
913
|
+
this.serverMessages = 0;
|
|
914
|
+
this.clientBytes = 0;
|
|
915
|
+
this.serverBytes = 0;
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
return {
|
|
919
|
+
metrics,
|
|
920
|
+
wrapTransport(pair) {
|
|
921
|
+
const client = {
|
|
922
|
+
...pair.client,
|
|
923
|
+
async connect(url, options) {
|
|
924
|
+
metrics.connectionAttempts++;
|
|
925
|
+
await pair.client.connect(url, options);
|
|
926
|
+
},
|
|
927
|
+
async disconnect() {
|
|
928
|
+
metrics.disconnections++;
|
|
929
|
+
await pair.client.disconnect();
|
|
930
|
+
},
|
|
931
|
+
send(message) {
|
|
932
|
+
metrics.clientMessages++;
|
|
933
|
+
metrics.clientBytes += wireBytes(message);
|
|
934
|
+
pair.client.send(message);
|
|
935
|
+
},
|
|
936
|
+
onMessage: (handler) => pair.client.onMessage(handler),
|
|
937
|
+
onClose: (handler) => pair.client.onClose(handler),
|
|
938
|
+
onError: (handler) => pair.client.onError(handler),
|
|
939
|
+
isConnected: () => pair.client.isConnected()
|
|
940
|
+
};
|
|
941
|
+
const serverTransport = {
|
|
942
|
+
...pair.serverTransport,
|
|
943
|
+
send(message) {
|
|
944
|
+
metrics.serverMessages++;
|
|
945
|
+
metrics.serverBytes += wireBytes(message);
|
|
946
|
+
pair.serverTransport.send(message);
|
|
947
|
+
},
|
|
948
|
+
onMessage: (handler) => pair.serverTransport.onMessage(handler),
|
|
949
|
+
onClose: (handler) => pair.serverTransport.onClose(handler),
|
|
950
|
+
onError: (handler) => pair.serverTransport.onError(handler),
|
|
951
|
+
isConnected: () => pair.serverTransport.isConnected(),
|
|
952
|
+
close: (code, reason) => pair.serverTransport.close(code, reason)
|
|
953
|
+
};
|
|
954
|
+
return { client, serverTransport };
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
959
|
+
0 && (module.exports = {
|
|
960
|
+
ChaosTransport,
|
|
961
|
+
TestDevice,
|
|
962
|
+
TestServer,
|
|
963
|
+
checkConvergence,
|
|
964
|
+
createMixedTestNetwork,
|
|
965
|
+
createTestAuthBinding,
|
|
966
|
+
createTestNetwork,
|
|
967
|
+
createTransportMetrics,
|
|
968
|
+
expectConverged,
|
|
969
|
+
expectConvergedEventually,
|
|
970
|
+
wrapTransportPairWithProtobufWire,
|
|
971
|
+
wrapTransportPairWithServerClock
|
|
972
|
+
});
|
|
973
|
+
//# sourceMappingURL=index.cjs.map
|