@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,57 +3,55 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
type Doc,
|
|
7
|
+
type Heads,
|
|
6
8
|
getBackend,
|
|
7
9
|
getHeads,
|
|
8
|
-
isAutomerge,
|
|
9
10
|
equals as headsEquals,
|
|
11
|
+
isAutomerge,
|
|
10
12
|
save,
|
|
11
|
-
type Doc,
|
|
12
|
-
type Heads,
|
|
13
13
|
} from '@automerge/automerge';
|
|
14
14
|
import {
|
|
15
|
-
type DocHandleChangePayload,
|
|
16
|
-
Repo,
|
|
17
15
|
type AnyDocumentId,
|
|
18
16
|
type DocHandle,
|
|
17
|
+
type DocHandleChangePayload,
|
|
19
18
|
type DocumentId,
|
|
19
|
+
type HandleState,
|
|
20
20
|
type PeerCandidatePayload,
|
|
21
21
|
type PeerDisconnectedPayload,
|
|
22
22
|
type PeerId,
|
|
23
|
+
Repo,
|
|
23
24
|
type StorageAdapterInterface,
|
|
24
25
|
type StorageKey,
|
|
25
26
|
interpretAsDocumentId,
|
|
26
|
-
type HandleState,
|
|
27
27
|
} from '@automerge/automerge-repo';
|
|
28
|
+
import type * as Record from 'effect/Record';
|
|
28
29
|
|
|
29
|
-
import { Event, asyncTimeout } from '@dxos/async';
|
|
30
|
-
import { Context, Resource, cancelWithContext
|
|
31
|
-
import {
|
|
32
|
-
import { type IndexMetadataStore } from '@dxos/indexing';
|
|
30
|
+
import { DeferredTask, Event, asyncTimeout } from '@dxos/async';
|
|
31
|
+
import { Context, type Lifecycle, Resource, cancelWithContext } from '@dxos/context';
|
|
32
|
+
import { type CollectionId, DatabaseDirectory } from '@dxos/echo-protocol';
|
|
33
33
|
import { invariant } from '@dxos/invariant';
|
|
34
34
|
import { PublicKey } from '@dxos/keys';
|
|
35
35
|
import { type LevelDB } from '@dxos/kv-store';
|
|
36
36
|
import { log } from '@dxos/log';
|
|
37
|
-
import {
|
|
37
|
+
import { type SpaceSyncState } from '@dxos/protocols/proto/dxos/echo/service';
|
|
38
38
|
import { type DocHeadsList, type FlushRequest } from '@dxos/protocols/proto/dxos/echo/service';
|
|
39
39
|
import { trace } from '@dxos/tracing';
|
|
40
|
-
import { bufferToArray } from '@dxos/util';
|
|
40
|
+
import { ComplexSet, bufferToArray, defaultMap, isNonNullable, range } from '@dxos/util';
|
|
41
41
|
|
|
42
|
-
import { CollectionSynchronizer, diffCollectionState
|
|
42
|
+
import { type CollectionState, CollectionSynchronizer, diffCollectionState } from './collection-synchronizer';
|
|
43
43
|
import { type EchoDataMonitor } from './echo-data-monitor';
|
|
44
44
|
import { EchoNetworkAdapter, isEchoPeerMetadata } from './echo-network-adapter';
|
|
45
|
-
import { type
|
|
45
|
+
import { type AutomergeReplicator, type RemoteDocumentExistenceCheckProps } from './echo-replicator';
|
|
46
46
|
import { HeadsStore } from './heads-store';
|
|
47
|
-
import {
|
|
47
|
+
import { type BeforeSaveProps, LevelDBStorageAdapter } from './leveldb-storage-adapter';
|
|
48
48
|
|
|
49
49
|
export type PeerIdProvider = () => string | undefined;
|
|
50
50
|
|
|
51
51
|
export type RootDocumentSpaceKeyProvider = (documentId: string) => PublicKey | undefined;
|
|
52
52
|
|
|
53
|
-
export type
|
|
53
|
+
export type AutomergeHostProps = {
|
|
54
54
|
db: LevelDB;
|
|
55
|
-
|
|
56
|
-
indexMetadataStore: IndexMetadataStore;
|
|
57
55
|
dataMonitor?: EchoDataMonitor;
|
|
58
56
|
|
|
59
57
|
/**
|
|
@@ -65,6 +63,12 @@ export type AutomergeHostParams = {
|
|
|
65
63
|
|
|
66
64
|
export type LoadDocOptions = {
|
|
67
65
|
timeout?: number;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* If true, the document will be fetched from the network if it is not found in the local storage.
|
|
69
|
+
* Setting this to false does not guarantee that the document will not be fetched from the network.
|
|
70
|
+
*/
|
|
71
|
+
fetchFromNetwork?: boolean;
|
|
68
72
|
};
|
|
69
73
|
|
|
70
74
|
export type CreateDocOptions = {
|
|
@@ -72,19 +76,38 @@ export type CreateDocOptions = {
|
|
|
72
76
|
* Import the document together with its history.
|
|
73
77
|
*/
|
|
74
78
|
preserveHistory?: boolean;
|
|
79
|
+
|
|
80
|
+
documentId?: DocumentId;
|
|
75
81
|
};
|
|
76
82
|
|
|
77
83
|
export const FIND_PARAMS = {
|
|
78
84
|
allowableStates: ['ready', 'requesting'] satisfies HandleState[],
|
|
79
85
|
};
|
|
80
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Maximum amount of documents to sync in a single bundle.
|
|
89
|
+
*/
|
|
90
|
+
const BUNDLE_SIZE = 100;
|
|
91
|
+
/**
|
|
92
|
+
* Maximum amount of concurrent tasks to run when pushing or pulling bundles.
|
|
93
|
+
*/
|
|
94
|
+
const BUNDLE_SYNC_CONCURRENCY = 2;
|
|
95
|
+
/**
|
|
96
|
+
* If the number of documents to sync is greater than this threshold, we will use bundles.
|
|
97
|
+
*/
|
|
98
|
+
const BUNDLE_SYNC_THRESHOLD = 50;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Only announce documents that are known to require sync.
|
|
102
|
+
*/
|
|
103
|
+
const OPTIMIZED_SHARE_POLICY = true;
|
|
104
|
+
|
|
81
105
|
/**
|
|
82
106
|
* Abstracts over the AutomergeRepo.
|
|
83
107
|
*/
|
|
84
108
|
@trace.resource()
|
|
85
109
|
export class AutomergeHost extends Resource {
|
|
86
110
|
private readonly _db: LevelDB;
|
|
87
|
-
private readonly _indexMetadataStore: IndexMetadataStore;
|
|
88
111
|
private readonly _echoNetworkAdapter: EchoNetworkAdapter;
|
|
89
112
|
|
|
90
113
|
private readonly _collectionSynchronizer = new CollectionSynchronizer({
|
|
@@ -97,6 +120,14 @@ export class AutomergeHost extends Resource {
|
|
|
97
120
|
private _storage!: StorageAdapterInterface & Lifecycle;
|
|
98
121
|
private readonly _headsStore: HeadsStore;
|
|
99
122
|
|
|
123
|
+
private _syncTask: DeferredTask | undefined = undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Cache of collections that would be synced on next sync task run.
|
|
126
|
+
*/
|
|
127
|
+
private readonly _collectionsToSync = new ComplexSet<{ collectionId: string; peerId: PeerId }>(
|
|
128
|
+
({ collectionId, peerId }) => `${collectionId}|${peerId}`,
|
|
129
|
+
);
|
|
130
|
+
|
|
100
131
|
@trace.info()
|
|
101
132
|
private _peerId!: PeerId;
|
|
102
133
|
|
|
@@ -110,13 +141,32 @@ export class AutomergeHost extends Resource {
|
|
|
110
141
|
*/
|
|
111
142
|
public readonly documentsSaved = new Event();
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
private readonly _headsUpdates = new Map<DocumentId, Heads>();
|
|
145
|
+
private _onHeadsChangedTask?: DeferredTask;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Documents created in this session.
|
|
149
|
+
*/
|
|
150
|
+
private _createdDocuments = new Set<DocumentId>();
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Documents that need to be synced based on the result of collection-sync.
|
|
154
|
+
*/
|
|
155
|
+
private _documentsToSync = new Set<DocumentId>();
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Documents that are not avaiale locally that should be requested.
|
|
159
|
+
*/
|
|
160
|
+
private _documentsToRequest = new Set<DocumentId>();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Documents requested by remote peers.
|
|
164
|
+
*/
|
|
165
|
+
private _documentsRequested = new Map<PeerId, Set<DocumentId>>();
|
|
166
|
+
|
|
167
|
+
private _sharePolicyChangedTask?: DeferredTask;
|
|
168
|
+
|
|
169
|
+
constructor({ db, dataMonitor, peerIdProvider, getSpaceKeyByRootDocumentId }: AutomergeHostProps) {
|
|
120
170
|
super();
|
|
121
171
|
this._db = db;
|
|
122
172
|
this._storage = new LevelDBStorageAdapter({
|
|
@@ -134,8 +184,11 @@ export class AutomergeHost extends Resource {
|
|
|
134
184
|
onCollectionStateReceived: this._onCollectionStateReceived.bind(this),
|
|
135
185
|
monitor: dataMonitor,
|
|
136
186
|
});
|
|
187
|
+
this._echoNetworkAdapter.documentRequested.on(({ peerId, documentId }) => {
|
|
188
|
+
defaultMap(this._documentsRequested, peerId, () => new Set()).add(documentId);
|
|
189
|
+
this._sharePolicyChangedTask!.schedule();
|
|
190
|
+
});
|
|
137
191
|
this._headsStore = new HeadsStore({ db: db.sublevel('heads') });
|
|
138
|
-
this._indexMetadataStore = indexMetadataStore;
|
|
139
192
|
this._peerIdProvider = peerIdProvider;
|
|
140
193
|
this._getSpaceKeyByRootDocumentId = getSpaceKeyByRootDocumentId;
|
|
141
194
|
}
|
|
@@ -143,12 +196,18 @@ export class AutomergeHost extends Resource {
|
|
|
143
196
|
protected override async _open(): Promise<void> {
|
|
144
197
|
this._peerId = `host-${this._peerIdProvider?.() ?? PublicKey.random().toHex()}` as PeerId;
|
|
145
198
|
|
|
199
|
+
this._onHeadsChangedTask = new DeferredTask(this._ctx, async () => {
|
|
200
|
+
const docHeads = Array.from(this._headsUpdates.entries());
|
|
201
|
+
this._headsUpdates.clear();
|
|
202
|
+
this._onHeadsChanged(docHeads);
|
|
203
|
+
});
|
|
204
|
+
|
|
146
205
|
await this._storage.open?.();
|
|
147
206
|
|
|
148
207
|
// Construct the automerge repo.
|
|
149
208
|
this._repo = new Repo({
|
|
150
209
|
peerId: this._peerId as PeerId,
|
|
151
|
-
|
|
210
|
+
shareConfig: this._shareConfig,
|
|
152
211
|
storage: this._storage,
|
|
153
212
|
network: [
|
|
154
213
|
// Upstream swarm.
|
|
@@ -180,6 +239,27 @@ export class AutomergeHost extends Resource {
|
|
|
180
239
|
}
|
|
181
240
|
});
|
|
182
241
|
|
|
242
|
+
this._syncTask = new DeferredTask(this._ctx, async () => {
|
|
243
|
+
const collectionToSync = Array.from(this._collectionsToSync.values());
|
|
244
|
+
if (collectionToSync.length === 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
await Promise.all(
|
|
248
|
+
collectionToSync.map(async ({ collectionId, peerId }) => {
|
|
249
|
+
try {
|
|
250
|
+
await this._handleCollectionSync(collectionId, peerId);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
log.error('failed to sync collection', { collectionId, peerId, err });
|
|
253
|
+
}
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
this._sharePolicyChangedTask = new DeferredTask(this._ctx, async () => {
|
|
259
|
+
log('share policy changed');
|
|
260
|
+
this._repo.shareConfigChanged();
|
|
261
|
+
});
|
|
262
|
+
|
|
183
263
|
await this._echoNetworkAdapter.open();
|
|
184
264
|
await this._collectionSynchronizer.open();
|
|
185
265
|
await this._echoNetworkAdapter.open();
|
|
@@ -190,14 +270,9 @@ export class AutomergeHost extends Resource {
|
|
|
190
270
|
await this._collectionSynchronizer.close();
|
|
191
271
|
await this._storage.close?.();
|
|
192
272
|
await this._echoNetworkAdapter.close();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* @deprecated To be abstracted away.
|
|
198
|
-
*/
|
|
199
|
-
get repo(): Repo {
|
|
200
|
-
return this._repo;
|
|
273
|
+
this._syncTask = undefined;
|
|
274
|
+
this._onHeadsChangedTask = undefined;
|
|
275
|
+
this._sharePolicyChangedTask = undefined;
|
|
201
276
|
}
|
|
202
277
|
|
|
203
278
|
get peerId(): PeerId {
|
|
@@ -208,11 +283,17 @@ export class AutomergeHost extends Resource {
|
|
|
208
283
|
return Object.keys(this._repo.handles).length;
|
|
209
284
|
}
|
|
210
285
|
|
|
211
|
-
|
|
286
|
+
get handles(): Readonly<Record<DocumentId, DocHandle<any>>> {
|
|
287
|
+
return this._repo.handles;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async addReplicator(replicator: AutomergeReplicator): Promise<void> {
|
|
291
|
+
invariant(this.isOpen, 'AutomergeHost is not open');
|
|
212
292
|
await this._echoNetworkAdapter.addReplicator(replicator);
|
|
213
293
|
}
|
|
214
294
|
|
|
215
|
-
async removeReplicator(replicator:
|
|
295
|
+
async removeReplicator(replicator: AutomergeReplicator): Promise<void> {
|
|
296
|
+
invariant(this.isOpen, 'AutomergeHost is not open');
|
|
216
297
|
await this._echoNetworkAdapter.removeReplicator(replicator);
|
|
217
298
|
}
|
|
218
299
|
|
|
@@ -220,13 +301,29 @@ export class AutomergeHost extends Resource {
|
|
|
220
301
|
* Loads the document handle from the repo and waits for it to be ready.
|
|
221
302
|
*/
|
|
222
303
|
async loadDoc<T>(ctx: Context, documentId: AnyDocumentId, opts?: LoadDocOptions): Promise<DocHandle<T>> {
|
|
304
|
+
invariant(this.isOpen, 'AutomergeHost is not open');
|
|
223
305
|
let handle: DocHandle<T> | undefined;
|
|
224
306
|
if (typeof documentId === 'string') {
|
|
225
307
|
// NOTE: documentId might also be a URL, in which case this lookup will fail.
|
|
226
308
|
handle = this._repo.handles[documentId as DocumentId];
|
|
227
309
|
}
|
|
228
310
|
if (!handle) {
|
|
229
|
-
|
|
311
|
+
if (!opts?.fetchFromNetwork) {
|
|
312
|
+
handle = await this._repo.find(documentId as DocumentId, FIND_PARAMS);
|
|
313
|
+
} else {
|
|
314
|
+
try {
|
|
315
|
+
handle = await this._repo.find(documentId as DocumentId, {
|
|
316
|
+
allowableStates: ['ready', 'requesting', 'unavailable'],
|
|
317
|
+
});
|
|
318
|
+
if (handle.state === 'requesting' || handle.state === 'unavailable') {
|
|
319
|
+
this._documentsToRequest.add(handle.documentId);
|
|
320
|
+
this._sharePolicyChangedTask!.schedule();
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
log.error('failed to load document', { documentId, err });
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
230
327
|
}
|
|
231
328
|
|
|
232
329
|
// `whenReady` creates a timeout so we guard it with an if to skip it if the handle is already ready.
|
|
@@ -242,6 +339,7 @@ export class AutomergeHost extends Resource {
|
|
|
242
339
|
}
|
|
243
340
|
|
|
244
341
|
async exportDoc(ctx: Context, id: AnyDocumentId): Promise<Uint8Array> {
|
|
342
|
+
invariant(this.isOpen, 'AutomergeHost is not open');
|
|
245
343
|
const documentId = interpretAsDocumentId(id);
|
|
246
344
|
|
|
247
345
|
const chunks = await this._storage.loadRange([documentId]);
|
|
@@ -251,10 +349,11 @@ export class AutomergeHost extends Resource {
|
|
|
251
349
|
/**
|
|
252
350
|
* Create new persisted document.
|
|
253
351
|
*/
|
|
254
|
-
createDoc<T>(initialValue?: T | Doc<T> | Uint8Array, opts?: CreateDocOptions): DocHandle<T
|
|
352
|
+
async createDoc<T>(initialValue?: T | Doc<T> | Uint8Array, opts?: CreateDocOptions): Promise<DocHandle<T>> {
|
|
353
|
+
invariant(this.isOpen, 'AutomergeHost is not open');
|
|
255
354
|
if (opts?.preserveHistory) {
|
|
256
355
|
if (initialValue instanceof Uint8Array) {
|
|
257
|
-
return this._repo.import(initialValue);
|
|
356
|
+
return this._repo.import(initialValue, { docId: opts?.documentId });
|
|
258
357
|
}
|
|
259
358
|
|
|
260
359
|
if (!isAutomerge(initialValue)) {
|
|
@@ -262,17 +361,27 @@ export class AutomergeHost extends Resource {
|
|
|
262
361
|
}
|
|
263
362
|
|
|
264
363
|
// TODO(dmaretskyi): There's a more efficient way.
|
|
265
|
-
|
|
364
|
+
const handle = this._repo.import(save(initialValue as Doc<T>), { docId: opts?.documentId });
|
|
365
|
+
this._createdDocuments.add(handle.documentId);
|
|
366
|
+
this._sharePolicyChangedTask!.schedule();
|
|
367
|
+
return handle as DocHandle<T>;
|
|
266
368
|
} else {
|
|
267
369
|
if (initialValue instanceof Uint8Array) {
|
|
268
370
|
throw new Error('Cannot create document from Uint8Array without preserving history');
|
|
269
371
|
}
|
|
270
372
|
|
|
271
|
-
|
|
373
|
+
if (opts?.documentId) {
|
|
374
|
+
throw new Error('Cannot prefil document id when not importing an existing doc');
|
|
375
|
+
}
|
|
376
|
+
const handle = await this._repo.create2<T>(initialValue);
|
|
377
|
+
this._createdDocuments.add(handle.documentId);
|
|
378
|
+
this._sharePolicyChangedTask!.schedule();
|
|
379
|
+
return handle;
|
|
272
380
|
}
|
|
273
381
|
}
|
|
274
382
|
|
|
275
383
|
async waitUntilHeadsReplicated(heads: DocHeadsList): Promise<void> {
|
|
384
|
+
invariant(this.isOpen, 'AutomergeHost is not open');
|
|
276
385
|
const entries = heads.entries;
|
|
277
386
|
if (!entries?.length) {
|
|
278
387
|
return;
|
|
@@ -289,7 +398,7 @@ export class AutomergeHost extends Resource {
|
|
|
289
398
|
});
|
|
290
399
|
if (headsToWait.length > 0) {
|
|
291
400
|
await Promise.all(
|
|
292
|
-
headsToWait.map(async (entry
|
|
401
|
+
headsToWait.map(async (entry) => {
|
|
293
402
|
const handle = await this.loadDoc<DatabaseDirectory>(Context.default(), entry.documentId as DocumentId);
|
|
294
403
|
await waitForHeads(handle, entry.heads!);
|
|
295
404
|
}),
|
|
@@ -303,6 +412,7 @@ export class AutomergeHost extends Resource {
|
|
|
303
412
|
}
|
|
304
413
|
|
|
305
414
|
async reIndexHeads(documentIds: DocumentId[]): Promise<void> {
|
|
415
|
+
invariant(this.isOpen, 'AutomergeHost is not open');
|
|
306
416
|
for (const documentId of documentIds) {
|
|
307
417
|
log('re-indexing heads for document', { documentId });
|
|
308
418
|
const handle = await this._repo.find(documentId, FIND_PARAMS);
|
|
@@ -319,28 +429,42 @@ export class AutomergeHost extends Resource {
|
|
|
319
429
|
log('done re-indexing heads');
|
|
320
430
|
}
|
|
321
431
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
private async _sharePolicy(peerId: PeerId, documentId?: DocumentId): Promise<boolean> {
|
|
327
|
-
if (peerId.startsWith('client-')) {
|
|
328
|
-
return false; // Only send docs to clients if they are requested.
|
|
329
|
-
}
|
|
432
|
+
private readonly _shareConfig = {
|
|
433
|
+
access: async (peerId: PeerId, documentId: DocumentId): Promise<boolean> => {
|
|
434
|
+
return true;
|
|
435
|
+
},
|
|
330
436
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
437
|
+
// TODO(dmaretskyi): Share based on HALO permissions and space affinity.
|
|
438
|
+
// Hosts, running in the worker, don't share documents unless requested by other peers.
|
|
439
|
+
// NOTE: If both peers return sharePolicy=false the replication will not happen
|
|
440
|
+
// https://github.com/automerge/automerge-repo/pull/292
|
|
441
|
+
// Called for all loaded documents so they could be advertised to the sync server.
|
|
442
|
+
announce: async (peerId: PeerId, documentId?: DocumentId): Promise<boolean> => {
|
|
443
|
+
if (!documentId) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
334
446
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
447
|
+
if (OPTIMIZED_SHARE_POLICY) {
|
|
448
|
+
if (
|
|
449
|
+
!this._createdDocuments.has(documentId) &&
|
|
450
|
+
!this._documentsToSync.has(documentId) &&
|
|
451
|
+
!this._documentsToRequest.has(documentId)
|
|
452
|
+
) {
|
|
453
|
+
// Skip advertising documents that don't need to be synced.
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
339
457
|
|
|
340
|
-
|
|
341
|
-
|
|
458
|
+
const peerMetadata = this._repo.peerMetadataByPeerId[peerId];
|
|
459
|
+
if (isEchoPeerMetadata(peerMetadata)) {
|
|
460
|
+
return this._echoNetworkAdapter.shouldAdvertise(peerId, { documentId });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return false;
|
|
464
|
+
},
|
|
465
|
+
};
|
|
342
466
|
|
|
343
|
-
private async _beforeSave({ path, batch }:
|
|
467
|
+
private async _beforeSave({ path, batch }: BeforeSaveProps): Promise<void> {
|
|
344
468
|
const handle = this._repo.handles[path[0] as DocumentId];
|
|
345
469
|
if (!handle || !handle.isReady()) {
|
|
346
470
|
return;
|
|
@@ -352,14 +476,6 @@ export class AutomergeHost extends Resource {
|
|
|
352
476
|
|
|
353
477
|
const heads = getHeads(doc);
|
|
354
478
|
this._headsStore.setHeads(handle.documentId, heads, batch);
|
|
355
|
-
|
|
356
|
-
const spaceKey = DatabaseDirectory.getSpaceKey(doc) ?? undefined;
|
|
357
|
-
const objectIds = Object.keys(doc.objects ?? {});
|
|
358
|
-
const encodedIds = objectIds.map((objectId) =>
|
|
359
|
-
objectPointerCodec.encode({ documentId: handle.documentId, objectId, spaceKey }),
|
|
360
|
-
);
|
|
361
|
-
const idToLastHash = new Map(encodedIds.map((id) => [id, heads]));
|
|
362
|
-
this._indexMetadataStore.markDirty(idToLastHash, batch);
|
|
363
479
|
}
|
|
364
480
|
|
|
365
481
|
private _shouldSyncCollection(collectionId: string, peerId: PeerId): boolean {
|
|
@@ -375,14 +491,24 @@ export class AutomergeHost extends Resource {
|
|
|
375
491
|
* Called by AutomergeStorageAdapter after levelDB batch commit.
|
|
376
492
|
*/
|
|
377
493
|
private async _afterSave(path: StorageKey): Promise<void> {
|
|
378
|
-
this.
|
|
494
|
+
if (!this.isOpen) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
379
497
|
|
|
380
498
|
const documentId = path[0] as DocumentId;
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
this._onHeadsChanged(documentId, heads);
|
|
499
|
+
const handle = this._repo.handles[documentId];
|
|
500
|
+
if (!handle || !handle.isReady()) {
|
|
501
|
+
return;
|
|
385
502
|
}
|
|
503
|
+
const document = handle.doc();
|
|
504
|
+
if (!document) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const heads = getHeads(document);
|
|
509
|
+
this._headsUpdates.set(documentId, heads);
|
|
510
|
+
invariant(this._onHeadsChangedTask, 'onHeadsChangedTask is not initialized');
|
|
511
|
+
this._onHeadsChangedTask.schedule();
|
|
386
512
|
this.documentsSaved.emit();
|
|
387
513
|
}
|
|
388
514
|
|
|
@@ -391,7 +517,7 @@ export class AutomergeHost extends Resource {
|
|
|
391
517
|
return this._repo.peers;
|
|
392
518
|
}
|
|
393
519
|
|
|
394
|
-
private async _isDocumentInRemoteCollection(params:
|
|
520
|
+
private async _isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckProps): Promise<boolean> {
|
|
395
521
|
for (const collectionId of this._collectionSynchronizer.getRegisteredCollectionIds()) {
|
|
396
522
|
const remoteCollections = this._collectionSynchronizer.getRemoteCollectionStates(collectionId);
|
|
397
523
|
const remotePeerDocs = remoteCollections.get(params.peerId as PeerId)?.documents;
|
|
@@ -433,11 +559,17 @@ export class AutomergeHost extends Resource {
|
|
|
433
559
|
async flush({ documentIds }: FlushRequest = {}): Promise<void> {
|
|
434
560
|
// Note: Sync protocol for client and services ensures that all handles should have all changes.
|
|
435
561
|
|
|
436
|
-
const loadedDocuments = documentIds
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
562
|
+
const loadedDocuments = (documentIds ?? Object.keys(this._repo.handles)).filter(
|
|
563
|
+
(documentId): documentId is DocumentId => {
|
|
564
|
+
const handle = this._repo.handles[documentId as DocumentId];
|
|
565
|
+
return handle && handle.isReady();
|
|
566
|
+
},
|
|
567
|
+
);
|
|
440
568
|
await this._repo.flush(loadedDocuments);
|
|
569
|
+
|
|
570
|
+
// Ensure that document verions have propagated accross the system.
|
|
571
|
+
// This is important for the case where we are doing flush and then waiting for sync to happen.
|
|
572
|
+
await this._onHeadsChangedTask?.runBlocking();
|
|
441
573
|
}
|
|
442
574
|
|
|
443
575
|
async getHeads(documentIds: DocumentId[]): Promise<(Heads | undefined)[]> {
|
|
@@ -463,6 +595,13 @@ export class AutomergeHost extends Resource {
|
|
|
463
595
|
return result;
|
|
464
596
|
}
|
|
465
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Iterate over all document heads stored on disk.
|
|
600
|
+
*/
|
|
601
|
+
listDocumentHeads(): AsyncGenerator<{ documentId: DocumentId; heads: Heads }> {
|
|
602
|
+
return this._headsStore.iterateAll();
|
|
603
|
+
}
|
|
604
|
+
|
|
466
605
|
//
|
|
467
606
|
// Collection sync.
|
|
468
607
|
//
|
|
@@ -479,8 +618,8 @@ export class AutomergeHost extends Resource {
|
|
|
479
618
|
this._collectionSynchronizer.refreshCollection(collectionId);
|
|
480
619
|
}
|
|
481
620
|
|
|
482
|
-
async getCollectionSyncState(collectionId: string): Promise<
|
|
483
|
-
const result:
|
|
621
|
+
async getCollectionSyncState(collectionId: string): Promise<SpaceSyncState> {
|
|
622
|
+
const result: SpaceSyncState = {
|
|
484
623
|
peers: [],
|
|
485
624
|
};
|
|
486
625
|
|
|
@@ -493,13 +632,16 @@ export class AutomergeHost extends Resource {
|
|
|
493
632
|
|
|
494
633
|
for (const [peerId, state] of remoteState) {
|
|
495
634
|
const diff = diffCollectionState(localState, state);
|
|
496
|
-
result.peers
|
|
635
|
+
result.peers!.push({
|
|
497
636
|
peerId,
|
|
498
637
|
missingOnRemote: diff.missingOnRemote.length,
|
|
499
638
|
missingOnLocal: diff.missingOnLocal.length,
|
|
500
639
|
differentDocuments: diff.different.length,
|
|
501
|
-
localDocumentCount: Object.
|
|
502
|
-
remoteDocumentCount: Object.
|
|
640
|
+
localDocumentCount: Object.entries(localState.documents).filter(([_, heads]) => heads.length > 0).length,
|
|
641
|
+
remoteDocumentCount: Object.entries(state.documents).filter(([_, heads]) => heads.length > 0).length,
|
|
642
|
+
|
|
643
|
+
totalDocumentCount: new Set([...Object.keys(localState.documents), ...Object.keys(state.documents)]).size,
|
|
644
|
+
unsyncedDocumentCount: new Set([...diff.missingOnLocal, ...diff.missingOnRemote, ...diff.different]).size,
|
|
503
645
|
});
|
|
504
646
|
}
|
|
505
647
|
|
|
@@ -515,6 +657,16 @@ export class AutomergeHost extends Resource {
|
|
|
515
657
|
heads.map((heads, index) => [documentIds[index], heads ?? []]),
|
|
516
658
|
);
|
|
517
659
|
this._collectionSynchronizer.setLocalCollectionState(collectionId, { documents });
|
|
660
|
+
|
|
661
|
+
// Proactively push our updated local state to peers that are interested in this collection.
|
|
662
|
+
// This reduces reliance on the next periodic query and prevents replication stalls in fast paths
|
|
663
|
+
// where the remote queries before our local state is ready.
|
|
664
|
+
const interestedPeers = this._echoNetworkAdapter.getPeersInterestedInCollection(collectionId);
|
|
665
|
+
if (interestedPeers.length > 0) {
|
|
666
|
+
for (const peerId of interestedPeers) {
|
|
667
|
+
this._sendCollectionState(collectionId, peerId, { documents });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
518
670
|
}
|
|
519
671
|
|
|
520
672
|
async clearLocalCollectionState(collectionId: string): Promise<void> {
|
|
@@ -546,6 +698,11 @@ export class AutomergeHost extends Resource {
|
|
|
546
698
|
}
|
|
547
699
|
|
|
548
700
|
private _onRemoteCollectionStateUpdated(collectionId: string, peerId: PeerId): void {
|
|
701
|
+
this._collectionsToSync.add({ collectionId, peerId });
|
|
702
|
+
this._syncTask?.schedule();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private async _handleCollectionSync(collectionId: string, peerId: PeerId) {
|
|
549
706
|
const localState = this._collectionSynchronizer.getLocalCollectionState(collectionId);
|
|
550
707
|
const remoteState = this._collectionSynchronizer.getRemoteCollectionStates(collectionId).get(peerId);
|
|
551
708
|
|
|
@@ -554,36 +711,184 @@ export class AutomergeHost extends Resource {
|
|
|
554
711
|
}
|
|
555
712
|
|
|
556
713
|
const { different, missingOnLocal, missingOnRemote } = diffCollectionState(localState, remoteState);
|
|
557
|
-
const toReplicate = [...missingOnLocal, ...missingOnRemote, ...different];
|
|
558
714
|
|
|
559
|
-
if (
|
|
715
|
+
if (different.length === 0 && missingOnLocal.length === 0 && missingOnRemote.length === 0) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const toReplicateWithoutBatching = [...different];
|
|
720
|
+
const bundleSyncEnabled = this._echoNetworkAdapter.bundleSyncEnabledForPeer(peerId);
|
|
721
|
+
if (bundleSyncEnabled && missingOnRemote.length >= BUNDLE_SYNC_THRESHOLD) {
|
|
722
|
+
log('pushing bundle', { amount: missingOnRemote.length });
|
|
723
|
+
const { syncInteractively } = await this._pushInBundles(peerId, missingOnRemote);
|
|
724
|
+
toReplicateWithoutBatching.push(...syncInteractively);
|
|
725
|
+
} else {
|
|
726
|
+
log.verbose('failed to push bundle, replicating interactively', {
|
|
727
|
+
collectionId,
|
|
728
|
+
peerId,
|
|
729
|
+
amount: missingOnRemote.length,
|
|
730
|
+
});
|
|
731
|
+
toReplicateWithoutBatching.push(...missingOnRemote);
|
|
732
|
+
}
|
|
733
|
+
if (bundleSyncEnabled && missingOnLocal.length >= BUNDLE_SYNC_THRESHOLD) {
|
|
734
|
+
log('pulling bundle', { amount: missingOnLocal.length });
|
|
735
|
+
const { syncInteractively } = await this._pullInBundles(peerId, missingOnLocal);
|
|
736
|
+
toReplicateWithoutBatching.push(...syncInteractively);
|
|
737
|
+
} else {
|
|
738
|
+
log.verbose('failed to pull bundle, replicating interactively', {
|
|
739
|
+
collectionId,
|
|
740
|
+
peerId,
|
|
741
|
+
amount: missingOnLocal.length,
|
|
742
|
+
});
|
|
743
|
+
toReplicateWithoutBatching.push(...missingOnLocal);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (toReplicateWithoutBatching.length === 0) {
|
|
560
747
|
return;
|
|
561
748
|
}
|
|
562
749
|
|
|
563
750
|
log('replicating documents after collection sync', {
|
|
564
751
|
collectionId,
|
|
565
752
|
peerId,
|
|
566
|
-
|
|
567
|
-
count:
|
|
753
|
+
toReplicateWithoutBatching,
|
|
754
|
+
count: toReplicateWithoutBatching.length,
|
|
568
755
|
});
|
|
569
756
|
|
|
570
757
|
// Load the documents so they will start syncing.
|
|
571
|
-
for (const documentId of
|
|
758
|
+
for (const documentId of toReplicateWithoutBatching) {
|
|
759
|
+
// Unless we track the document in "to sync" list, it will not be advertised.
|
|
760
|
+
this._documentsToSync!.add(documentId);
|
|
572
761
|
this._repo.findWithProgress(documentId);
|
|
573
762
|
}
|
|
763
|
+
this._sharePolicyChangedTask!.schedule();
|
|
574
764
|
}
|
|
575
765
|
|
|
576
|
-
|
|
766
|
+
// TODO(mykola): Add retries of batches https://gist.github.com/mykola-vrmchk/fde270259e9209fcbf1331e5abbf12cf
|
|
767
|
+
private async _pushInBundles(
|
|
768
|
+
peerId: PeerId,
|
|
769
|
+
documentIds: DocumentId[],
|
|
770
|
+
): Promise<{ syncInteractively: DocumentId[] }> {
|
|
771
|
+
const documentsToPush = [...documentIds];
|
|
772
|
+
const syncInteractively: DocumentId[] = [];
|
|
773
|
+
|
|
774
|
+
// Push bundles in parallel with BUNDLE_SYNC_CONCURRENCY max concurrent tasks.
|
|
775
|
+
while (documentsToPush.length > 0) {
|
|
776
|
+
await Promise.all(
|
|
777
|
+
range(BUNDLE_SYNC_CONCURRENCY).map(async () => {
|
|
778
|
+
const bundle = documentsToPush.splice(0, BUNDLE_SIZE);
|
|
779
|
+
if (bundle.length === 0) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
await this._pushBundle(peerId, bundle).catch((err) => {
|
|
783
|
+
log.warn('failed to push bundle, replicating interactively', { peerId, bundle, err });
|
|
784
|
+
syncInteractively.push(...bundle);
|
|
785
|
+
});
|
|
786
|
+
}),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return { syncInteractively };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private async _pushBundle(peerId: PeerId, documentIds: DocumentId[]): Promise<void> {
|
|
794
|
+
if (this._ctx.disposed) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const docs = documentIds.map((documentId) => {
|
|
799
|
+
const handle = this._repo.handles[documentId];
|
|
800
|
+
if (!handle || !handle.isReady()) {
|
|
801
|
+
log.warn('document not ready, skipping', { documentId });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const doc = handle.doc();
|
|
805
|
+
if (!doc) {
|
|
806
|
+
log.warn('document not available, skipping', { documentId });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
documentId,
|
|
811
|
+
data: save(doc),
|
|
812
|
+
heads: getHeads(doc),
|
|
813
|
+
};
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
await this._echoNetworkAdapter.pushBundle(peerId, docs.filter(isNonNullable));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private async _pullInBundles(
|
|
820
|
+
peerId: PeerId,
|
|
821
|
+
documentIds: DocumentId[],
|
|
822
|
+
): Promise<{ syncInteractively: DocumentId[] }> {
|
|
823
|
+
const documentsToPull = [...documentIds];
|
|
824
|
+
const syncInteractively: DocumentId[] = [];
|
|
825
|
+
const docsToImport: Record<DocumentId, Uint8Array> = {};
|
|
826
|
+
|
|
827
|
+
// Pull bundles in parallel with BUNDLE_SYNC_CONCURRENCY max concurrent tasks.
|
|
828
|
+
while (documentsToPull.length > 0) {
|
|
829
|
+
await Promise.all(
|
|
830
|
+
range(BUNDLE_SYNC_CONCURRENCY).map(async () => {
|
|
831
|
+
const bundle = documentsToPull.splice(0, BUNDLE_SIZE);
|
|
832
|
+
if (bundle.length === 0) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const result = await this._pullBundle(peerId, bundle).catch((err) => {
|
|
836
|
+
log.warn('failed to pull bundle, replicating interactively', { peerId, bundle, err });
|
|
837
|
+
syncInteractively.push(...bundle);
|
|
838
|
+
});
|
|
839
|
+
if (result) {
|
|
840
|
+
Object.assign(docsToImport, result.docsToImport);
|
|
841
|
+
}
|
|
842
|
+
}),
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
for (const [documentId, data] of Object.entries(docsToImport)) {
|
|
847
|
+
this._repo.import(data, { docId: documentId as DocumentId });
|
|
848
|
+
}
|
|
849
|
+
await this._repo.flush(Object.keys(docsToImport) as DocumentId[]);
|
|
850
|
+
|
|
851
|
+
return { syncInteractively };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private async _pullBundle(
|
|
855
|
+
peerId: PeerId,
|
|
856
|
+
documentIds: DocumentId[],
|
|
857
|
+
): Promise<{ docsToImport: Record<DocumentId, Uint8Array> } | undefined> {
|
|
858
|
+
if (this._ctx.disposed) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
// NOTE: We are expecting that documents that are being pulled are not present locally, so we are pulling all changes.
|
|
862
|
+
const docHeads = Object.fromEntries(documentIds.map((documentId) => [documentId, []]));
|
|
863
|
+
const bundle = await this._echoNetworkAdapter.pullBundle(peerId, docHeads);
|
|
864
|
+
return { docsToImport: bundle };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private _onHeadsChanged(docHeads: [DocumentId, Heads][]): void {
|
|
577
868
|
const collectionsChanged = new Set<CollectionId>();
|
|
869
|
+
|
|
578
870
|
for (const collectionId of this._collectionSynchronizer.getRegisteredCollectionIds()) {
|
|
579
871
|
const state = this._collectionSynchronizer.getLocalCollectionState(collectionId);
|
|
580
|
-
if (state
|
|
581
|
-
|
|
582
|
-
|
|
872
|
+
if (!state) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
let newState: CollectionState | undefined;
|
|
876
|
+
|
|
877
|
+
for (const [documentId, heads] of docHeads) {
|
|
878
|
+
if (documentId in state.documents) {
|
|
879
|
+
if (!newState) {
|
|
880
|
+
newState = structuredClone(state);
|
|
881
|
+
}
|
|
882
|
+
newState.documents[documentId] = heads;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (newState) {
|
|
583
887
|
this._collectionSynchronizer.setLocalCollectionState(collectionId, newState);
|
|
584
888
|
collectionsChanged.add(collectionId as CollectionId);
|
|
585
889
|
}
|
|
586
890
|
}
|
|
891
|
+
|
|
587
892
|
for (const collectionId of collectionsChanged) {
|
|
588
893
|
this.collectionStateUpdated.emit({ collectionId });
|
|
589
894
|
}
|
|
@@ -619,35 +924,3 @@ const decodeCollectionState = (state: unknown): CollectionState => {
|
|
|
619
924
|
const encodeCollectionState = (state: CollectionState): unknown => {
|
|
620
925
|
return state;
|
|
621
926
|
};
|
|
622
|
-
|
|
623
|
-
export type CollectionSyncState = {
|
|
624
|
-
peers: PeerSyncState[];
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
export type PeerSyncState = {
|
|
628
|
-
peerId: PeerId;
|
|
629
|
-
/**
|
|
630
|
-
* Documents that are present locally but not on the remote peer.
|
|
631
|
-
*/
|
|
632
|
-
missingOnRemote: number;
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Documents that are present on the remote peer but not locally.
|
|
636
|
-
*/
|
|
637
|
-
missingOnLocal: number;
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Documents that are present on both peers but have different heads.
|
|
641
|
-
*/
|
|
642
|
-
differentDocuments: number;
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Total number of documents locally.
|
|
646
|
-
*/
|
|
647
|
-
localDocumentCount: number;
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Total number of documents on the remote peer.
|
|
651
|
-
*/
|
|
652
|
-
remoteDocumentCount: number;
|
|
653
|
-
};
|