@dxos/echo-pipeline 0.8.3 → 0.8.4-main.05e74ebcff
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 +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/chunk-46DCRDA7.mjs +271 -0
- package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
- package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-LR7UFVQX.mjs} +313 -709
- package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/neutral/index.mjs +7332 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-host.d.ts +119 -54
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
- package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +46 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/errors.d.ts +35 -0
- package/dist/types/src/automerge/errors.d.ts.map +1 -0
- package/dist/types/src/automerge/handle-state.d.ts +52 -0
- package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
- package/dist/types/src/automerge/heads-store.d.ts +9 -2
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +3 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +11 -5
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
- package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
- package/dist/types/src/common/codec.d.ts +1 -1
- package/dist/types/src/common/codec.d.ts.map +1 -1
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/common/space-id.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
- package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts +13 -7
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +9 -3
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +74 -28
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +2 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
- package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
- package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +19 -11
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
- package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
- package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +11 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
- package/dist/types/src/edge/index.d.ts +1 -0
- package/dist/types/src/edge/index.d.ts.map +1 -1
- package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
- package/dist/types/src/filter/filter-match.d.ts +4 -1
- package/dist/types/src/filter/filter-match.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts +3 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +22 -10
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +66 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +66 -11
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +12 -1
- package/dist/types/src/query/query-planner.d.ts.map +1 -1
- package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
- package/dist/types/src/space/auth.d.ts +3 -3
- package/dist/types/src/space/auth.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +5 -4
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +8 -8
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +10 -9
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +6 -6
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +0 -2
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +14 -13
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +59 -55
- package/src/automerge/automerge-host-subduction.test.ts +263 -0
- package/src/automerge/automerge-host.test.ts +162 -39
- package/src/automerge/automerge-host.ts +679 -201
- package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
- package/src/automerge/automerge-repo.test.ts +349 -91
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +98 -17
- package/src/automerge/collection-synchronizer.ts +141 -37
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +5 -4
- package/src/automerge/echo-network-adapter.ts +76 -31
- package/src/automerge/echo-replicator.ts +50 -15
- package/src/automerge/errors.ts +10 -0
- package/src/automerge/handle-state.ts +62 -0
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +3 -1
- package/src/automerge/leveldb-storage-adapter.ts +77 -11
- package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
- package/src/automerge/mesh-echo-replicator.ts +15 -11
- package/src/automerge/storage-adapter.test.ts +1 -1
- package/src/common/space-id.ts +1 -1
- package/src/db-host/automerge-data-source.test.ts +299 -0
- package/src/db-host/automerge-data-source.ts +187 -0
- package/src/db-host/data-service.ts +35 -26
- package/src/db-host/database-root.ts +20 -8
- package/src/db-host/documents-synchronizer.test.ts +145 -14
- package/src/db-host/documents-synchronizer.ts +163 -51
- package/src/db-host/echo-host.ts +384 -99
- package/src/db-host/index.ts +2 -0
- package/src/db-host/invalidation-hint.ts +50 -0
- package/src/db-host/local-queue-service.ts +127 -0
- package/src/db-host/query-invalidation.test.ts +270 -0
- package/src/db-host/query-service.ts +111 -106
- package/src/db-host/queue-data-source.ts +160 -0
- package/src/db-host/queue-service.test.ts +145 -0
- package/src/db-host/space-state-manager.ts +12 -10
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +12 -10
- package/src/edge/echo-edge-replicator.ts +144 -58
- package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
- package/src/edge/echo-edge-subduction-replicator.ts +377 -0
- package/src/edge/index.ts +1 -0
- package/src/filter/filter-match.test.ts +68 -32
- package/src/filter/filter-match.ts +230 -21
- package/src/metadata/metadata-store.ts +11 -8
- package/src/pipeline/pipeline-stress.test.ts +1 -1
- package/src/pipeline/pipeline.test.ts +2 -2
- package/src/pipeline/pipeline.ts +9 -7
- package/src/query/errors.ts +3 -1
- package/src/query/plan.ts +84 -9
- package/src/query/query-executor.ts +1197 -165
- package/src/query/query-planner.test.ts +993 -81
- package/src/query/query-planner.ts +503 -27
- package/src/space/admission-discovery-extension.ts +3 -3
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +5 -4
- package/src/space/control-pipeline.ts +16 -13
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +16 -20
- package/src/space/space-protocol.browser.test.ts +15 -14
- package/src/space/space-protocol.test.ts +15 -14
- package/src/space/space-protocol.ts +22 -18
- package/src/space/space.test.ts +3 -3
- package/src/space/space.ts +11 -11
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +3 -3
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +19 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
- package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -4743
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
- package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
- package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
- package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
- package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
- package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
- package/dist/lib/node/filter/index.cjs +0 -32
- package/dist/lib/node/filter/index.cjs.map +0 -7
- package/dist/lib/node/index.cjs +0 -4699
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node/testing/index.cjs +0 -753
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -11
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4743
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -742
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
- package/dist/types/src/testing/test-data.d.ts +0 -18
- package/dist/types/src/testing/test-data.d.ts.map +0 -1
- package/dist/types/src/testing/test-schema.d.ts +0 -37
- package/dist/types/src/testing/test-schema.d.ts.map +0 -1
- package/src/db-host/documents-iterator.ts +0 -74
- package/src/testing/test-data.ts +0 -127
- package/src/testing/test-schema.ts +0 -53
- /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Repo, generateAutomergeUrl, initSubduction, parseAutomergeUrl } from '@automerge/automerge-repo';
|
|
6
|
+
import {
|
|
7
|
+
AuthenticatedTransport,
|
|
8
|
+
BlobMeta,
|
|
9
|
+
CommitId,
|
|
10
|
+
Digest,
|
|
11
|
+
MemorySigner,
|
|
12
|
+
MemoryStorage,
|
|
13
|
+
Nonce,
|
|
14
|
+
SedimentreeId,
|
|
15
|
+
Subduction,
|
|
16
|
+
commitIdOfBase58Id,
|
|
17
|
+
} from '@automerge/automerge-subduction';
|
|
18
|
+
import { beforeAll, describe, test } from 'vitest';
|
|
19
|
+
|
|
20
|
+
import { sleep } from '@dxos/async';
|
|
21
|
+
|
|
22
|
+
class AsyncQueue<T> {
|
|
23
|
+
private _items: T[] = [];
|
|
24
|
+
private _waiters: ((item: T) => void)[] = [];
|
|
25
|
+
|
|
26
|
+
push(item: T): void {
|
|
27
|
+
const waiter = this._waiters.shift();
|
|
28
|
+
if (waiter) {
|
|
29
|
+
waiter(item);
|
|
30
|
+
} else {
|
|
31
|
+
this._items.push(item);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async pull(): Promise<T> {
|
|
36
|
+
const item = this._items.shift();
|
|
37
|
+
if (item !== undefined) {
|
|
38
|
+
return item;
|
|
39
|
+
}
|
|
40
|
+
return new Promise<T>((resolve) => this._waiters.push(resolve));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clear(): void {
|
|
44
|
+
this._items.length = 0;
|
|
45
|
+
this._waiters.length = 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pure-JS transport backed by async queues.
|
|
51
|
+
* Implements the subduction Transport interface for in-memory testing.
|
|
52
|
+
*/
|
|
53
|
+
class MemoryTransport {
|
|
54
|
+
private _outBytes: AsyncQueue<Uint8Array>;
|
|
55
|
+
private _inBytes: AsyncQueue<Uint8Array>;
|
|
56
|
+
private _disconnectCallbacks: (() => void)[] = [];
|
|
57
|
+
|
|
58
|
+
constructor(outBytes: AsyncQueue<Uint8Array>, inBytes: AsyncQueue<Uint8Array>) {
|
|
59
|
+
this._outBytes = outBytes;
|
|
60
|
+
this._inBytes = inBytes;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async sendBytes(bytes: Uint8Array): Promise<void> {
|
|
64
|
+
this._outBytes.push(new Uint8Array(bytes));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async recvBytes(): Promise<Uint8Array> {
|
|
68
|
+
return this._inBytes.pull();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async disconnect(): Promise<void> {
|
|
72
|
+
for (const callback of this._disconnectCallbacks) {
|
|
73
|
+
callback();
|
|
74
|
+
}
|
|
75
|
+
this._inBytes.clear();
|
|
76
|
+
this._outBytes.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onDisconnect(callback: () => void): void {
|
|
80
|
+
this._disconnectCallbacks.push(callback);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const createMemoryTransportPair = (): [MemoryTransport, MemoryTransport] => {
|
|
85
|
+
const bytesAB = new AsyncQueue<Uint8Array>();
|
|
86
|
+
const bytesBA = new AsyncQueue<Uint8Array>();
|
|
87
|
+
return [new MemoryTransport(bytesAB, bytesBA), new MemoryTransport(bytesBA, bytesAB)];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build a deterministic {@link CommitId} seeded by a small integer. Values are arbitrary;
|
|
92
|
+
* used in tests only to satisfy the `head` parameter of `Subduction.addCommit`.
|
|
93
|
+
*/
|
|
94
|
+
const commitIdOf = (seed: number): CommitId => CommitId.fromBytes(new Uint8Array(32).fill(seed));
|
|
95
|
+
|
|
96
|
+
describe('automerge-subduction', () => {
|
|
97
|
+
beforeAll(async () => {
|
|
98
|
+
await initSubduction();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('creates signer and signs payloads', async ({ expect }) => {
|
|
102
|
+
const signer = MemorySigner.generate();
|
|
103
|
+
const signature = await signer.sign(new Uint8Array([1, 2, 3]));
|
|
104
|
+
const verifyingKey = signer.verifyingKey();
|
|
105
|
+
const peerId = signer.peerId();
|
|
106
|
+
|
|
107
|
+
expect(signature).toBeInstanceOf(Uint8Array);
|
|
108
|
+
expect(signature.length).toBe(64);
|
|
109
|
+
expect(verifyingKey).toBeInstanceOf(Uint8Array);
|
|
110
|
+
expect(verifyingKey.length).toBe(32);
|
|
111
|
+
expect(peerId.toString().length).toBeGreaterThan(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('round-trips digest bytes and hex', ({ expect }) => {
|
|
115
|
+
const digestBytes = new Uint8Array(32).fill(2);
|
|
116
|
+
const digest = Digest.fromBytes(digestBytes);
|
|
117
|
+
const fromHex = Digest.fromHexString(digest.toHexString());
|
|
118
|
+
|
|
119
|
+
expect(digest.toBytes()).toEqual(digestBytes);
|
|
120
|
+
expect(digest.toHexString()).toHaveLength(64);
|
|
121
|
+
expect(fromHex.toBytes()).toEqual(digestBytes);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('nonce random bytes are fixed-width', ({ expect }) => {
|
|
125
|
+
const nonce = Nonce.random();
|
|
126
|
+
expect(nonce.bytes.length).toBe(8);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('blob meta reports digest and size', ({ expect }) => {
|
|
130
|
+
const blobMeta = new BlobMeta(new Uint8Array([5, 6, 7, 8]));
|
|
131
|
+
expect(blobMeta.sizeBytes).toBe(4n);
|
|
132
|
+
expect(blobMeta.digest().toBytes().length).toBe(32);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('stores and loads sedimentree IDs in memory storage', async ({ expect }) => {
|
|
136
|
+
const storage = new MemoryStorage();
|
|
137
|
+
const id = SedimentreeId.fromBytes(new Uint8Array(32).fill(7));
|
|
138
|
+
|
|
139
|
+
await storage.saveSedimentreeId(id);
|
|
140
|
+
const ids = await storage.loadAllSedimentreeIds();
|
|
141
|
+
|
|
142
|
+
expect(ids).toHaveLength(1);
|
|
143
|
+
expect(ids[0].toBytes()).toEqual(id.toBytes());
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('adds a commit and exposes it through subduction read APIs', async ({ expect }) => {
|
|
147
|
+
const signer = MemorySigner.generate();
|
|
148
|
+
const storage = new MemoryStorage();
|
|
149
|
+
const subduction = new Subduction(signer, storage);
|
|
150
|
+
const id = SedimentreeId.fromBytes(new Uint8Array(32).fill(9));
|
|
151
|
+
const blob = new Uint8Array([10, 11, 12]);
|
|
152
|
+
|
|
153
|
+
const requestedFragment = await subduction.addCommit(id, commitIdOf(1), [], blob);
|
|
154
|
+
const commits = await subduction.getCommits(id);
|
|
155
|
+
const blobs = await subduction.getBlobs(id);
|
|
156
|
+
|
|
157
|
+
expect(requestedFragment).toBeUndefined();
|
|
158
|
+
expect(commits).toBeDefined();
|
|
159
|
+
expect(commits).toHaveLength(1);
|
|
160
|
+
expect(blobs).toHaveLength(1);
|
|
161
|
+
expect(blobs[0]).toEqual(blob);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('throws on invalid base58 input', ({ expect }) => {
|
|
165
|
+
expect(() => commitIdOfBase58Id('contains-0')).toThrow();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('automerge-repo URL helpers round-trip document IDs', ({ expect }) => {
|
|
169
|
+
const url = generateAutomergeUrl();
|
|
170
|
+
const parsed = parseAutomergeUrl(url);
|
|
171
|
+
|
|
172
|
+
expect(parsed.documentId.length).toBeGreaterThan(0);
|
|
173
|
+
expect(`automerge:${parsed.documentId}`).toBe(url);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('automerge-repo creates documents with subduction signer', async ({ expect }) => {
|
|
177
|
+
const signer = MemorySigner.generate();
|
|
178
|
+
const repo = new Repo({
|
|
179
|
+
network: [],
|
|
180
|
+
signer,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const handle = repo.create<{ title?: string }>();
|
|
184
|
+
handle.change((doc) => {
|
|
185
|
+
doc.title = 'hello';
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(handle.doc()?.title).toBe('hello');
|
|
189
|
+
await repo.shutdown();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('establishes subduction connection and syncs via AuthenticatedTransport', async ({ expect }) => {
|
|
193
|
+
const signerA = MemorySigner.generate();
|
|
194
|
+
const signerB = MemorySigner.generate();
|
|
195
|
+
const subductionA = new Subduction(signerA, new MemoryStorage());
|
|
196
|
+
const subductionB = new Subduction(signerB, new MemoryStorage());
|
|
197
|
+
|
|
198
|
+
const [transportA, transportB] = createMemoryTransportPair();
|
|
199
|
+
|
|
200
|
+
const [authA, authB] = await Promise.all([
|
|
201
|
+
AuthenticatedTransport.setup(transportA, signerA, signerB.peerId()),
|
|
202
|
+
AuthenticatedTransport.accept(transportB, signerB),
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
await subductionA.addConnection(authA);
|
|
206
|
+
await subductionB.addConnection(authB);
|
|
207
|
+
|
|
208
|
+
const peersA = await subductionA.getConnectedPeerIds();
|
|
209
|
+
const peersB = await subductionB.getConnectedPeerIds();
|
|
210
|
+
expect(peersA).toHaveLength(1);
|
|
211
|
+
expect(peersB).toHaveLength(1);
|
|
212
|
+
|
|
213
|
+
const sid = SedimentreeId.fromBytes(new Uint8Array(32).fill(42));
|
|
214
|
+
await subductionA.addCommit(sid, commitIdOf(2), [], new Uint8Array([1, 2, 3]));
|
|
215
|
+
|
|
216
|
+
const result = await subductionA.syncWithAllPeers(sid, true);
|
|
217
|
+
expect(result.entries().length).toBeGreaterThan(0);
|
|
218
|
+
|
|
219
|
+
const blobsOnB = await subductionB.getBlobs(sid);
|
|
220
|
+
expect(blobsOnB).toHaveLength(1);
|
|
221
|
+
}, 10_000);
|
|
222
|
+
|
|
223
|
+
test('syncs between two subduction instances using connectTransport', async ({ expect }) => {
|
|
224
|
+
const signerA = MemorySigner.generate();
|
|
225
|
+
const signerB = MemorySigner.generate();
|
|
226
|
+
const subductionA = new Subduction(signerA, new MemoryStorage(), 'test-service');
|
|
227
|
+
const subductionB = new Subduction(signerB, new MemoryStorage(), 'test-service');
|
|
228
|
+
|
|
229
|
+
const [transportA, transportB] = createMemoryTransportPair();
|
|
230
|
+
|
|
231
|
+
const [peerIdB, peerIdA] = await Promise.all([
|
|
232
|
+
subductionA.connectTransport(transportA, 'test-service'),
|
|
233
|
+
subductionB.acceptTransport(transportB, 'test-service'),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
expect(peerIdB.toString()).toBe(signerB.peerId().toString());
|
|
237
|
+
expect(peerIdA.toString()).toBe(signerA.peerId().toString());
|
|
238
|
+
|
|
239
|
+
const sid = SedimentreeId.fromBytes(new Uint8Array(32).fill(99));
|
|
240
|
+
await subductionA.addCommit(sid, commitIdOf(3), [], new Uint8Array([4, 5, 6]));
|
|
241
|
+
|
|
242
|
+
await subductionA.syncWithAllPeers(sid, false);
|
|
243
|
+
|
|
244
|
+
const blobsOnB = await subductionB.getBlobs(sid);
|
|
245
|
+
expect(blobsOnB).toHaveLength(1);
|
|
246
|
+
expect(blobsOnB[0]).toEqual(new Uint8Array([4, 5, 6]));
|
|
247
|
+
}, 10_000);
|
|
248
|
+
|
|
249
|
+
test('full sync exchanges all sedimentrees between peers', async ({ expect }) => {
|
|
250
|
+
const signerA = MemorySigner.generate();
|
|
251
|
+
const signerB = MemorySigner.generate();
|
|
252
|
+
const subductionA = new Subduction(signerA, new MemoryStorage());
|
|
253
|
+
const subductionB = new Subduction(signerB, new MemoryStorage());
|
|
254
|
+
|
|
255
|
+
// Add commits on both sides before connecting.
|
|
256
|
+
const sidA = SedimentreeId.fromBytes(new Uint8Array(32).fill(1));
|
|
257
|
+
const sidB = SedimentreeId.fromBytes(new Uint8Array(32).fill(2));
|
|
258
|
+
await subductionA.addCommit(sidA, commitIdOf(5), [], new Uint8Array([10, 20]));
|
|
259
|
+
await subductionB.addCommit(sidB, commitIdOf(6), [], new Uint8Array([30, 40]));
|
|
260
|
+
|
|
261
|
+
const [transportA, transportB] = createMemoryTransportPair();
|
|
262
|
+
const [authA, authB] = await Promise.all([
|
|
263
|
+
AuthenticatedTransport.setup(transportA, signerA, signerB.peerId()),
|
|
264
|
+
AuthenticatedTransport.accept(transportB, signerB),
|
|
265
|
+
]);
|
|
266
|
+
await subductionA.addConnection(authA);
|
|
267
|
+
await subductionB.addConnection(authB);
|
|
268
|
+
|
|
269
|
+
// Full sync exchanges everything.
|
|
270
|
+
await subductionA.fullSyncWithPeer(signerB.peerId());
|
|
271
|
+
await subductionB.fullSyncWithPeer(signerA.peerId());
|
|
272
|
+
|
|
273
|
+
const blobsAonB = await subductionB.getBlobs(sidA);
|
|
274
|
+
const blobsBonA = await subductionA.getBlobs(sidB);
|
|
275
|
+
expect(blobsAonB).toHaveLength(1);
|
|
276
|
+
expect(blobsAonB[0]).toEqual(new Uint8Array([10, 20]));
|
|
277
|
+
expect(blobsBonA).toHaveLength(1);
|
|
278
|
+
expect(blobsBonA[0]).toEqual(new Uint8Array([30, 40]));
|
|
279
|
+
}, 10_000);
|
|
280
|
+
|
|
281
|
+
// After B drops handshake state and rehydrates from durable storage, does
|
|
282
|
+
// subduction re-handshake on the reused logical channel when A pushes?
|
|
283
|
+
// Empirical answer: no — A's auto-broadcast does not reach B'.
|
|
284
|
+
test(
|
|
285
|
+
'does not auto-rehydrate handshake on a logical connection where one end dropped it',
|
|
286
|
+
{ timeout: 15_000 },
|
|
287
|
+
async ({ expect }) => {
|
|
288
|
+
const signerA = MemorySigner.generate();
|
|
289
|
+
const signerB = MemorySigner.generate();
|
|
290
|
+
const storageA = new MemoryStorage();
|
|
291
|
+
const storageB = new MemoryStorage();
|
|
292
|
+
const [transportA, transportB] = createMemoryTransportPair();
|
|
293
|
+
|
|
294
|
+
const subA = new Subduction(signerA, storageA, 'svc');
|
|
295
|
+
let subB = new Subduction(signerB, storageB, 'svc');
|
|
296
|
+
|
|
297
|
+
const [authA, authB] = await Promise.all([
|
|
298
|
+
AuthenticatedTransport.setup(transportA, signerA, signerB.peerId()),
|
|
299
|
+
AuthenticatedTransport.accept(transportB, signerB),
|
|
300
|
+
]);
|
|
301
|
+
await subA.addConnection(authA);
|
|
302
|
+
await subB.addConnection(authB);
|
|
303
|
+
|
|
304
|
+
const sid = SedimentreeId.fromBytes(new Uint8Array(32).fill(7));
|
|
305
|
+
await subA.addCommit(sid, commitIdOf(1), [], new Uint8Array([1, 2, 3]));
|
|
306
|
+
await expect.poll(() => subB.getBlobs(sid).then((bs) => bs.length), { timeout: 5_000 }).toEqual(1);
|
|
307
|
+
|
|
308
|
+
// Simulate B crash: clearing the queue's waiters halts the orphaned
|
|
309
|
+
// wasm pump that survives `free()`. Do NOT use `disconnectAll` /
|
|
310
|
+
// `disconnectFromPeer` — those send a graceful goodbye A would observe.
|
|
311
|
+
await transportB.disconnect();
|
|
312
|
+
subB.free();
|
|
313
|
+
|
|
314
|
+
subB = await Subduction.hydrate(signerB, storageB, 'svc');
|
|
315
|
+
expect(await subB.getBlobs(sid)).toHaveLength(1);
|
|
316
|
+
expect(await subB.getConnectedPeerIds()).toHaveLength(0);
|
|
317
|
+
expect(await subA.getConnectedPeerIds()).toHaveLength(1);
|
|
318
|
+
|
|
319
|
+
await subA.addCommit(sid, commitIdOf(2), [commitIdOf(1)], new Uint8Array([4, 5, 6]));
|
|
320
|
+
await sleep(500);
|
|
321
|
+
|
|
322
|
+
// TODO(mykola): When subduction-core grows channel-level handshake
|
|
323
|
+
// recovery, flip to `.toHaveLength(2)` and drop "does not" from name.
|
|
324
|
+
expect(await subB.getBlobs(sid)).toHaveLength(1);
|
|
325
|
+
|
|
326
|
+
subA.free();
|
|
327
|
+
subB.free();
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
});
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
5
|
+
import * as A from '@automerge/automerge';
|
|
6
|
+
import type { DocumentId, PeerId } from '@automerge/automerge-repo';
|
|
7
|
+
import { describe, onTestFinished, test } from 'vitest';
|
|
7
8
|
|
|
8
9
|
import { sleep } from '@dxos/async';
|
|
10
|
+
import { range } from '@dxos/util';
|
|
9
11
|
|
|
10
|
-
import { CollectionSynchronizer, diffCollectionState
|
|
12
|
+
import { type CollectionState, CollectionSynchronizer, diffCollectionState } from './collection-synchronizer';
|
|
11
13
|
|
|
12
14
|
describe('CollectionSynchronizer', () => {
|
|
13
|
-
test('sync two peers', async () => {
|
|
15
|
+
test('sync two peers', async ({ expect }) => {
|
|
14
16
|
const LATENCY = 10;
|
|
15
17
|
|
|
16
18
|
const peerId1 = 'peer1' as PeerId;
|
|
@@ -53,22 +55,99 @@ describe('CollectionSynchronizer', () => {
|
|
|
53
55
|
peer1.onConnectionOpen(peerId2);
|
|
54
56
|
peer2.onConnectionOpen(peerId1);
|
|
55
57
|
|
|
58
|
+
const updated = Promise.all([
|
|
59
|
+
peer1.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId2),
|
|
60
|
+
peer2.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId1),
|
|
61
|
+
]);
|
|
62
|
+
|
|
56
63
|
peer1.setLocalCollectionState(collectionId, STATE_1);
|
|
57
64
|
peer2.setLocalCollectionState(collectionId, STATE_2);
|
|
58
65
|
|
|
59
66
|
peer1.refreshCollection(collectionId);
|
|
60
67
|
peer2.refreshCollection(collectionId);
|
|
61
68
|
|
|
62
|
-
await
|
|
63
|
-
peer1.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId2),
|
|
64
|
-
peer2.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId1),
|
|
65
|
-
]);
|
|
69
|
+
await updated;
|
|
66
70
|
|
|
67
71
|
expect(peer1.getRemoteCollectionStates(collectionId).get(peerId2)).to.deep.equal(STATE_2);
|
|
68
72
|
expect(peer2.getRemoteCollectionStates(collectionId).get(peerId1)).to.deep.equal(STATE_1);
|
|
69
73
|
});
|
|
70
74
|
|
|
71
|
-
test('
|
|
75
|
+
test('pushes state to all interested peers on setLocalCollectionState', async ({ expect }) => {
|
|
76
|
+
// Push covers peers that queried us before we had state — `onCollectionStateQueried`
|
|
77
|
+
// silently drops that case; without this push the asker would wait up to POLL_INTERVAL
|
|
78
|
+
// for its polling loop to retry (further gated by MIN_QUERY_INTERVAL).
|
|
79
|
+
const peerId1 = 'peer1' as PeerId;
|
|
80
|
+
const peerId2 = 'peer2' as PeerId;
|
|
81
|
+
const collectionId = 'collection-test';
|
|
82
|
+
|
|
83
|
+
const sentStates: Array<{ peerId: PeerId; state: CollectionState }> = [];
|
|
84
|
+
const peer = await new CollectionSynchronizer({
|
|
85
|
+
queryCollectionState: () => {},
|
|
86
|
+
sendCollectionState: (_collectionId, peerId, state) => {
|
|
87
|
+
sentStates.push({ peerId, state });
|
|
88
|
+
},
|
|
89
|
+
shouldSyncCollection: () => true,
|
|
90
|
+
}).open();
|
|
91
|
+
onTestFinished(async () => {
|
|
92
|
+
await peer.close();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
peer.onConnectionOpen(peerId1);
|
|
96
|
+
peer.onConnectionOpen(peerId2);
|
|
97
|
+
|
|
98
|
+
peer.setLocalCollectionState(collectionId, STATE_1);
|
|
99
|
+
await sleep(10);
|
|
100
|
+
|
|
101
|
+
expect(sentStates.map((m) => m.peerId).sort()).to.deep.equal([peerId1, peerId2].sort());
|
|
102
|
+
expect(sentStates.every((m) => m.state === STATE_1)).to.equal(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('does not send to a peer after onConnectionClosed', async ({ expect }) => {
|
|
106
|
+
// Regression: `onConnectionClosed` must remove the peer from `interestedPeers`,
|
|
107
|
+
// otherwise the next `_broadcastLocalState` calls `sendCollectionState` for a
|
|
108
|
+
// now-stale peerId and the real network adapter throws "Connection not found.".
|
|
109
|
+
const peerId1 = 'peer1' as PeerId;
|
|
110
|
+
const peerId2 = 'peer2' as PeerId;
|
|
111
|
+
const collectionId = 'collection-test';
|
|
112
|
+
|
|
113
|
+
const connected = new Set<PeerId>([peerId1, peerId2]);
|
|
114
|
+
const sentTo: PeerId[] = [];
|
|
115
|
+
const peer = await new CollectionSynchronizer({
|
|
116
|
+
queryCollectionState: () => {},
|
|
117
|
+
sendCollectionState: (_collectionId, peerId) => {
|
|
118
|
+
if (!connected.has(peerId)) {
|
|
119
|
+
throw new Error('Connection not found.');
|
|
120
|
+
}
|
|
121
|
+
sentTo.push(peerId);
|
|
122
|
+
},
|
|
123
|
+
shouldSyncCollection: () => true,
|
|
124
|
+
}).open();
|
|
125
|
+
onTestFinished(async () => {
|
|
126
|
+
await peer.close();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Activate the collection before any peer connects so the connect-time
|
|
130
|
+
// microtask adds peers to `interestedPeers` without ever calling
|
|
131
|
+
// `_broadcastLocalState` (so `lastBroadcast` stays unset for them).
|
|
132
|
+
peer.setLocalCollectionState(collectionId, STATE_1);
|
|
133
|
+
await sleep(10);
|
|
134
|
+
|
|
135
|
+
peer.onConnectionOpen(peerId1);
|
|
136
|
+
peer.onConnectionOpen(peerId2);
|
|
137
|
+
await sleep(10);
|
|
138
|
+
|
|
139
|
+
connected.delete(peerId2);
|
|
140
|
+
peer.onConnectionClosed(peerId2);
|
|
141
|
+
|
|
142
|
+
// Without the fix, peer2 is still in `interestedPeers` with no `lastBroadcast`
|
|
143
|
+
// entry, so the broadcast gate passes and `sendCollectionState` throws.
|
|
144
|
+
peer.setLocalCollectionState(collectionId, STATE_2);
|
|
145
|
+
await sleep(10);
|
|
146
|
+
|
|
147
|
+
expect(sentTo).to.deep.equal([peerId1]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('diff collection state', ({ expect }) => {
|
|
72
151
|
const diff = diffCollectionState(STATE_1, STATE_2);
|
|
73
152
|
|
|
74
153
|
expect(diff).to.deep.equal({
|
|
@@ -79,18 +158,20 @@ describe('CollectionSynchronizer', () => {
|
|
|
79
158
|
});
|
|
80
159
|
});
|
|
81
160
|
|
|
161
|
+
const TEST_HEADS = range(4).map((i) => A.getHeads(A.from({ i: i.toString() })));
|
|
162
|
+
|
|
82
163
|
const STATE_1: CollectionState = {
|
|
83
164
|
documents: {
|
|
84
|
-
a: [
|
|
85
|
-
b: [
|
|
86
|
-
c: [
|
|
87
|
-
},
|
|
165
|
+
a: TEST_HEADS[0],
|
|
166
|
+
b: TEST_HEADS[1],
|
|
167
|
+
c: TEST_HEADS[2],
|
|
168
|
+
} as Record<DocumentId, A.Heads>,
|
|
88
169
|
};
|
|
89
170
|
|
|
90
171
|
const STATE_2: CollectionState = {
|
|
91
172
|
documents: {
|
|
92
|
-
a: [
|
|
93
|
-
b: [
|
|
94
|
-
d: [
|
|
95
|
-
},
|
|
173
|
+
a: TEST_HEADS[0],
|
|
174
|
+
b: TEST_HEADS[3],
|
|
175
|
+
d: TEST_HEADS[2],
|
|
176
|
+
} as Record<DocumentId, A.Heads>,
|
|
96
177
|
};
|