@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/LICENSE +21 -0
- package/dist/index.cjs +207 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -4
- package/dist/index.d.ts +90 -4
- package/dist/index.js +216 -6
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|