@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1068cf700f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/neutral/chunk-FJPXA75J.mjs +235 -0
- package/dist/lib/neutral/chunk-FJPXA75J.mjs.map +7 -0
- package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-WIQUYO7H.mjs} +300 -213
- package/dist/lib/neutral/chunk-WIQUYO7H.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/{browser → neutral}/index.mjs +2832 -946
- 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 +73 -224
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host.d.ts +53 -42
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- 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/collection-synchronizer.d.ts +6 -4
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +22 -10
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +37 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- 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 +1 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +5 -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 +5 -5
- package/dist/types/src/automerge/mesh-echo-replicator.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/db-host/automerge-data-source.d.ts +34 -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/data-service.d.ts +5 -4
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +44 -23
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +1 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/local-queue-service.d.ts +16 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +14 -9
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +28 -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 +2 -1
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +10 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +8 -6
- package/dist/types/src/edge/echo-edge-replicator.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/pipeline.d.ts +3 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +21 -9
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +57 -5
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +49 -10
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +9 -0
- 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 +3 -3
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +7 -7
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +6 -6
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +4 -4
- package/dist/types/src/space/space.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-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 +53 -45
- package/src/automerge/automerge-host.test.ts +55 -24
- package/src/automerge/automerge-host.ts +399 -126
- package/src/automerge/automerge-repo.test.ts +214 -46
- package/src/automerge/collection-synchronizer.test.ts +22 -15
- package/src/automerge/collection-synchronizer.ts +57 -22
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +3 -3
- package/src/automerge/echo-network-adapter.ts +73 -28
- package/src/automerge/echo-replicator.ts +40 -15
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +1 -1
- package/src/automerge/leveldb-storage-adapter.ts +11 -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 +292 -0
- package/src/db-host/automerge-data-source.ts +144 -0
- package/src/db-host/data-service.ts +19 -20
- package/src/db-host/database-root.ts +2 -2
- package/src/db-host/documents-synchronizer.test.ts +73 -13
- package/src/db-host/documents-synchronizer.ts +58 -44
- package/src/db-host/echo-host.ts +194 -84
- package/src/db-host/index.ts +1 -0
- package/src/db-host/local-queue-service.ts +147 -0
- package/src/db-host/query-service.ts +28 -95
- package/src/db-host/queue-data-source.ts +132 -0
- package/src/db-host/queue-service.test.ts +145 -0
- package/src/db-host/space-state-manager.ts +6 -2
- package/src/db-host/stub.ts +28 -0
- package/src/edge/echo-edge-replicator.test.ts +8 -6
- package/src/edge/echo-edge-replicator.ts +124 -42
- package/src/filter/filter-match.test.ts +58 -32
- package/src/filter/filter-match.ts +163 -15
- package/src/metadata/metadata-store.ts +8 -5
- package/src/pipeline/pipeline-stress.test.ts +4 -2
- package/src/pipeline/pipeline.test.ts +3 -2
- package/src/pipeline/pipeline.ts +10 -7
- package/src/query/errors.ts +3 -1
- package/src/query/plan.ts +75 -5
- package/src/query/query-executor.ts +812 -117
- package/src/query/query-planner.test.ts +652 -34
- package/src/query/query-planner.ts +276 -13
- package/src/space/admission-discovery-extension.ts +3 -3
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +4 -3
- package/src/space/control-pipeline.ts +11 -8
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +11 -10
- package/src/space/space-protocol.browser.test.ts +2 -2
- package/src/space/space-protocol.test.ts +3 -2
- package/src/space/space-protocol.ts +17 -14
- package/src/space/space.test.ts +1 -1
- package/src/space/space.ts +6 -5
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +4 -3
- package/src/testing/test-network-adapter.ts +16 -3
- package/src/testing/test-replicator.ts +23 -18
- 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.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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
next as A,
|
|
6
7
|
type Heads,
|
|
7
8
|
change,
|
|
8
9
|
clone,
|
|
@@ -10,7 +11,6 @@ import {
|
|
|
10
11
|
from,
|
|
11
12
|
getBackend,
|
|
12
13
|
getHeads,
|
|
13
|
-
next as A,
|
|
14
14
|
save,
|
|
15
15
|
saveSince,
|
|
16
16
|
} from '@automerge/automerge';
|
|
@@ -19,28 +19,31 @@ import {
|
|
|
19
19
|
type DocHandle,
|
|
20
20
|
type DocumentId,
|
|
21
21
|
type HandleState,
|
|
22
|
-
type
|
|
22
|
+
type Message,
|
|
23
23
|
type PeerId,
|
|
24
24
|
Repo,
|
|
25
25
|
type SharePolicy,
|
|
26
|
+
type StorageAdapterInterface,
|
|
26
27
|
generateAutomergeUrl,
|
|
27
28
|
parseAutomergeUrl,
|
|
28
29
|
} from '@automerge/automerge-repo';
|
|
29
|
-
import {
|
|
30
|
+
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
30
31
|
|
|
31
32
|
import { asyncTimeout, sleep } from '@dxos/async';
|
|
32
33
|
import { randomBytes } from '@dxos/crypto';
|
|
33
34
|
import { PublicKey } from '@dxos/keys';
|
|
34
35
|
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
36
|
+
import { log } from '@dxos/log';
|
|
35
37
|
import { TestBuilder as TeleportBuilder, TestPeer as TeleportPeer } from '@dxos/teleport/testing';
|
|
36
38
|
import { openAndClose } from '@dxos/test-utils';
|
|
37
39
|
import { isNonNullable, range } from '@dxos/util';
|
|
38
40
|
|
|
41
|
+
import { TestAdapter, type TestConnectionStateProvider } from '../testing';
|
|
42
|
+
|
|
39
43
|
import { FIND_PARAMS } from './automerge-host';
|
|
40
44
|
import { EchoNetworkAdapter } from './echo-network-adapter';
|
|
41
45
|
import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
|
|
42
46
|
import { MeshEchoReplicator } from './mesh-echo-replicator';
|
|
43
|
-
import { TestAdapter, type TestConnectionStateProvider } from '../testing';
|
|
44
47
|
|
|
45
48
|
const HOST_AND_CLIENT: [string, string] = ['host', 'client'];
|
|
46
49
|
|
|
@@ -161,32 +164,6 @@ describe('AutomergeRepo', () => {
|
|
|
161
164
|
expect((await asyncTimeout(client.find<any>(handle.url), 1000))!.doc().text).to.equal(text);
|
|
162
165
|
});
|
|
163
166
|
|
|
164
|
-
test('share policy gets enabled afterwards', async () => {
|
|
165
|
-
let sharePolicy = false;
|
|
166
|
-
|
|
167
|
-
const { repos, adapters } = await createHostClientRepoTopology({
|
|
168
|
-
sharePolicy: async () => sharePolicy,
|
|
169
|
-
});
|
|
170
|
-
const [host, client] = repos;
|
|
171
|
-
await connectAdapters(adapters);
|
|
172
|
-
|
|
173
|
-
const handle = host.create();
|
|
174
|
-
const text = 'Hello world';
|
|
175
|
-
handle.change((doc: any) => {
|
|
176
|
-
doc.text = text;
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
await client.find(handle.url, { allowableStates: ['unavailable'] });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
sharePolicy = true;
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
await client.find(handle.url, { allowableStates: ['unavailable'] });
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
167
|
test('two documents and share policy switching', async () => {
|
|
191
168
|
const allowedDocs: DocumentId[] = [];
|
|
192
169
|
|
|
@@ -420,13 +397,7 @@ describe('AutomergeRepo', () => {
|
|
|
420
397
|
|
|
421
398
|
test('client creates doc and syncs with a Repo', async () => {
|
|
422
399
|
const repo = new Repo({ network: [] });
|
|
423
|
-
const receiveByServer =
|
|
424
|
-
const serverHandle = await repo.find(docId, FIND_PARAMS);
|
|
425
|
-
serverHandle.update((doc) => {
|
|
426
|
-
return A.loadIncremental(doc, blob);
|
|
427
|
-
});
|
|
428
|
-
};
|
|
429
|
-
|
|
400
|
+
const receiveByServer = (blob: Uint8Array, docId: DocumentId) => repo.import<any>(blob, { docId });
|
|
430
401
|
let clientDoc = A.from<{ field?: string }>({});
|
|
431
402
|
const { documentId } = parseAutomergeUrl(generateAutomergeUrl());
|
|
432
403
|
// Sync handshake.
|
|
@@ -434,7 +405,7 @@ describe('AutomergeRepo', () => {
|
|
|
434
405
|
|
|
435
406
|
// Sync protocol.
|
|
436
407
|
const sendDoc = async (doc: A.Doc<any>) => {
|
|
437
|
-
|
|
408
|
+
receiveByServer(saveSince(doc, sentHeads), documentId);
|
|
438
409
|
sentHeads = getHeads(doc);
|
|
439
410
|
};
|
|
440
411
|
|
|
@@ -456,10 +427,9 @@ describe('AutomergeRepo', () => {
|
|
|
456
427
|
|
|
457
428
|
const repo = new Repo({ network: [], storage });
|
|
458
429
|
const receiveByServer = async (blob: Uint8Array, docId: DocumentId) => {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
});
|
|
430
|
+
repo.import<any>(blob, { docId });
|
|
431
|
+
// TODO(mykola): This should not be required. Document is not persisted without it.
|
|
432
|
+
await repo.flush([docId]);
|
|
463
433
|
};
|
|
464
434
|
|
|
465
435
|
let clientDoc = A.from<{ field?: string }>({ field: 'foo' });
|
|
@@ -516,6 +486,139 @@ describe('AutomergeRepo', () => {
|
|
|
516
486
|
await asyncTimeout(handleB.whenReady(), 1000);
|
|
517
487
|
expect(handleB.doc()!.text).to.equal(text);
|
|
518
488
|
});
|
|
489
|
+
|
|
490
|
+
test('retry share config', async () => {
|
|
491
|
+
let announce = false;
|
|
492
|
+
|
|
493
|
+
const { repos, adapters } = await createRepoTopology({
|
|
494
|
+
peers: ['A', 'B'],
|
|
495
|
+
connections: [['A', 'B']],
|
|
496
|
+
options: {
|
|
497
|
+
shareConfig: {
|
|
498
|
+
access: async () => true,
|
|
499
|
+
announce: async () => announce,
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
onMessage: (message) => {
|
|
503
|
+
console.log(`${message.senderId} -> ${message.targetId}: ${message.type} ${message.documentId ?? ''}`);
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
const [repoA, repoB] = repos;
|
|
507
|
+
await connectAdapters(adapters);
|
|
508
|
+
console.log({ peersA: repoA.peers, peersB: repoB.peers });
|
|
509
|
+
|
|
510
|
+
const docA = repoA.create({ text: 'Hello world' });
|
|
511
|
+
|
|
512
|
+
const docB = await repoB.find(docA.url, { allowableStates: ['ready', 'unavailable'] });
|
|
513
|
+
expect(docB.state).to.equal('unavailable');
|
|
514
|
+
log.info('unavailable');
|
|
515
|
+
|
|
516
|
+
announce = true;
|
|
517
|
+
repoB.shareConfigChanged();
|
|
518
|
+
await docB.whenReady();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
describe('create2 tests', () => {
|
|
523
|
+
test('basic document creation', async () => {
|
|
524
|
+
const repo = new Repo({ network: [] });
|
|
525
|
+
|
|
526
|
+
const handle = await repo.create2<{ text: string }>();
|
|
527
|
+
expect(handle.doc()).to.deep.equal({});
|
|
528
|
+
|
|
529
|
+
handle.change((doc: any) => {
|
|
530
|
+
doc.text = 'Hello World';
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(handle.doc()?.text).to.equal('Hello World');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('document mutation', async () => {
|
|
537
|
+
const repo = new Repo({ network: [] });
|
|
538
|
+
|
|
539
|
+
const handle = await repo.create2<{ counter: number }>({ counter: 0 });
|
|
540
|
+
|
|
541
|
+
for (let i = 1; i <= 10; i++) {
|
|
542
|
+
handle.change((doc: any) => {
|
|
543
|
+
doc.counter = i;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
expect(handle.doc()?.counter).to.equal(10);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test('import document and mutate', async () => {
|
|
551
|
+
const repo = new Repo({ network: [] });
|
|
552
|
+
|
|
553
|
+
const original = await repo.create2<{ text: string }>({ text: 'original' });
|
|
554
|
+
const blob = A.save(original.doc()!);
|
|
555
|
+
|
|
556
|
+
const imported = repo.import<{ text: string }>(blob);
|
|
557
|
+
expect(imported.doc()?.text).to.equal('original');
|
|
558
|
+
|
|
559
|
+
imported.change((doc: any) => {
|
|
560
|
+
doc.text = 'mutated';
|
|
561
|
+
});
|
|
562
|
+
expect(imported.doc()?.text).to.equal('mutated');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('reload document with flush', async () => {
|
|
566
|
+
const path = createTmpPath();
|
|
567
|
+
const text = 'Hello World!';
|
|
568
|
+
let url: AutomergeUrl;
|
|
569
|
+
|
|
570
|
+
{
|
|
571
|
+
const level = createTestLevel(path);
|
|
572
|
+
const storage = await createLevelAdapter(level);
|
|
573
|
+
const repo = new Repo({ network: [], storage });
|
|
574
|
+
const handle = await repo.create2<{ text: string }>();
|
|
575
|
+
url = handle.url;
|
|
576
|
+
handle.change((doc: any) => {
|
|
577
|
+
doc.text = text;
|
|
578
|
+
});
|
|
579
|
+
await repo.flush([handle.documentId]);
|
|
580
|
+
await level.close();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
{
|
|
584
|
+
const level = createTestLevel(path);
|
|
585
|
+
const storage = await createLevelAdapter(level);
|
|
586
|
+
const repo = new Repo({ network: [], storage });
|
|
587
|
+
const handle = await repo.find<{ text: string }>(url);
|
|
588
|
+
await handle.whenReady();
|
|
589
|
+
expect(handle.doc()?.text).to.equal(text);
|
|
590
|
+
await level.close();
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('reload document without flush', async () => {
|
|
595
|
+
const path = createTmpPath();
|
|
596
|
+
const text = 'Hello World!';
|
|
597
|
+
let url: AutomergeUrl;
|
|
598
|
+
|
|
599
|
+
{
|
|
600
|
+
const level = createTestLevel(path);
|
|
601
|
+
const storage = await createLevelAdapter(level);
|
|
602
|
+
const repo = new Repo({ network: [], storage });
|
|
603
|
+
const handle = await repo.create2<{ text: string }>();
|
|
604
|
+
url = handle.url;
|
|
605
|
+
handle.change((doc: any) => {
|
|
606
|
+
doc.text = text;
|
|
607
|
+
});
|
|
608
|
+
// No explicit flush - rely on auto-save.
|
|
609
|
+
await sleep(200);
|
|
610
|
+
await level.close();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
{
|
|
614
|
+
const level = createTestLevel(path);
|
|
615
|
+
const storage = await createLevelAdapter(level);
|
|
616
|
+
const repo = new Repo({ network: [], storage });
|
|
617
|
+
const handle = await repo.find<{ text: string }>(url);
|
|
618
|
+
await handle.whenReady();
|
|
619
|
+
expect(handle.doc()?.text).to.equal(text);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
519
622
|
});
|
|
520
623
|
|
|
521
624
|
describe('teleport', () => {
|
|
@@ -677,10 +780,66 @@ describe('AutomergeRepo', () => {
|
|
|
677
780
|
await doc.whenReady();
|
|
678
781
|
expect(doc.doc()).to.deep.eq(document.doc());
|
|
679
782
|
});
|
|
783
|
+
|
|
784
|
+
test('document is passively replicated to connected peers', async () => {
|
|
785
|
+
const [spaceKey] = PublicKey.randomSequence();
|
|
786
|
+
|
|
787
|
+
const teleportBuilder = new TeleportBuilder();
|
|
788
|
+
onTestFinished(() => teleportBuilder.destroy());
|
|
789
|
+
|
|
790
|
+
const peer1 = await createTeleportTestPeer(teleportBuilder, spaceKey);
|
|
791
|
+
const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey);
|
|
792
|
+
|
|
793
|
+
const handle = peer1.repo.create();
|
|
794
|
+
handle.change((doc: any) => (doc.text = 'hello'));
|
|
795
|
+
await connectPeers(spaceKey, teleportBuilder, peer1, peer2);
|
|
796
|
+
|
|
797
|
+
await expect
|
|
798
|
+
.poll(async () => {
|
|
799
|
+
const doc = peer2.repo.handles[handle.documentId];
|
|
800
|
+
await doc.whenReady();
|
|
801
|
+
return doc.doc()!.text;
|
|
802
|
+
})
|
|
803
|
+
.toEqual('hello');
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test('imported document is passively replicated to connected peers', async () => {
|
|
807
|
+
let blob: Uint8Array;
|
|
808
|
+
let documentId: DocumentId;
|
|
809
|
+
{
|
|
810
|
+
const repo = new Repo();
|
|
811
|
+
const handle = repo.create();
|
|
812
|
+
handle.change((doc: any) => (doc.text = 'hello'));
|
|
813
|
+
blob = A.save(handle.doc()!);
|
|
814
|
+
documentId = handle.documentId;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const [spaceKey] = PublicKey.randomSequence();
|
|
818
|
+
|
|
819
|
+
const teleportBuilder = new TeleportBuilder();
|
|
820
|
+
onTestFinished(() => teleportBuilder.destroy());
|
|
821
|
+
|
|
822
|
+
const peer1 = await createTeleportTestPeer(teleportBuilder, spaceKey);
|
|
823
|
+
const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey);
|
|
824
|
+
await connectPeers(spaceKey, teleportBuilder, peer1, peer2);
|
|
825
|
+
|
|
826
|
+
const handle = peer1.repo.import(blob, { docId: documentId });
|
|
827
|
+
await handle.whenReady();
|
|
828
|
+
|
|
829
|
+
await expect
|
|
830
|
+
.poll(
|
|
831
|
+
async () => {
|
|
832
|
+
const doc = peer2.repo.handles[handle.documentId];
|
|
833
|
+
await doc.whenReady();
|
|
834
|
+
return doc.doc()!.text;
|
|
835
|
+
},
|
|
836
|
+
{ timeout: 1_000 },
|
|
837
|
+
)
|
|
838
|
+
.toEqual('hello');
|
|
839
|
+
});
|
|
680
840
|
});
|
|
681
841
|
|
|
682
|
-
const createLevelAdapter = async () => {
|
|
683
|
-
const level = createTestLevel();
|
|
842
|
+
const createLevelAdapter = async (level = createTestLevel()) => {
|
|
684
843
|
const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
685
844
|
await openAndClose(level, storage);
|
|
686
845
|
return storage;
|
|
@@ -702,19 +861,23 @@ describe('AutomergeRepo', () => {
|
|
|
702
861
|
};
|
|
703
862
|
});
|
|
704
863
|
|
|
864
|
+
type ShareConfig = Exclude<ConstructorParameters<typeof Repo>[0], undefined>['shareConfig'];
|
|
865
|
+
|
|
705
866
|
type ConnectedRepoOptions = {
|
|
706
867
|
storages?: StorageAdapterInterface[];
|
|
707
868
|
connectionStateProvider?: TestConnectionStateProvider;
|
|
708
869
|
sharePolicy?: SharePolicy;
|
|
870
|
+
shareConfig?: ShareConfig;
|
|
709
871
|
};
|
|
710
872
|
|
|
711
873
|
const createRepoTopology = async <Peers extends string[], Peer extends string = Peers[number]>(args: {
|
|
712
874
|
peers: Peers;
|
|
713
875
|
connections: [Peer, Peer][];
|
|
714
876
|
options?: ConnectedRepoOptions;
|
|
877
|
+
onMessage?: (message: Message) => void;
|
|
715
878
|
}) => {
|
|
716
879
|
const adapters = args.connections.map(
|
|
717
|
-
() => TestAdapter.createPair(args.options?.connectionStateProvider) as [TestAdapter, TestAdapter],
|
|
880
|
+
() => TestAdapter.createPair(args.options?.connectionStateProvider, args.onMessage) as [TestAdapter, TestAdapter],
|
|
718
881
|
);
|
|
719
882
|
const repos = args.peers.map((peerId, peerIndex) => {
|
|
720
883
|
const network = adapters
|
|
@@ -730,7 +893,8 @@ const createRepoTopology = async <Peers extends string[], Peer extends string =
|
|
|
730
893
|
peerId: peerId as PeerId,
|
|
731
894
|
storage: args.options?.storages?.[peerIndex],
|
|
732
895
|
network,
|
|
733
|
-
sharePolicy: args.options?.sharePolicy ??
|
|
896
|
+
sharePolicy: (args.options?.sharePolicy ?? args.options?.shareConfig) ? undefined : async () => true,
|
|
897
|
+
shareConfig: args.options?.shareConfig,
|
|
734
898
|
});
|
|
735
899
|
});
|
|
736
900
|
return { repos, adapters };
|
|
@@ -816,3 +980,7 @@ const connectPeers = async (
|
|
|
816
980
|
};
|
|
817
981
|
|
|
818
982
|
type TeleportTestPeer = { repo: Repo; meshAdapter: MeshEchoReplicator; teleport: TeleportPeer };
|
|
983
|
+
|
|
984
|
+
export const createTmpPath = (): string => {
|
|
985
|
+
return `/tmp/dxos-${PublicKey.random().toHex()}`;
|
|
986
|
+
};
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import type
|
|
6
|
-
import
|
|
5
|
+
import { type Heads } from '@automerge/automerge';
|
|
6
|
+
import * as Automerge from '@automerge/automerge';
|
|
7
|
+
import type { DocumentId, PeerId } from '@automerge/automerge-repo';
|
|
8
|
+
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
7
9
|
|
|
8
10
|
import { sleep } from '@dxos/async';
|
|
11
|
+
import { range } from '@dxos/util';
|
|
9
12
|
|
|
10
|
-
import { CollectionSynchronizer, diffCollectionState
|
|
13
|
+
import { type CollectionState, CollectionSynchronizer, diffCollectionState } from './collection-synchronizer';
|
|
11
14
|
|
|
12
15
|
describe('CollectionSynchronizer', () => {
|
|
13
16
|
test('sync two peers', async () => {
|
|
@@ -53,16 +56,18 @@ describe('CollectionSynchronizer', () => {
|
|
|
53
56
|
peer1.onConnectionOpen(peerId2);
|
|
54
57
|
peer2.onConnectionOpen(peerId1);
|
|
55
58
|
|
|
59
|
+
const updated = Promise.all([
|
|
60
|
+
peer1.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId2),
|
|
61
|
+
peer2.remoteStateUpdated.waitFor((ev) => ev.collectionId === collectionId && ev.peerId === peerId1),
|
|
62
|
+
]);
|
|
63
|
+
|
|
56
64
|
peer1.setLocalCollectionState(collectionId, STATE_1);
|
|
57
65
|
peer2.setLocalCollectionState(collectionId, STATE_2);
|
|
58
66
|
|
|
59
67
|
peer1.refreshCollection(collectionId);
|
|
60
68
|
peer2.refreshCollection(collectionId);
|
|
61
69
|
|
|
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
|
-
]);
|
|
70
|
+
await updated;
|
|
66
71
|
|
|
67
72
|
expect(peer1.getRemoteCollectionStates(collectionId).get(peerId2)).to.deep.equal(STATE_2);
|
|
68
73
|
expect(peer2.getRemoteCollectionStates(collectionId).get(peerId1)).to.deep.equal(STATE_1);
|
|
@@ -79,18 +84,20 @@ describe('CollectionSynchronizer', () => {
|
|
|
79
84
|
});
|
|
80
85
|
});
|
|
81
86
|
|
|
87
|
+
const TEST_HEADS = range(4).map((i) => Automerge.getHeads(Automerge.from({ i: i.toString() })));
|
|
88
|
+
|
|
82
89
|
const STATE_1: CollectionState = {
|
|
83
90
|
documents: {
|
|
84
|
-
a: [
|
|
85
|
-
b: [
|
|
86
|
-
c: [
|
|
87
|
-
},
|
|
91
|
+
a: TEST_HEADS[0],
|
|
92
|
+
b: TEST_HEADS[1],
|
|
93
|
+
c: TEST_HEADS[2],
|
|
94
|
+
} as Record<DocumentId, Heads>,
|
|
88
95
|
};
|
|
89
96
|
|
|
90
97
|
const STATE_2: CollectionState = {
|
|
91
98
|
documents: {
|
|
92
|
-
a: [
|
|
93
|
-
b: [
|
|
94
|
-
d: [
|
|
95
|
-
},
|
|
99
|
+
a: TEST_HEADS[0],
|
|
100
|
+
b: TEST_HEADS[3],
|
|
101
|
+
d: TEST_HEADS[2],
|
|
102
|
+
} as Record<DocumentId, Heads>,
|
|
96
103
|
};
|
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { next as
|
|
5
|
+
import { next as A, type Heads } from '@automerge/automerge';
|
|
6
6
|
import type { DocumentId, PeerId } from '@automerge/automerge-repo';
|
|
7
|
+
import * as Array from 'effect/Array';
|
|
8
|
+
import * as Record from 'effect/Record';
|
|
7
9
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
+
import { Event, asyncReturn, scheduleTask, scheduleTaskInterval } from '@dxos/async';
|
|
11
|
+
import { type Context, Resource } from '@dxos/context';
|
|
10
12
|
import { log } from '@dxos/log';
|
|
11
13
|
import { trace } from '@dxos/tracing';
|
|
12
14
|
import { defaultMap } from '@dxos/util';
|
|
13
15
|
|
|
14
16
|
const MIN_QUERY_INTERVAL = 5_000;
|
|
15
17
|
|
|
16
|
-
const POLL_INTERVAL =
|
|
18
|
+
const POLL_INTERVAL = 10_000;
|
|
17
19
|
|
|
18
|
-
export type
|
|
20
|
+
export type CollectionSynchronizerProps = {
|
|
19
21
|
sendCollectionState: (collectionId: string, peerId: PeerId, state: CollectionState) => void;
|
|
20
22
|
queryCollectionState: (collectionId: string, peerId: PeerId) => void;
|
|
21
23
|
shouldSyncCollection: (collectionId: string, peerId: PeerId) => boolean;
|
|
@@ -26,9 +28,9 @@ export type CollectionSynchronizerParams = {
|
|
|
26
28
|
*/
|
|
27
29
|
@trace.resource()
|
|
28
30
|
export class CollectionSynchronizer extends Resource {
|
|
29
|
-
private readonly _sendCollectionState:
|
|
30
|
-
private readonly _queryCollectionState:
|
|
31
|
-
private readonly _shouldSyncCollection:
|
|
31
|
+
private readonly _sendCollectionState: CollectionSynchronizerProps['sendCollectionState'];
|
|
32
|
+
private readonly _queryCollectionState: CollectionSynchronizerProps['queryCollectionState'];
|
|
33
|
+
private readonly _shouldSyncCollection: CollectionSynchronizerProps['shouldSyncCollection'];
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* CollectionId -> State.
|
|
@@ -40,7 +42,7 @@ export class CollectionSynchronizer extends Resource {
|
|
|
40
42
|
|
|
41
43
|
public readonly remoteStateUpdated = new Event<{ collectionId: string; peerId: PeerId; newDocsAppeared: boolean }>();
|
|
42
44
|
|
|
43
|
-
constructor(params:
|
|
45
|
+
constructor(params: CollectionSynchronizerProps) {
|
|
44
46
|
super();
|
|
45
47
|
this._sendCollectionState = params.sendCollectionState;
|
|
46
48
|
this._queryCollectionState = params.queryCollectionState;
|
|
@@ -76,6 +78,10 @@ export class CollectionSynchronizer extends Resource {
|
|
|
76
78
|
log('setLocalCollectionState', { collectionId, state });
|
|
77
79
|
this._getOrCreatePerCollectionState(collectionId).localState = state;
|
|
78
80
|
|
|
81
|
+
for (const peerId of this._connectedPeers) {
|
|
82
|
+
this._diffCollectionState(collectionId, peerId);
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
queueMicrotask(async () => {
|
|
80
86
|
if (!this._ctx.disposed && this._activeCollections.has(collectionId)) {
|
|
81
87
|
this._refreshInterestedPeers(collectionId);
|
|
@@ -117,6 +123,7 @@ export class CollectionSynchronizer extends Resource {
|
|
|
117
123
|
* Callback when a connection to a peer is established.
|
|
118
124
|
*/
|
|
119
125
|
onConnectionOpen(peerId: PeerId): void {
|
|
126
|
+
log('onConnectionOpen', { peerId });
|
|
120
127
|
const spanId = getSpanName(peerId);
|
|
121
128
|
trace.spanStart({
|
|
122
129
|
id: spanId,
|
|
@@ -146,6 +153,8 @@ export class CollectionSynchronizer extends Resource {
|
|
|
146
153
|
* Callback when a connection to a peer is closed.
|
|
147
154
|
*/
|
|
148
155
|
onConnectionClosed(peerId: PeerId): void {
|
|
156
|
+
log('onConnectionClosed', { peerId });
|
|
157
|
+
|
|
149
158
|
this._connectedPeers.delete(peerId);
|
|
150
159
|
|
|
151
160
|
for (const perCollectionState of this._perCollectionStates.values()) {
|
|
@@ -171,8 +180,20 @@ export class CollectionSynchronizer extends Resource {
|
|
|
171
180
|
log('onRemoteStateReceived', { collectionId, peerId, state });
|
|
172
181
|
validateCollectionState(state);
|
|
173
182
|
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
perCollectionState.remoteStates.set(peerId, state);
|
|
184
|
+
this._diffCollectionState(collectionId, peerId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private _diffCollectionState(collectionId: string, peerId: PeerId) {
|
|
188
|
+
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
189
|
+
const remoteState = perCollectionState.remoteStates.get(peerId);
|
|
190
|
+
if (!remoteState) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
log('diffCollectionState', { collectionId, peerId });
|
|
195
|
+
const localState = perCollectionState.localState ?? { documents: {} };
|
|
196
|
+
const diff = diffCollectionState(localState, remoteState);
|
|
176
197
|
const spanId = getSpanName(peerId);
|
|
177
198
|
if (diff.different.length === 0) {
|
|
178
199
|
trace.spanEnd(spanId);
|
|
@@ -186,9 +207,20 @@ export class CollectionSynchronizer extends Resource {
|
|
|
186
207
|
attributes: { peerId },
|
|
187
208
|
});
|
|
188
209
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
log('diff', {
|
|
211
|
+
localState: localState.documents,
|
|
212
|
+
remoteState: remoteState.documents,
|
|
213
|
+
missingOnLocal: diff.missingOnLocal,
|
|
214
|
+
missingOnRemote: diff.missingOnRemote,
|
|
215
|
+
different: diff.different,
|
|
216
|
+
});
|
|
217
|
+
if (diff.missingOnLocal.length > 0 || diff.different.length > 0 || diff.missingOnRemote.length > 0) {
|
|
218
|
+
log('emit remote state update');
|
|
219
|
+
this.remoteStateUpdated.emit({
|
|
220
|
+
peerId,
|
|
221
|
+
collectionId,
|
|
222
|
+
newDocsAppeared: diff.missingOnLocal.length > 0,
|
|
223
|
+
});
|
|
192
224
|
}
|
|
193
225
|
}
|
|
194
226
|
|
|
@@ -223,7 +255,7 @@ export type CollectionState = {
|
|
|
223
255
|
/**
|
|
224
256
|
* DocumentId -> Heads.
|
|
225
257
|
*/
|
|
226
|
-
documents: Record<
|
|
258
|
+
documents: Record<DocumentId, Heads>;
|
|
227
259
|
};
|
|
228
260
|
|
|
229
261
|
export type CollectionStateDiff = {
|
|
@@ -233,18 +265,21 @@ export type CollectionStateDiff = {
|
|
|
233
265
|
};
|
|
234
266
|
|
|
235
267
|
export const diffCollectionState = (local: CollectionState, remote: CollectionState): CollectionStateDiff => {
|
|
236
|
-
const
|
|
268
|
+
const localDocuments = Record.filter(local.documents, (heads) => heads.length > 0);
|
|
269
|
+
const remoteDocuments = Record.filter(remote.documents, (heads) => heads.length > 0);
|
|
270
|
+
// NOTE: Using `Array.union` is slow.
|
|
271
|
+
const allDocuments = [...new Set([...Record.keys(localDocuments), ...Record.keys(remoteDocuments)])] as DocumentId[];
|
|
237
272
|
|
|
238
273
|
const missingOnRemote: DocumentId[] = [];
|
|
239
274
|
const missingOnLocal: DocumentId[] = [];
|
|
240
275
|
const different: DocumentId[] = [];
|
|
241
276
|
for (const documentId of allDocuments) {
|
|
242
|
-
if (!
|
|
243
|
-
missingOnLocal.push(documentId
|
|
244
|
-
} else if (!
|
|
245
|
-
missingOnRemote.push(documentId
|
|
246
|
-
} else if (!
|
|
247
|
-
different.push(documentId
|
|
277
|
+
if (!localDocuments[documentId]) {
|
|
278
|
+
missingOnLocal.push(documentId);
|
|
279
|
+
} else if (!remoteDocuments[documentId]) {
|
|
280
|
+
missingOnRemote.push(documentId);
|
|
281
|
+
} else if (!A.equals(local.documents[documentId], remote.documents[documentId])) {
|
|
282
|
+
different.push(documentId);
|
|
248
283
|
}
|
|
249
284
|
}
|
|
250
285
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { type Message } from '@automerge/automerge-repo';
|
|
6
6
|
|
|
7
7
|
import { type TimeAware, trace } from '@dxos/tracing';
|
|
8
|
-
import { CircularBuffer,
|
|
8
|
+
import { CircularBuffer, SlidingWindowSummary, type SlidingWindowSummaryConfig, mapValues } from '@dxos/util';
|
|
9
9
|
|
|
10
10
|
import { type NetworkDataMonitor } from './echo-network-adapter';
|
|
11
11
|
import { type StorageAdapterDataMonitor } from './leveldb-storage-adapter';
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { type PeerId, cbor } from '@automerge/automerge-repo';
|
|
6
|
+
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Trigger, sleep, waitForCondition } from '@dxos/async';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { PublicKey } from '@dxos/keys';
|
|
11
11
|
import { type SyncMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|