@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1c7ec43d41
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/{browser/chunk-35I6ERLG.mjs → neutral/chunk-VIHYDCCC.mjs} +313 -709
- package/dist/lib/neutral/chunk-VIHYDCCC.mjs.map +7 -0
- package/dist/lib/neutral/chunk-YBC6ONBZ.mjs +247 -0
- package/dist/lib/neutral/chunk-YBC6ONBZ.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/neutral/index.mjs +7065 -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 +99 -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 +38 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- 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 +2 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +9 -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 +8 -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 +9 -8
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +62 -27
- 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 +56 -54
- package/src/automerge/automerge-host-subduction.test.ts +247 -0
- package/src/automerge/automerge-host.test.ts +65 -39
- package/src/automerge/automerge-host.ts +589 -206
- package/src/automerge/automerge-repo-subduction.test.ts +1049 -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 +53 -17
- package/src/automerge/collection-synchronizer.ts +119 -36
- 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 +74 -30
- package/src/automerge/echo-replicator.ts +41 -15
- package/src/automerge/handle-state.ts +62 -0
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +2 -1
- package/src/automerge/leveldb-storage-adapter.ts +40 -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 +25 -26
- package/src/db-host/database-root.ts +20 -8
- package/src/db-host/documents-synchronizer.test.ts +143 -14
- package/src/db-host/documents-synchronizer.ts +72 -55
- package/src/db-host/echo-host.ts +348 -96
- 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 +184 -20
- 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 +82 -8
- package/src/query/query-executor.ts +1199 -164
- package/src/query/query-planner.test.ts +993 -81
- package/src/query/query-planner.ts +467 -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
|
@@ -2,39 +2,168 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { generateAutomergeUrl, parseAutomergeUrl, Repo } from '@automerge/automerge-repo';
|
|
5
|
+
import { type DocumentId } from '@automerge/automerge-repo';
|
|
7
6
|
import { describe, expect, test } from 'vitest';
|
|
8
7
|
|
|
9
|
-
import { sleep } from '@dxos/async';
|
|
8
|
+
import { Trigger, asyncTimeout, sleep } from '@dxos/async';
|
|
9
|
+
import { Context } from '@dxos/context';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
10
12
|
import { openAndClose } from '@dxos/test-utils';
|
|
11
13
|
|
|
14
|
+
import { AutomergeHost } from '../automerge';
|
|
12
15
|
import { DocumentsSynchronizer } from './documents-synchronizer';
|
|
13
16
|
|
|
14
17
|
describe('DocumentsSynchronizer', () => {
|
|
18
|
+
test('two synchronizers receive updates for shared document', async () => {
|
|
19
|
+
const kv = createTestLevel();
|
|
20
|
+
const host = new AutomergeHost({
|
|
21
|
+
db: kv,
|
|
22
|
+
});
|
|
23
|
+
await openAndClose(host);
|
|
24
|
+
const handle = await host.createDoc<{ text: string }>({ text: 'initial' });
|
|
25
|
+
|
|
26
|
+
// Create two synchronizers (simulates two clients connected to the same host).
|
|
27
|
+
const initialSync1 = new Trigger();
|
|
28
|
+
const initialSync2 = new Trigger();
|
|
29
|
+
const propagatedUpdate2 = new Trigger();
|
|
30
|
+
let initialSyncSettled = false;
|
|
31
|
+
const synchronizer1 = new DocumentsSynchronizer({
|
|
32
|
+
automergeHost: host,
|
|
33
|
+
sendUpdates: (batch) => {
|
|
34
|
+
for (const update of batch.updates ?? []) {
|
|
35
|
+
if (update.documentId === handle.documentId) {
|
|
36
|
+
initialSync1.wake();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
await openAndClose(synchronizer1);
|
|
42
|
+
|
|
43
|
+
const updates2: string[] = [];
|
|
44
|
+
const synchronizer2 = new DocumentsSynchronizer({
|
|
45
|
+
automergeHost: host,
|
|
46
|
+
sendUpdates: (batch) => {
|
|
47
|
+
for (const update of batch.updates ?? []) {
|
|
48
|
+
updates2.push(update.documentId);
|
|
49
|
+
if (update.documentId === handle.documentId) {
|
|
50
|
+
if (initialSyncSettled) {
|
|
51
|
+
propagatedUpdate2.wake();
|
|
52
|
+
} else {
|
|
53
|
+
initialSync2.wake();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
await openAndClose(synchronizer2);
|
|
60
|
+
|
|
61
|
+
// Both synchronizers subscribe to the same document.
|
|
62
|
+
await synchronizer1.addDocuments([handle.documentId]);
|
|
63
|
+
await synchronizer2.addDocuments([handle.documentId]);
|
|
64
|
+
|
|
65
|
+
await asyncTimeout(Promise.all([initialSync1.wait(), initialSync2.wait()]), 1_000);
|
|
66
|
+
initialSyncSettled = true;
|
|
67
|
+
const initialUpdates2 = updates2.length;
|
|
68
|
+
|
|
69
|
+
// Synchronizer 1 makes a change (simulates client 1 creating an object).
|
|
70
|
+
await synchronizer1.update(Context.default(), [
|
|
71
|
+
{
|
|
72
|
+
documentId: handle.documentId,
|
|
73
|
+
mutation: new Uint8Array([]), // Empty mutation for test - the actual mutation is applied via handle.change
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// Apply the actual change to the handle (simulates what happens when client sends mutation).
|
|
78
|
+
handle.change((doc: any) => {
|
|
79
|
+
doc.text = 'modified by client 1';
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await asyncTimeout(propagatedUpdate2.wait(), 1_000);
|
|
83
|
+
|
|
84
|
+
// Synchronizer 2 should receive the update even though synchronizer 1 made the change.
|
|
85
|
+
expect(updates2.length).to.be.greaterThan(initialUpdates2);
|
|
86
|
+
});
|
|
87
|
+
|
|
15
88
|
test('do not get init changes for client created docs', async () => {
|
|
16
89
|
let counter = 0;
|
|
17
|
-
const
|
|
90
|
+
const kv = createTestLevel();
|
|
91
|
+
const host = new AutomergeHost({
|
|
92
|
+
db: kv,
|
|
93
|
+
});
|
|
94
|
+
await openAndClose(host);
|
|
18
95
|
const synchronizer = new DocumentsSynchronizer({
|
|
19
|
-
|
|
96
|
+
automergeHost: host,
|
|
20
97
|
sendUpdates: () => {
|
|
21
98
|
counter++;
|
|
22
99
|
},
|
|
23
100
|
});
|
|
24
101
|
await openAndClose(synchronizer);
|
|
25
102
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
32
|
-
]);
|
|
103
|
+
// First create the document on the host (simulates DataService.createDocument).
|
|
104
|
+
const handle = await host.createDoc<{ text: string }>({ text: 'hello' });
|
|
105
|
+
|
|
106
|
+
// Add document to synchronizer (simulates updateSubscription with addIds).
|
|
107
|
+
await synchronizer.addDocuments([handle.documentId]);
|
|
33
108
|
|
|
34
109
|
// Wait for the changes to be processed.
|
|
35
110
|
await sleep(100);
|
|
36
111
|
|
|
37
|
-
//
|
|
38
|
-
|
|
112
|
+
// Updates will be sent for the initial sync (this is expected behavior).
|
|
113
|
+
// The key is that subsequent updates from the client should be properly synced.
|
|
114
|
+
expect(counter).to.be.greaterThanOrEqual(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('persistence', () => {
|
|
118
|
+
test('document created on host persists without explicit flush', async () => {
|
|
119
|
+
const path = `/tmp/dxos-${PublicKey.random().toHex()}`;
|
|
120
|
+
let documentId: DocumentId;
|
|
121
|
+
const text = 'Hello World!';
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
const kv = createTestLevel(path);
|
|
125
|
+
const host = new AutomergeHost({
|
|
126
|
+
db: kv,
|
|
127
|
+
});
|
|
128
|
+
await openAndClose(host);
|
|
129
|
+
const synchronizer = new DocumentsSynchronizer({
|
|
130
|
+
automergeHost: host,
|
|
131
|
+
sendUpdates: () => {},
|
|
132
|
+
});
|
|
133
|
+
await openAndClose(synchronizer);
|
|
134
|
+
|
|
135
|
+
// Create document on host (simulates DataService.createDocument).
|
|
136
|
+
const handle = await host.createDoc<{ text: string }>({ text });
|
|
137
|
+
documentId = handle.documentId;
|
|
138
|
+
|
|
139
|
+
// Add to synchronizer (simulates updateSubscription with addIds).
|
|
140
|
+
await synchronizer.addDocuments([documentId]);
|
|
141
|
+
|
|
142
|
+
// Wait for auto-save (no explicit flush).
|
|
143
|
+
await sleep(500);
|
|
144
|
+
|
|
145
|
+
// Close in same order as repo-proxy.test.ts: level first, then host.
|
|
146
|
+
await kv.close();
|
|
147
|
+
await host.close();
|
|
148
|
+
await synchronizer.close();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
// Reopen and verify persistence.
|
|
153
|
+
const kv = createTestLevel(path);
|
|
154
|
+
const host = new AutomergeHost({
|
|
155
|
+
db: kv,
|
|
156
|
+
});
|
|
157
|
+
await openAndClose(host);
|
|
158
|
+
|
|
159
|
+
const handle = await host.loadDoc<{ text: string }>(Context.default(), documentId);
|
|
160
|
+
await handle.whenReady();
|
|
161
|
+
|
|
162
|
+
expect(handle.doc()?.text).to.equal(text);
|
|
163
|
+
|
|
164
|
+
await kv.close();
|
|
165
|
+
await host.close();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
39
168
|
});
|
|
40
169
|
});
|
|
@@ -3,30 +3,34 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { next as A, type Heads } from '@automerge/automerge';
|
|
6
|
-
import { type
|
|
6
|
+
import { type DocumentId, type DocumentQuery } from '@automerge/automerge-repo';
|
|
7
7
|
|
|
8
8
|
import { UpdateScheduler } from '@dxos/async';
|
|
9
|
-
import { Resource } from '@dxos/context';
|
|
9
|
+
import { Context, LifecycleState, Resource } from '@dxos/context';
|
|
10
10
|
import { type DatabaseDirectory } from '@dxos/echo-protocol';
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { log } from '@dxos/log';
|
|
13
13
|
import { type BatchedDocumentUpdates, type DocumentUpdate } from '@dxos/protocols/proto/dxos/echo/service';
|
|
14
|
+
import { retry } from '@dxos/util';
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
+
import type { AutomergeHost } from '../automerge';
|
|
16
17
|
|
|
17
18
|
const MAX_UPDATE_FREQ = 10; // [updates/sec]
|
|
18
19
|
|
|
19
|
-
export type
|
|
20
|
-
|
|
20
|
+
export type DocumentsSynchronizerProps = {
|
|
21
|
+
automergeHost: AutomergeHost;
|
|
21
22
|
sendUpdates: (updates: BatchedDocumentUpdates) => void;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
interface DocSyncState {
|
|
25
|
-
|
|
26
|
+
docQuery: DocumentQuery<DatabaseDirectory>;
|
|
26
27
|
lastSentHead?: Heads;
|
|
27
28
|
clearSubscriptions?: () => void;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
const WRAP_AROUND_RETRY_LIMIT = 3;
|
|
32
|
+
const WRAP_AROUND_RETRY_INITIAL_DELAY = 100; // [ms]
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* Manages a connection and replication between worker's Automerge Repo and the client's Repo.
|
|
32
36
|
*/
|
|
@@ -43,30 +47,36 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
43
47
|
*/
|
|
44
48
|
private _sendUpdatesJob?: UpdateScheduler = undefined;
|
|
45
49
|
|
|
46
|
-
constructor(private readonly _params:
|
|
50
|
+
constructor(private readonly _params: DocumentsSynchronizerProps) {
|
|
47
51
|
super();
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
addDocuments(documentIds: DocumentId[]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
async addDocuments(documentIds: DocumentId[]): Promise<void> {
|
|
55
|
+
await Promise.all(
|
|
56
|
+
documentIds.map(async (documentId) => {
|
|
57
|
+
try {
|
|
58
|
+
await retry(
|
|
59
|
+
{ count: WRAP_AROUND_RETRY_LIMIT, delayMs: WRAP_AROUND_RETRY_INITIAL_DELAY, exponent: 2 },
|
|
60
|
+
async () => {
|
|
61
|
+
try {
|
|
62
|
+
log('loading document', { documentId });
|
|
63
|
+
const docQuery = this._params.automergeHost.findWithProgress<DatabaseDirectory>(
|
|
64
|
+
documentId as DocumentId,
|
|
65
|
+
);
|
|
66
|
+
this._startSync(docQuery);
|
|
67
|
+
this._pendingUpdates.add(docQuery.documentId);
|
|
68
|
+
this._sendUpdatesJob!.trigger();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.warn('failed to load document', { err });
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.catch(err);
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
removeDocuments(documentIds: DocumentId[]): void {
|
|
@@ -88,36 +98,35 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
88
98
|
this._syncStates.clear();
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
async update(updates: DocumentUpdate[]): Promise<void> {
|
|
92
|
-
for (const { documentId, mutation
|
|
93
|
-
|
|
94
|
-
const doc = await this._params.repo.find<DatabaseDirectory>(documentId as DocumentId, FIND_PARAMS);
|
|
95
|
-
doc.update((doc) => A.loadIncremental(doc, mutation));
|
|
96
|
-
this._startSync(doc);
|
|
97
|
-
} else {
|
|
98
|
-
this._writeMutation(documentId as DocumentId, mutation);
|
|
99
|
-
}
|
|
101
|
+
async update(ctx: Context, updates: DocumentUpdate[]): Promise<void> {
|
|
102
|
+
for (const { documentId, mutation } of updates) {
|
|
103
|
+
await this._writeMutation(documentId as DocumentId, mutation);
|
|
100
104
|
}
|
|
105
|
+
// TODO(mykola): This should not be required.
|
|
106
|
+
await this._params.automergeHost.flush(ctx, {
|
|
107
|
+
documentIds: updates.map(({ documentId }) => documentId as DocumentId),
|
|
108
|
+
});
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
private _startSync(
|
|
104
|
-
if (this._syncStates.has(
|
|
105
|
-
log('Document already being synced', { documentId:
|
|
111
|
+
private _startSync(docQuery: DocumentQuery<DatabaseDirectory>) {
|
|
112
|
+
if (this._syncStates.has(docQuery.documentId)) {
|
|
113
|
+
log('Document already being synced', { documentId: docQuery.documentId });
|
|
106
114
|
return;
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
const syncState: DocSyncState = {
|
|
117
|
+
const syncState: DocSyncState = { docQuery };
|
|
110
118
|
this._subscribeForChanges(syncState);
|
|
111
|
-
this._syncStates.set(
|
|
119
|
+
this._syncStates.set(docQuery.documentId, syncState);
|
|
120
|
+
return syncState;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
_subscribeForChanges(syncState: DocSyncState): void {
|
|
115
124
|
const handler = () => {
|
|
116
|
-
this._pendingUpdates.add(syncState.
|
|
125
|
+
this._pendingUpdates.add(syncState.docQuery.documentId);
|
|
117
126
|
this._sendUpdatesJob!.trigger();
|
|
118
127
|
};
|
|
119
|
-
syncState.handle.on('heads-changed', handler);
|
|
120
|
-
syncState.clearSubscriptions = () => syncState.handle.off('heads-changed', handler);
|
|
128
|
+
syncState.docQuery.handle.on('heads-changed', handler);
|
|
129
|
+
syncState.clearSubscriptions = () => syncState.docQuery.handle.off('heads-changed', handler);
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
private async _checkAndSendUpdates(): Promise<void> {
|
|
@@ -144,11 +153,14 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
144
153
|
private _getPendingChanges(documentId: DocumentId): Uint8Array | void {
|
|
145
154
|
const syncState = this._syncStates.get(documentId);
|
|
146
155
|
invariant(syncState, 'Sync state for document not found');
|
|
147
|
-
const handle = syncState.handle;
|
|
148
|
-
if (!handle ||
|
|
156
|
+
const handle = syncState.docQuery.handle;
|
|
157
|
+
if (!handle || syncState.docQuery.peek().state !== 'ready') {
|
|
149
158
|
return;
|
|
150
159
|
}
|
|
151
160
|
const doc = handle.doc();
|
|
161
|
+
if (!doc) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
152
164
|
const mutation = syncState.lastSentHead ? A.saveSince(doc, syncState.lastSentHead) : A.save(doc);
|
|
153
165
|
if (mutation.length === 0) {
|
|
154
166
|
return;
|
|
@@ -157,16 +169,21 @@ export class DocumentsSynchronizer extends Resource {
|
|
|
157
169
|
return mutation;
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
private _writeMutation(documentId: DocumentId, mutation: Uint8Array): void {
|
|
172
|
+
private async _writeMutation(documentId: DocumentId, mutation: Uint8Array): Promise<void> {
|
|
173
|
+
if (this._lifecycleState === LifecycleState.CLOSED) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
log('write mutation', { documentId });
|
|
177
|
+
|
|
161
178
|
const syncState = this._syncStates.get(documentId);
|
|
162
179
|
invariant(syncState, 'Sync state for document not found');
|
|
163
|
-
syncState.handle.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
180
|
+
const headsBefore = A.getHeads(syncState.docQuery.handle.doc());
|
|
181
|
+
// This will update corresponding handle in the repo.
|
|
182
|
+
await this._params.automergeHost.createDoc(mutation, { documentId, preserveHistory: true });
|
|
183
|
+
|
|
184
|
+
if (A.equals(headsBefore, syncState.lastSentHead)) {
|
|
185
|
+
// No new mutations were discovered on network, so we do not need to send updates from worker to client.
|
|
186
|
+
syncState.lastSentHead = A.getHeads(syncState.docQuery.handle.doc());
|
|
187
|
+
}
|
|
171
188
|
}
|
|
172
189
|
}
|