@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/{browser/chunk-WJJXJTNS.mjs → neutral/chunk-46DCRDA7.mjs} +44 -13
- package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
- package/dist/lib/{browser/chunk-FR6GJ4S4.mjs → neutral/chunk-LR7UFVQX.mjs} +145 -618
- package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +1 -2
- package/dist/lib/neutral/index.mjs +7332 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +49 -292
- 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 +105 -27
- 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 +22 -2
- 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 +17 -11
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +28 -16
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/errors.d.ts +35 -0
- package/dist/types/src/automerge/errors.d.ts.map +1 -0
- package/dist/types/src/automerge/handle-state.d.ts +52 -0
- package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
- package/dist/types/src/automerge/heads-store.d.ts +9 -2
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +2 -0
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +10 -4
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +8 -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/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 +12 -6
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +9 -3
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +74 -28
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +2 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
- package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
- package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +18 -10
- 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 +3 -2
- 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 +7 -6
- 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 +1 -1
- package/dist/types/src/filter/filter-match.d.ts.map +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 +2 -2
- 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 +16 -20
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +59 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +55 -12
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +10 -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 +4 -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 +9 -8
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +5 -5
- 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.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 +13 -13
- package/dist/types/src/testing/test-replicator.d.ts.map +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 +263 -0
- package/src/automerge/automerge-host.test.ts +145 -32
- package/src/automerge/automerge-host.ts +442 -163
- package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
- package/src/automerge/automerge-repo.test.ts +284 -77
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +96 -16
- package/src/automerge/collection-synchronizer.ts +125 -26
- package/src/automerge/echo-network-adapter.test.ts +2 -1
- package/src/automerge/echo-network-adapter.ts +44 -32
- package/src/automerge/echo-replicator.ts +30 -16
- package/src/automerge/errors.ts +10 -0
- package/src/automerge/handle-state.ts +62 -0
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +2 -0
- package/src/automerge/leveldb-storage-adapter.ts +70 -4
- package/src/automerge/mesh-echo-replicator-connection.ts +8 -8
- package/src/automerge/mesh-echo-replicator.ts +14 -11
- 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 +27 -10
- package/src/db-host/database-root.ts +19 -7
- package/src/db-host/documents-synchronizer.test.ts +145 -14
- package/src/db-host/documents-synchronizer.ts +158 -59
- package/src/db-host/echo-host.ts +376 -90
- 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 +108 -111
- 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 +11 -9
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +9 -9
- package/src/edge/echo-edge-replicator.ts +50 -42
- 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 +41 -25
- package/src/filter/filter-match.ts +75 -20
- package/src/metadata/metadata-store.ts +8 -5
- package/src/pipeline/pipeline-stress.test.ts +1 -3
- package/src/pipeline/pipeline.test.ts +0 -1
- package/src/pipeline/pipeline.ts +2 -3
- package/src/query/errors.ts +2 -2
- package/src/query/plan.ts +73 -9
- package/src/query/query-executor.ts +1064 -172
- package/src/query/query-planner.test.ts +854 -86
- package/src/query/query-planner.ts +444 -38
- package/src/space/admission-discovery-extension.ts +1 -1
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +2 -2
- package/src/space/control-pipeline.ts +7 -7
- package/src/space/space-manager.ts +12 -17
- package/src/space/space-protocol.browser.test.ts +13 -12
- package/src/space/space-protocol.test.ts +13 -13
- package/src/space/space-protocol.ts +16 -15
- package/src/space/space.test.ts +2 -2
- package/src/space/space.ts +9 -10
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +0 -1
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +15 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-FR6GJ4S4.mjs.map +0 -7
- package/dist/lib/browser/chunk-WJJXJTNS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -5492
- 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-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JQCCVDLT.mjs +0 -2144
- package/dist/lib/node-esm/chunk-JQCCVDLT.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs +0 -240
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -13
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -5492
- 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 -764
- 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
|
@@ -13,11 +13,13 @@ import { log } from '@dxos/log';
|
|
|
13
13
|
import { trace } from '@dxos/tracing';
|
|
14
14
|
import { defaultMap } from '@dxos/util';
|
|
15
15
|
|
|
16
|
+
import { PeerNotFoundError } from './errors';
|
|
17
|
+
|
|
16
18
|
const MIN_QUERY_INTERVAL = 5_000;
|
|
17
19
|
|
|
18
|
-
const POLL_INTERVAL =
|
|
20
|
+
const POLL_INTERVAL = 10_000;
|
|
19
21
|
|
|
20
|
-
export type
|
|
22
|
+
export type CollectionSynchronizerProps = {
|
|
21
23
|
sendCollectionState: (collectionId: string, peerId: PeerId, state: CollectionState) => void;
|
|
22
24
|
queryCollectionState: (collectionId: string, peerId: PeerId) => void;
|
|
23
25
|
shouldSyncCollection: (collectionId: string, peerId: PeerId) => boolean;
|
|
@@ -26,11 +28,11 @@ export type CollectionSynchronizerParams = {
|
|
|
26
28
|
/**
|
|
27
29
|
* Implements collection sync protocol.
|
|
28
30
|
*/
|
|
29
|
-
@trace.resource()
|
|
31
|
+
@trace.resource({ lifecycle: true })
|
|
30
32
|
export class CollectionSynchronizer extends Resource {
|
|
31
|
-
private readonly _sendCollectionState:
|
|
32
|
-
private readonly _queryCollectionState:
|
|
33
|
-
private readonly _shouldSyncCollection:
|
|
33
|
+
private readonly _sendCollectionState: CollectionSynchronizerProps['sendCollectionState'];
|
|
34
|
+
private readonly _queryCollectionState: CollectionSynchronizerProps['queryCollectionState'];
|
|
35
|
+
private readonly _shouldSyncCollection: CollectionSynchronizerProps['shouldSyncCollection'];
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* CollectionId -> State.
|
|
@@ -42,7 +44,7 @@ export class CollectionSynchronizer extends Resource {
|
|
|
42
44
|
|
|
43
45
|
public readonly remoteStateUpdated = new Event<{ collectionId: string; peerId: PeerId; newDocsAppeared: boolean }>();
|
|
44
46
|
|
|
45
|
-
constructor(params:
|
|
47
|
+
constructor(params: CollectionSynchronizerProps) {
|
|
46
48
|
super();
|
|
47
49
|
this._sendCollectionState = params.sendCollectionState;
|
|
48
50
|
this._queryCollectionState = params.queryCollectionState;
|
|
@@ -76,13 +78,29 @@ export class CollectionSynchronizer extends Resource {
|
|
|
76
78
|
this._activeCollections.add(collectionId);
|
|
77
79
|
|
|
78
80
|
log('setLocalCollectionState', { collectionId, state });
|
|
79
|
-
this._getOrCreatePerCollectionState(collectionId)
|
|
81
|
+
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
82
|
+
perCollectionState.localState = state;
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
for (const peerId of this._connectedPeers) {
|
|
85
|
+
this._diffCollectionState(collectionId, peerId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._scheduleBroadcast(collectionId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Coalesce bursts of `setLocalCollectionState` (e.g. from `_onHeadsChanged` during an
|
|
93
|
+
* import) into one broadcast microtask per collection. If another call arrives while
|
|
94
|
+
* the microtask is pending, we just flag the collection dirty — the in-flight run
|
|
95
|
+
* already reads the latest `localState`. If it arrives during the run itself, we
|
|
96
|
+
* schedule exactly one follow-up.
|
|
97
|
+
*/
|
|
98
|
+
private _scheduleBroadcast(collectionId: string): void {
|
|
99
|
+
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
100
|
+
queueMicrotask(() => {
|
|
101
|
+
this._refreshInterestedPeers(collectionId);
|
|
102
|
+
this._broadcastLocalState(collectionId);
|
|
103
|
+
this.refreshCollection(collectionId);
|
|
86
104
|
});
|
|
87
105
|
}
|
|
88
106
|
|
|
@@ -119,10 +137,14 @@ export class CollectionSynchronizer extends Resource {
|
|
|
119
137
|
* Callback when a connection to a peer is established.
|
|
120
138
|
*/
|
|
121
139
|
onConnectionOpen(peerId: PeerId): void {
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
log('onConnectionOpen', { peerId });
|
|
141
|
+
const spanId = getSpanId(peerId);
|
|
142
|
+
// Browser-timeline-only span; the derived ctx is intentionally discarded because
|
|
143
|
+
// the downstream `_queryCollectionState` hop is user-supplied callback land with
|
|
144
|
+
// no ctx plumbing.
|
|
145
|
+
void trace.spanStart({
|
|
124
146
|
id: spanId,
|
|
125
|
-
methodName:
|
|
147
|
+
methodName: SYNC_SPAN_METHOD,
|
|
126
148
|
instance: this,
|
|
127
149
|
parentCtx: this._ctx,
|
|
128
150
|
showInBrowserTimeline: true,
|
|
@@ -148,21 +170,37 @@ export class CollectionSynchronizer extends Resource {
|
|
|
148
170
|
* Callback when a connection to a peer is closed.
|
|
149
171
|
*/
|
|
150
172
|
onConnectionClosed(peerId: PeerId): void {
|
|
173
|
+
log('onConnectionClosed', { peerId });
|
|
174
|
+
|
|
151
175
|
this._connectedPeers.delete(peerId);
|
|
152
176
|
|
|
153
177
|
for (const perCollectionState of this._perCollectionStates.values()) {
|
|
154
178
|
perCollectionState.remoteStates.delete(peerId);
|
|
179
|
+
perCollectionState.interestedPeers.delete(peerId);
|
|
180
|
+
perCollectionState.lastQueried.delete(peerId);
|
|
181
|
+
perCollectionState.lastBroadcast.delete(peerId);
|
|
155
182
|
}
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
/**
|
|
159
186
|
* Callback when a peer queries the state of a collection.
|
|
187
|
+
*
|
|
188
|
+
* If we have no local state yet we silently drop the query; the peer will receive our
|
|
189
|
+
* state via {@link _broadcastLocalState} once `setLocalCollectionState` is called.
|
|
160
190
|
*/
|
|
161
191
|
onCollectionStateQueried(collectionId: string, peerId: PeerId): void {
|
|
162
192
|
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
163
193
|
|
|
164
194
|
if (perCollectionState.localState) {
|
|
165
|
-
|
|
195
|
+
try {
|
|
196
|
+
this._sendCollectionState(collectionId, peerId, perCollectionState.localState);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (PeerNotFoundError.is(error)) {
|
|
199
|
+
log('peer not found when sending collection state callback', { error });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
166
204
|
}
|
|
167
205
|
}
|
|
168
206
|
|
|
@@ -173,24 +211,48 @@ export class CollectionSynchronizer extends Resource {
|
|
|
173
211
|
log('onRemoteStateReceived', { collectionId, peerId, state });
|
|
174
212
|
validateCollectionState(state);
|
|
175
213
|
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
214
|
+
perCollectionState.remoteStates.set(peerId, state);
|
|
215
|
+
this._diffCollectionState(collectionId, peerId);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private _diffCollectionState(collectionId: string, peerId: PeerId) {
|
|
219
|
+
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
220
|
+
const remoteState = perCollectionState.remoteStates.get(peerId);
|
|
221
|
+
if (!remoteState) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
log('diffCollectionState', { collectionId, peerId });
|
|
226
|
+
const localState = perCollectionState.localState ?? { documents: {} };
|
|
227
|
+
const diff = diffCollectionState(localState, remoteState);
|
|
228
|
+
const spanId = getSpanId(peerId);
|
|
179
229
|
if (diff.different.length === 0) {
|
|
180
230
|
trace.spanEnd(spanId);
|
|
181
231
|
} else {
|
|
182
|
-
|
|
232
|
+
// Browser-timeline-only span; see note in onConnectionOpen.
|
|
233
|
+
void trace.spanStart({
|
|
183
234
|
id: spanId,
|
|
184
|
-
methodName:
|
|
235
|
+
methodName: SYNC_SPAN_METHOD,
|
|
185
236
|
instance: this,
|
|
186
237
|
parentCtx: this._ctx,
|
|
187
238
|
showInBrowserTimeline: true,
|
|
188
239
|
attributes: { peerId },
|
|
189
240
|
});
|
|
190
241
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
242
|
+
log('diff', {
|
|
243
|
+
localState: localState.documents,
|
|
244
|
+
remoteState: remoteState.documents,
|
|
245
|
+
missingOnLocal: diff.missingOnLocal,
|
|
246
|
+
missingOnRemote: diff.missingOnRemote,
|
|
247
|
+
different: diff.different,
|
|
248
|
+
});
|
|
249
|
+
if (diff.missingOnLocal.length > 0 || diff.different.length > 0 || diff.missingOnRemote.length > 0) {
|
|
250
|
+
log('emit remote state update');
|
|
251
|
+
this.remoteStateUpdated.emit({
|
|
252
|
+
peerId,
|
|
253
|
+
collectionId,
|
|
254
|
+
newDocsAppeared: diff.missingOnLocal.length > 0,
|
|
255
|
+
});
|
|
194
256
|
}
|
|
195
257
|
}
|
|
196
258
|
|
|
@@ -200,9 +262,43 @@ export class CollectionSynchronizer extends Resource {
|
|
|
200
262
|
remoteStates: new Map(),
|
|
201
263
|
interestedPeers: new Set(),
|
|
202
264
|
lastQueried: new Map(),
|
|
265
|
+
lastBroadcast: new Map(),
|
|
203
266
|
}));
|
|
204
267
|
}
|
|
205
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Push local state to interested peers whose last-known remote state differs from local
|
|
271
|
+
* (or is unknown), then pull from peers via {@link refreshCollection}.
|
|
272
|
+
*
|
|
273
|
+
* Diff-gating avoids spamming peers that are already in sync; this matters because
|
|
274
|
+
* {@link setLocalCollectionState} is called on every local heads change.
|
|
275
|
+
*/
|
|
276
|
+
private _broadcastLocalState(collectionId: string): void {
|
|
277
|
+
if (this._ctx.disposed || !this._activeCollections.has(collectionId)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const perCollectionState = this._getOrCreatePerCollectionState(collectionId);
|
|
281
|
+
const localState = perCollectionState.localState;
|
|
282
|
+
if (!localState) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const peerId of perCollectionState.interestedPeers) {
|
|
286
|
+
const lastBroadcast = perCollectionState.lastBroadcast.get(peerId) ?? 0;
|
|
287
|
+
if (Date.now() - lastBroadcast > MIN_QUERY_INTERVAL) {
|
|
288
|
+
perCollectionState.lastBroadcast.set(peerId, Date.now());
|
|
289
|
+
try {
|
|
290
|
+
this._sendCollectionState(collectionId, peerId, localState);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (PeerNotFoundError.is(error)) {
|
|
293
|
+
log('peer not found when broadcasting collection state', { error });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
206
302
|
private _refreshInterestedPeers(collectionId: string): void {
|
|
207
303
|
for (const peerId of this._connectedPeers) {
|
|
208
304
|
if (this._shouldSyncCollection(collectionId, peerId)) {
|
|
@@ -219,6 +315,7 @@ type PerCollectionState = {
|
|
|
219
315
|
remoteStates: Map<PeerId, CollectionState>;
|
|
220
316
|
interestedPeers: Set<PeerId>;
|
|
221
317
|
lastQueried: Map<PeerId, number>;
|
|
318
|
+
lastBroadcast: Map<PeerId, number>;
|
|
222
319
|
};
|
|
223
320
|
|
|
224
321
|
export type CollectionState = {
|
|
@@ -275,6 +372,8 @@ const isValidDocumentId = (documentId: DocumentId) => {
|
|
|
275
372
|
return typeof documentId === 'string' && !documentId.includes(':');
|
|
276
373
|
};
|
|
277
374
|
|
|
278
|
-
const
|
|
375
|
+
const SYNC_SPAN_METHOD = 'syncPeer';
|
|
376
|
+
|
|
377
|
+
const getSpanId = (peerId: PeerId) => {
|
|
279
378
|
return `collection-sync-${peerId}`;
|
|
280
379
|
};
|
|
@@ -6,6 +6,7 @@ import { type PeerId, cbor } from '@automerge/automerge-repo';
|
|
|
6
6
|
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
7
7
|
|
|
8
8
|
import { Trigger, sleep, waitForCondition } from '@dxos/async';
|
|
9
|
+
import { Context } from '@dxos/context';
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
10
11
|
import { PublicKey } from '@dxos/keys';
|
|
11
12
|
import { type SyncMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|
|
@@ -113,7 +114,7 @@ describe('EchoNetworkAdapter', () => {
|
|
|
113
114
|
onTestFinished(async () => {
|
|
114
115
|
await adapter.close();
|
|
115
116
|
});
|
|
116
|
-
await adapter.addReplicator(replicator);
|
|
117
|
+
await adapter.addReplicator(Context.default(), replicator);
|
|
117
118
|
return adapter;
|
|
118
119
|
};
|
|
119
120
|
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
type PeerMetadata,
|
|
12
12
|
} from '@automerge/automerge-repo';
|
|
13
13
|
|
|
14
|
-
import { Trigger, synchronized } from '@dxos/async';
|
|
15
|
-
import { LifecycleState } from '@dxos/context';
|
|
14
|
+
import { Event, Trigger, synchronized } from '@dxos/async';
|
|
15
|
+
import { type Context, LifecycleState } from '@dxos/context';
|
|
16
16
|
import { invariant } from '@dxos/invariant';
|
|
17
17
|
import { type PublicKey } from '@dxos/keys';
|
|
18
18
|
import { log } from '@dxos/log';
|
|
@@ -20,14 +20,14 @@ import type { AutomergeProtocolMessage } from '@dxos/protocols';
|
|
|
20
20
|
import { isNonNullable } from '@dxos/util';
|
|
21
21
|
|
|
22
22
|
import { createIdFromSpaceKey } from '../common/space-id';
|
|
23
|
-
|
|
24
23
|
import {
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
type
|
|
29
|
-
type
|
|
24
|
+
type AutomergeReplicator,
|
|
25
|
+
type AutomergeReplicatorConnection,
|
|
26
|
+
type RemoteDocumentExistenceCheckProps,
|
|
27
|
+
type ShouldAdvertiseProps,
|
|
28
|
+
type ShouldSyncCollectionProps,
|
|
30
29
|
} from './echo-replicator';
|
|
30
|
+
import { PeerNotFoundError } from './errors';
|
|
31
31
|
import {
|
|
32
32
|
type CollectionQueryMessage,
|
|
33
33
|
type CollectionStateMessage,
|
|
@@ -43,9 +43,9 @@ export interface NetworkDataMonitor {
|
|
|
43
43
|
recordMessageSendingFailed(message: Message): void;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export type
|
|
46
|
+
export type EchoNetworkAdapterProps = {
|
|
47
47
|
getContainingSpaceForDocument: (documentId: string) => Promise<PublicKey | null>;
|
|
48
|
-
isDocumentInRemoteCollection: (params:
|
|
48
|
+
isDocumentInRemoteCollection: (params: RemoteDocumentExistenceCheckProps) => Promise<boolean>;
|
|
49
49
|
onCollectionStateQueried: (collectionId: string, peerId: PeerId) => void;
|
|
50
50
|
onCollectionStateReceived: (collectionId: string, peerId: PeerId, state: unknown) => void;
|
|
51
51
|
monitor?: NetworkDataMonitor;
|
|
@@ -53,16 +53,17 @@ export type EchoNetworkAdapterParams = {
|
|
|
53
53
|
|
|
54
54
|
type ConnectionEntry = {
|
|
55
55
|
isOpen: boolean;
|
|
56
|
-
connection:
|
|
56
|
+
connection: AutomergeReplicatorConnection;
|
|
57
57
|
reader: ReadableStreamDefaultReader<AutomergeProtocolMessage>;
|
|
58
58
|
writer: WritableStreamDefaultWriter<AutomergeProtocolMessage>;
|
|
59
|
+
requestedDocuments: Set<DocumentId>;
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
|
-
* Manages a set of {@link
|
|
63
|
+
* Manages a set of {@link AutomergeReplicator} instances.
|
|
63
64
|
*/
|
|
64
65
|
export class EchoNetworkAdapter extends NetworkAdapter {
|
|
65
|
-
private readonly _replicators = new Set<
|
|
66
|
+
private readonly _replicators = new Set<AutomergeReplicator>();
|
|
66
67
|
/**
|
|
67
68
|
* Remote peer id -> connection.
|
|
68
69
|
*/
|
|
@@ -71,7 +72,9 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
71
72
|
private readonly _connected = new Trigger();
|
|
72
73
|
private readonly _ready = new Trigger();
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
public readonly documentRequested = new Event<{ documentId: DocumentId; peerId: PeerId }>();
|
|
76
|
+
|
|
77
|
+
constructor(private readonly _params: EchoNetworkAdapterProps) {
|
|
75
78
|
super();
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -133,13 +136,13 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
@synchronized
|
|
136
|
-
async addReplicator(replicator:
|
|
139
|
+
async addReplicator(ctx: Context, replicator: AutomergeReplicator): Promise<void> {
|
|
137
140
|
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
138
141
|
invariant(this.peerId);
|
|
139
142
|
invariant(!this._replicators.has(replicator));
|
|
140
143
|
|
|
141
144
|
this._replicators.add(replicator);
|
|
142
|
-
await replicator.connect({
|
|
145
|
+
await replicator.connect(ctx, {
|
|
143
146
|
peerId: this.peerId,
|
|
144
147
|
onConnectionOpen: this._onConnectionOpen.bind(this),
|
|
145
148
|
onConnectionClosed: this._onConnectionClosed.bind(this),
|
|
@@ -154,14 +157,14 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
@synchronized
|
|
157
|
-
async removeReplicator(replicator:
|
|
160
|
+
async removeReplicator(replicator: AutomergeReplicator): Promise<void> {
|
|
158
161
|
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
159
162
|
invariant(this._replicators.has(replicator));
|
|
160
163
|
await replicator.disconnect();
|
|
161
164
|
this._replicators.delete(replicator);
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
async shouldAdvertise(peerId: PeerId, params:
|
|
167
|
+
async shouldAdvertise(peerId: PeerId, params: ShouldAdvertiseProps): Promise<boolean> {
|
|
165
168
|
const connection = this._connections.get(peerId);
|
|
166
169
|
if (!connection) {
|
|
167
170
|
return false;
|
|
@@ -170,7 +173,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
170
173
|
return connection.connection.shouldAdvertise(params);
|
|
171
174
|
}
|
|
172
175
|
|
|
173
|
-
shouldSyncCollection(peerId: PeerId, params:
|
|
176
|
+
shouldSyncCollection(peerId: PeerId, params: ShouldSyncCollectionProps): boolean {
|
|
174
177
|
const connection = this._connections.get(peerId);
|
|
175
178
|
if (!connection) {
|
|
176
179
|
return false;
|
|
@@ -219,26 +222,26 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
219
222
|
return connection.connection.bundleSyncEnabled;
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
async pushBundle(peerId: PeerId, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
|
|
225
|
+
async pushBundle(ctx: Context, peerId: PeerId, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]) {
|
|
223
226
|
const connection = this._connections.get(peerId);
|
|
224
227
|
if (!connection) {
|
|
225
|
-
throw new
|
|
228
|
+
throw new PeerNotFoundError({ context: { peerId } });
|
|
226
229
|
}
|
|
227
|
-
return connection.connection.pushBundle!(bundle);
|
|
230
|
+
return connection.connection.pushBundle!(ctx, bundle);
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
async pullBundle(peerId: PeerId, docHeads: Record<DocumentId, Heads>) {
|
|
233
|
+
async pullBundle(ctx: Context, peerId: PeerId, docHeads: Record<DocumentId, Heads>) {
|
|
231
234
|
const connection = this._connections.get(peerId);
|
|
232
235
|
if (!connection) {
|
|
233
|
-
throw new
|
|
236
|
+
throw new PeerNotFoundError({ context: { peerId } });
|
|
234
237
|
}
|
|
235
|
-
return connection.connection.pullBundle!(docHeads);
|
|
238
|
+
return connection.connection.pullBundle!(ctx, docHeads);
|
|
236
239
|
}
|
|
237
240
|
|
|
238
241
|
private _send(message: Message): void {
|
|
239
242
|
const connectionEntry = this._connections.get(message.targetId);
|
|
240
243
|
if (!connectionEntry) {
|
|
241
|
-
throw new
|
|
244
|
+
throw new PeerNotFoundError({ context: { peerId: message.targetId } });
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
// TODO(dmaretskyi): Find a way to enforce backpressure on AM-repo.
|
|
@@ -257,7 +260,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
257
260
|
});
|
|
258
261
|
}
|
|
259
262
|
|
|
260
|
-
private _onConnectionOpen(connection:
|
|
263
|
+
private _onConnectionOpen(connection: AutomergeReplicatorConnection): void {
|
|
261
264
|
log('connection opened', { peerId: connection.peerId });
|
|
262
265
|
invariant(!this._connections.has(connection.peerId as PeerId));
|
|
263
266
|
const connectionEntry: ConnectionEntry = {
|
|
@@ -265,6 +268,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
265
268
|
connection,
|
|
266
269
|
reader: connection.readable.getReader(),
|
|
267
270
|
writer: connection.writable.getWriter(),
|
|
271
|
+
requestedDocuments: new Set(),
|
|
268
272
|
};
|
|
269
273
|
|
|
270
274
|
this._connections.set(connection.peerId as PeerId, connectionEntry);
|
|
@@ -279,7 +283,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
279
283
|
break;
|
|
280
284
|
}
|
|
281
285
|
|
|
282
|
-
this._onMessage(value as Message);
|
|
286
|
+
this._onMessage(connectionEntry, value as Message);
|
|
283
287
|
}
|
|
284
288
|
} catch (err) {
|
|
285
289
|
if (connectionEntry.isOpen) {
|
|
@@ -293,7 +297,15 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
293
297
|
this._params.monitor?.recordPeerConnected(connection.peerId);
|
|
294
298
|
}
|
|
295
299
|
|
|
296
|
-
private _onMessage(message: Message): void {
|
|
300
|
+
private _onMessage(connectionEntry: ConnectionEntry, message: Message): void {
|
|
301
|
+
const amMessage = message as AutomergeProtocolMessage;
|
|
302
|
+
if (amMessage.type === 'request') {
|
|
303
|
+
this.documentRequested.emit({
|
|
304
|
+
documentId: amMessage.documentId as DocumentId,
|
|
305
|
+
peerId: connectionEntry.connection.peerId as PeerId,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
297
309
|
if (isCollectionQueryMessage(message)) {
|
|
298
310
|
this._params.onCollectionStateQueried(message.collectionId, message.senderId);
|
|
299
311
|
} else if (isCollectionStateMessage(message)) {
|
|
@@ -304,7 +316,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
304
316
|
this._params.monitor?.recordMessageReceived(message);
|
|
305
317
|
}
|
|
306
318
|
|
|
307
|
-
private _onConnectionClosed(connection:
|
|
319
|
+
private _onConnectionClosed(connection: AutomergeReplicatorConnection): void {
|
|
308
320
|
log('connection closed', { peerId: connection.peerId });
|
|
309
321
|
const entry = this._connections.get(connection.peerId as PeerId);
|
|
310
322
|
invariant(entry);
|
|
@@ -323,7 +335,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
323
335
|
* Trigger doc-synchronizer shared documents set recalculation. Happens on peer-candidate.
|
|
324
336
|
* TODO(y): replace with a proper API call when sharePolicy update becomes supported by automerge-repo
|
|
325
337
|
*/
|
|
326
|
-
private _onConnectionAuthScopeChanged(connection:
|
|
338
|
+
private _onConnectionAuthScopeChanged(connection: AutomergeReplicatorConnection): void {
|
|
327
339
|
log('Connection auth scope changed', { peerId: connection.peerId });
|
|
328
340
|
const entry = this._connections.get(connection.peerId as PeerId);
|
|
329
341
|
invariant(entry);
|
|
@@ -331,7 +343,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
331
343
|
this._emitPeerCandidate(connection);
|
|
332
344
|
}
|
|
333
345
|
|
|
334
|
-
private _emitPeerCandidate(connection:
|
|
346
|
+
private _emitPeerCandidate(connection: AutomergeReplicatorConnection): void {
|
|
335
347
|
this.emit('peer-candidate', {
|
|
336
348
|
peerId: connection.peerId as PeerId,
|
|
337
349
|
peerMetadata: createEchoPeerMetadata(),
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
import { type Heads } from '@automerge/automerge';
|
|
6
6
|
import { type DocumentId } from '@automerge/automerge-repo';
|
|
7
7
|
|
|
8
|
+
import { type Context } from '@dxos/context';
|
|
8
9
|
import { type PublicKey, type SpaceId } from '@dxos/keys';
|
|
9
10
|
import { type AutomergeProtocolMessage } from '@dxos/protocols';
|
|
10
11
|
|
|
11
12
|
// TODO(burdon): Rename AutomergeReplicator?
|
|
12
|
-
export interface
|
|
13
|
+
export interface AutomergeReplicator {
|
|
13
14
|
/**
|
|
14
15
|
* Called on when replicator is added to EchoHost.
|
|
15
16
|
*/
|
|
16
|
-
connect(context:
|
|
17
|
+
connect(ctx: Context, context: AutomergeReplicatorContext): Promise<void>;
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Called on when replicator is removed from EchoHost.
|
|
@@ -21,7 +22,16 @@ export interface EchoReplicator {
|
|
|
21
22
|
disconnect(): Promise<void>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Replicator with explicit per-space wiring (used by edge replicators that maintain one logical
|
|
27
|
+
* connection per `SpaceId`). `DataSpaceManager` drives these lifecycle hooks on space open/close.
|
|
28
|
+
*/
|
|
29
|
+
export interface EdgeAutomergeReplicator extends AutomergeReplicator {
|
|
30
|
+
connectToSpace(ctx: Context, spaceId: SpaceId): Promise<void>;
|
|
31
|
+
disconnectFromSpace(spaceId: SpaceId): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AutomergeReplicatorContext {
|
|
25
35
|
/**
|
|
26
36
|
* Our own peer id.
|
|
27
37
|
*/
|
|
@@ -32,14 +42,18 @@ export interface EchoReplicatorContext {
|
|
|
32
42
|
*/
|
|
33
43
|
getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
|
|
34
44
|
getContainingSpaceIdForDocument(documentId: string): Promise<SpaceId | null>;
|
|
35
|
-
isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckParams): Promise<boolean>;
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Returns false if collection sync hasn't happened yet.
|
|
48
|
+
*/
|
|
49
|
+
isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckProps): Promise<boolean>;
|
|
50
|
+
|
|
51
|
+
onConnectionOpen(connection: AutomergeReplicatorConnection): void;
|
|
52
|
+
onConnectionClosed(connection: AutomergeReplicatorConnection): void;
|
|
53
|
+
onConnectionAuthScopeChanged(connection: AutomergeReplicatorConnection): void;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
export interface
|
|
56
|
+
export interface AutomergeReplicatorConnection {
|
|
43
57
|
/**
|
|
44
58
|
* Remote peer id.
|
|
45
59
|
*/
|
|
@@ -59,15 +73,15 @@ export interface ReplicatorConnection {
|
|
|
59
73
|
* @returns true if the document should be advertised to this peer.
|
|
60
74
|
* The remote peer can still request the document by its id bypassing this check.
|
|
61
75
|
*/
|
|
62
|
-
shouldAdvertise(params:
|
|
76
|
+
shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean>;
|
|
63
77
|
|
|
64
78
|
/**
|
|
65
79
|
* @returns true if the collection should be synced to this peer.
|
|
66
80
|
*/
|
|
67
|
-
shouldSyncCollection(params:
|
|
81
|
+
shouldSyncCollection(params: ShouldSyncCollectionProps): boolean;
|
|
68
82
|
|
|
69
83
|
/**
|
|
70
|
-
* Batch syncing considered enabled if
|
|
84
|
+
* Batch syncing considered enabled if AutomergeReplicatorConnection implements `pushBatch` and `pullBatch` methods.
|
|
71
85
|
* @returns true if the batch syncing is enabled.
|
|
72
86
|
*/
|
|
73
87
|
get bundleSyncEnabled(): boolean;
|
|
@@ -75,24 +89,24 @@ export interface ReplicatorConnection {
|
|
|
75
89
|
/**
|
|
76
90
|
* Pushes the batch of documents to the remote peer.
|
|
77
91
|
*/
|
|
78
|
-
pushBundle?(bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]): Promise<void>;
|
|
92
|
+
pushBundle?(ctx: Context, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]): Promise<void>;
|
|
79
93
|
|
|
80
94
|
/**
|
|
81
95
|
* Pulls the batch of documents from the remote peer.
|
|
82
96
|
*/
|
|
83
97
|
// TODO(mykola): Use automerge-repo-bundles Bundle type here.
|
|
84
|
-
pullBundle?(docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>>;
|
|
98
|
+
pullBundle?(ctx: Context, docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>>;
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
export type
|
|
101
|
+
export type ShouldAdvertiseProps = {
|
|
88
102
|
documentId: string;
|
|
89
103
|
};
|
|
90
104
|
|
|
91
|
-
export type
|
|
105
|
+
export type ShouldSyncCollectionProps = {
|
|
92
106
|
collectionId: string;
|
|
93
107
|
};
|
|
94
108
|
|
|
95
|
-
export type
|
|
109
|
+
export type RemoteDocumentExistenceCheckProps = {
|
|
96
110
|
peerId: string;
|
|
97
111
|
documentId: string;
|
|
98
112
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { BaseError } from '@dxos/errors';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when attempting to send a message to a peer that is not connected.
|
|
9
|
+
*/
|
|
10
|
+
export class PeerNotFoundError extends BaseError.extend('PeerNotFoundError', 'Peer not found') {}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type DocumentId, type QueryState, type Repo } from '@automerge/automerge-repo';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Variants reported by {@link QueryState}: `'loading' | 'ready' | 'unavailable' | 'failed'`.
|
|
9
|
+
*/
|
|
10
|
+
export type HandleQueryState = QueryState<unknown>['state'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the {@link QueryState} variant for a document already known to the
|
|
14
|
+
* repo (i.e. one for which a handle has been registered via `Repo.find` /
|
|
15
|
+
* `Repo.findWithProgress` / `Repo.create`), or `undefined` if no handle/query
|
|
16
|
+
* exists for this document yet.
|
|
17
|
+
*
|
|
18
|
+
* Use this anywhere you need to ask "is this document loaded?" / "did sync
|
|
19
|
+
* give up?" — read the result against the {@link HandleQueryState} variants
|
|
20
|
+
* (e.g. `=== 'ready'`).
|
|
21
|
+
*
|
|
22
|
+
* # Why we can't just call `DocHandle.isReady()` / read `DocHandle.state`
|
|
23
|
+
*
|
|
24
|
+
* The current subduction fork of `@automerge/automerge-repo`
|
|
25
|
+
* (`2.6.0-subduction.10`) **gutted** the per-handle state machine. Inside the
|
|
26
|
+
* fork, every `DocHandle` state predicate is a constant placeholder:
|
|
27
|
+
*
|
|
28
|
+
* ```
|
|
29
|
+
* isReady = () => true
|
|
30
|
+
* isUnloaded = () => false
|
|
31
|
+
* isDeleted = () => false
|
|
32
|
+
* isUnavailable = () => false
|
|
33
|
+
* inState = (states) => states.includes('ready')
|
|
34
|
+
* get state = () => 'ready'
|
|
35
|
+
* whenReady = async () => {} // resolves immediately, ignores argument
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* That means `handle.isReady()` always lies "yes", `handle.state` always reads
|
|
39
|
+
* `'ready'`, and `await handle.whenReady()` never blocks — even for handles
|
|
40
|
+
* whose document has never been loaded from storage or synced from a peer.
|
|
41
|
+
* Reading `handle.doc()` on such a "ready" handle returns the empty initial
|
|
42
|
+
* doc (`A.init()` or `A.emptyChange(A.init())`) rather than throwing or
|
|
43
|
+
* returning undefined, which can silently corrupt downstream logic that
|
|
44
|
+
* trusted the handle's `isReady()` signal.
|
|
45
|
+
*
|
|
46
|
+
* Real liveness moved to the {@link DocumentQuery} returned by
|
|
47
|
+
* `Repo.findWithProgress`, whose state goes through
|
|
48
|
+
* `'loading' → 'ready' | 'unavailable' | 'failed'`. Production code that
|
|
49
|
+
* needs to know whether a document is actually loaded must consult the query
|
|
50
|
+
* state through this helper instead of any `DocHandle.*` predicate.
|
|
51
|
+
*
|
|
52
|
+
* TODO(mykola): Remove this helper once the fork either (a) restores the
|
|
53
|
+
* `DocHandle` state machine, or (b) every caller is migrated to query
|
|
54
|
+
* `DocumentProgress` directly with a richer API surface (await on state
|
|
55
|
+
* transitions, etc.).
|
|
56
|
+
*/
|
|
57
|
+
export const getHandleState = (repo: Repo, documentId: DocumentId): HandleQueryState | undefined => {
|
|
58
|
+
if (!repo.handles[documentId]) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return repo.findWithProgress(documentId).peek().state;
|
|
62
|
+
};
|