@dxos/echo-pipeline 0.6.5 → 0.6.6-staging.23d123d

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.
Files changed (45) hide show
  1. package/dist/lib/browser/{chunk-2MII6KJX.mjs → chunk-P6XSIJKM.mjs} +2184 -2132
  2. package/dist/lib/browser/chunk-P6XSIJKM.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +8 -8
  6. package/dist/lib/browser/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/{chunk-6MWU4MHX.cjs → chunk-IYTGTZ7D.cjs} +2177 -2125
  8. package/dist/lib/node/chunk-IYTGTZ7D.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +35 -35
  10. package/dist/lib/node/index.cjs.map +1 -1
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +18 -18
  13. package/dist/lib/node/testing/index.cjs.map +3 -3
  14. package/dist/types/src/automerge/automerge-host.d.ts +1 -0
  15. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  16. package/dist/types/src/automerge/echo-network-adapter.d.ts +2 -1
  17. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  18. package/dist/types/src/automerge/echo-replicator.d.ts +10 -1
  19. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  20. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  21. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  22. package/dist/types/src/automerge/network-protocol.d.ts +3 -28
  23. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  24. package/dist/types/src/space/space-manager.d.ts +3 -1
  25. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  26. package/dist/types/src/space/space-protocol.d.ts +10 -3
  27. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  28. package/dist/types/src/space/space.d.ts.map +1 -1
  29. package/dist/types/src/testing/test-network-adapter.d.ts +2 -1
  30. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  31. package/package.json +33 -33
  32. package/src/automerge/automerge-host.ts +13 -1
  33. package/src/automerge/automerge-repo.test.ts +380 -366
  34. package/src/automerge/echo-network-adapter.test.ts +1 -0
  35. package/src/automerge/echo-network-adapter.ts +8 -0
  36. package/src/automerge/echo-replicator.ts +11 -1
  37. package/src/automerge/mesh-echo-replicator-connection.ts +18 -0
  38. package/src/automerge/mesh-echo-replicator.ts +10 -2
  39. package/src/automerge/network-protocol.ts +8 -34
  40. package/src/space/space-manager.ts +15 -2
  41. package/src/space/space-protocol.ts +42 -5
  42. package/src/space/space.ts +4 -4
  43. package/src/testing/test-network-adapter.ts +5 -3
  44. package/dist/lib/browser/chunk-2MII6KJX.mjs.map +0 -7
  45. package/dist/lib/node/chunk-6MWU4MHX.cjs.map +0 -7
@@ -105,6 +105,7 @@ describe('EchoNetworkAdapter', () => {
105
105
  const createConnectedAdapter = async (replicator: MeshEchoReplicator) => {
106
106
  const adapter = new EchoNetworkAdapter({
107
107
  getContainingSpaceForDocument: async () => null,
108
+ isDocumentInRemoteCollection: async () => true,
108
109
  onCollectionStateQueried: () => {},
109
110
  onCollectionStateReceived: () => {},
110
111
  });
@@ -12,6 +12,7 @@ import { nonNullable } from '@dxos/util';
12
12
 
13
13
  import {
14
14
  type EchoReplicator,
15
+ type RemoteDocumentExistenceCheckParams,
15
16
  type ReplicatorConnection,
16
17
  type ShouldAdvertiseParams,
17
18
  type ShouldSyncCollectionParams,
@@ -22,6 +23,7 @@ import {
22
23
  type CollectionQueryMessage,
23
24
  type CollectionStateMessage,
24
25
  } from './network-protocol';
26
+ import { createIdFromSpaceKey } from '../space';
25
27
 
26
28
  export interface NetworkDataMonitor {
27
29
  recordPeerConnected(peerId: string): void;
@@ -33,6 +35,7 @@ export interface NetworkDataMonitor {
33
35
 
34
36
  export type EchoNetworkAdapterParams = {
35
37
  getContainingSpaceForDocument: (documentId: string) => Promise<PublicKey | null>;
38
+ isDocumentInRemoteCollection: (params: RemoteDocumentExistenceCheckParams) => Promise<boolean>;
36
39
  onCollectionStateQueried: (collectionId: string, peerId: PeerId) => void;
37
40
  onCollectionStateReceived: (collectionId: string, peerId: PeerId, state: unknown) => void;
38
41
  monitor?: NetworkDataMonitor;
@@ -111,7 +114,12 @@ export class EchoNetworkAdapter extends NetworkAdapter {
111
114
  onConnectionOpen: this._onConnectionOpen.bind(this),
112
115
  onConnectionClosed: this._onConnectionClosed.bind(this),
113
116
  onConnectionAuthScopeChanged: this._onConnectionAuthScopeChanged.bind(this),
117
+ isDocumentInRemoteCollection: this._params.isDocumentInRemoteCollection,
114
118
  getContainingSpaceForDocument: this._params.getContainingSpaceForDocument,
119
+ getContainingSpaceIdForDocument: async (documentId) => {
120
+ const key = await this._params.getContainingSpaceForDocument(documentId);
121
+ return key ? createIdFromSpaceKey(key) : null;
122
+ },
115
123
  });
116
124
  }
117
125
 
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Message } from '@dxos/automerge/automerge-repo';
6
- import { type PublicKey } from '@dxos/keys';
6
+ import { type PublicKey, type SpaceId } from '@dxos/keys';
7
7
 
8
8
  export interface EchoReplicator {
9
9
  /**
@@ -23,7 +23,12 @@ export interface EchoReplicatorContext {
23
23
  */
24
24
  get peerId(): string;
25
25
 
26
+ /**
27
+ * @deprecated Use `getContainingSpaceIdForDocument`.
28
+ */
26
29
  getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
30
+ getContainingSpaceIdForDocument(documentId: string): Promise<SpaceId | null>;
31
+ isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckParams): Promise<boolean>;
27
32
 
28
33
  onConnectionOpen(connection: ReplicatorConnection): void;
29
34
  onConnectionClosed(connection: ReplicatorConnection): void;
@@ -65,3 +70,8 @@ export type ShouldAdvertiseParams = {
65
70
  export type ShouldSyncCollectionParams = {
66
71
  collectionId: string;
67
72
  };
73
+
74
+ export type RemoteDocumentExistenceCheckParams = {
75
+ peerId: string;
76
+ documentId: string;
77
+ };
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import * as A from '@dxos/automerge/automerge';
5
6
  import { cbor, type Message } from '@dxos/automerge/automerge-repo';
6
7
  import { Resource } from '@dxos/context';
7
8
  import { invariant } from '@dxos/invariant';
@@ -47,6 +48,7 @@ export class MeshReplicatorConnection extends Resource implements ReplicatorConn
47
48
  write: async (message: Message, controller) => {
48
49
  invariant(this._isEnabled, 'Writing to a disabled connection');
49
50
  try {
51
+ logSendSync(message);
50
52
  await this.replicatorExtension.sendSyncMessage({ payload: cbor.encode(message) });
51
53
  } catch (err) {
52
54
  controller.error(err);
@@ -133,3 +135,19 @@ export class MeshReplicatorConnection extends Resource implements ReplicatorConn
133
135
  this._isEnabled = false;
134
136
  }
135
137
  }
138
+
139
+ const logSendSync = (message: Message) => {
140
+ log('sendSyncMessage', () => {
141
+ const decodedSyncMessage = message.type === 'sync' && message.data ? A.decodeSyncMessage(message.data) : undefined;
142
+ return {
143
+ sync: decodedSyncMessage && {
144
+ headsLength: decodedSyncMessage.heads.length,
145
+ requesting: decodedSyncMessage.need.length > 0,
146
+ sendingChanges: decodedSyncMessage.changes.length > 0,
147
+ },
148
+ type: message.type,
149
+ from: message.senderId,
150
+ to: message.targetId,
151
+ };
152
+ });
153
+ };
@@ -83,11 +83,19 @@ export class MeshEchoReplicator implements EchoReplicator {
83
83
  try {
84
84
  const spaceKey = await this._context.getContainingSpaceForDocument(params.documentId);
85
85
  if (!spaceKey) {
86
- log('space key not found for share policy check', {
86
+ const remoteDocumentExists = await this._context.isDocumentInRemoteCollection({
87
+ documentId: params.documentId,
88
+ peerId: connection.peerId,
89
+ });
90
+ log('document not found locally for share policy check, accepting the remote document', {
87
91
  peerId: connection.peerId,
88
92
  documentId: params.documentId,
93
+ remoteDocumentExists,
89
94
  });
90
- return false;
95
+ // If a document is not present locally return true if another peer claims to have it.
96
+ // Simply returning true will add the peer to "generous peers list" for this document which will
97
+ // start replication of the document after we receive, even if the peer is not in the corresponding space.
98
+ return remoteDocumentExists;
91
99
  }
92
100
 
93
101
  const spaceId = await createIdFromSpaceKey(spaceKey);
@@ -2,44 +2,18 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import type { Message, PeerId } from '@dxos/automerge/automerge-repo';
5
+ import type { Message } from '@dxos/automerge/automerge-repo';
6
+ import {
7
+ type CollectionQueryMessage,
8
+ type CollectionStateMessage,
9
+ MESSAGE_TYPE_COLLECTION_QUERY,
10
+ MESSAGE_TYPE_COLLECTION_STATE,
11
+ } from '@dxos/protocols';
6
12
 
7
- export const MESSAGE_TYPE_COLLECTION_QUERY = 'collection-query';
8
-
9
- export type CollectionQueryMessage = {
10
- type: typeof MESSAGE_TYPE_COLLECTION_QUERY;
11
- senderId: PeerId;
12
- targetId: PeerId;
13
- collectionId: string;
14
-
15
- /**
16
- * Identifier of the current state.
17
- * Remote peer will skip sending the state if it has the same tag.
18
- */
19
- stateTag?: string;
20
- };
13
+ export { type CollectionStateMessage, type CollectionQueryMessage };
21
14
 
22
15
  export const isCollectionQueryMessage = (message: Message): message is CollectionQueryMessage =>
23
16
  message.type === MESSAGE_TYPE_COLLECTION_QUERY;
24
17
 
25
- export const MESSAGE_TYPE_COLLECTION_STATE = 'collection-state';
26
-
27
- export type CollectionStateMessage = {
28
- type: typeof MESSAGE_TYPE_COLLECTION_STATE;
29
- senderId: PeerId;
30
- targetId: PeerId;
31
- collectionId: string;
32
-
33
- /**
34
- * State representation is implementation-defined.
35
- */
36
- state?: unknown;
37
-
38
- /**
39
- * Identifier of the current state.
40
- */
41
- stateTag?: string;
42
- };
43
-
44
18
  export const isCollectionStateMessage = (message: Message): message is CollectionStateMessage =>
45
19
  message.type === MESSAGE_TYPE_COLLECTION_STATE;
@@ -34,6 +34,8 @@ export type SpaceManagerParams = {
34
34
  snapshotStore: SnapshotStore;
35
35
 
36
36
  blobStore: BlobStore;
37
+
38
+ disableP2pReplication?: boolean;
37
39
  };
38
40
 
39
41
  export type ConstructSpaceParams = {
@@ -68,14 +70,23 @@ export class SpaceManager {
68
70
  private readonly _snapshotStore: SnapshotStore;
69
71
  private readonly _blobStore: BlobStore;
70
72
  private readonly _instanceId = PublicKey.random().toHex();
71
-
72
- constructor({ feedStore, networkManager, metadataStore, snapshotStore, blobStore }: SpaceManagerParams) {
73
+ private readonly _disableP2pReplication: boolean;
74
+
75
+ constructor({
76
+ feedStore,
77
+ networkManager,
78
+ metadataStore,
79
+ snapshotStore,
80
+ blobStore,
81
+ disableP2pReplication,
82
+ }: SpaceManagerParams) {
73
83
  // TODO(burdon): Assert.
74
84
  this._feedStore = feedStore;
75
85
  this._networkManager = networkManager;
76
86
  this._metadataStore = metadataStore;
77
87
  this._snapshotStore = snapshotStore;
78
88
  this._blobStore = blobStore;
89
+ this._disableP2pReplication = disableP2pReplication ?? false;
79
90
  }
80
91
 
81
92
  // TODO(burdon): Remove.
@@ -115,6 +126,7 @@ export class SpaceManager {
115
126
  onSessionAuth: onAuthorizedConnection,
116
127
  onAuthFailure,
117
128
  blobStore: this._blobStore,
129
+ disableP2pReplication: this._disableP2pReplication,
118
130
  });
119
131
  const snapshotManager = new SnapshotManager(this._snapshotStore, this._blobStore, protocol.blobSync);
120
132
 
@@ -157,6 +169,7 @@ export class SpaceManager {
157
169
  },
158
170
  onAuthFailure: (session: Teleport) => session.close(),
159
171
  blobStore: this._blobStore,
172
+ disableP2pReplication: this._disableP2pReplication,
160
173
  });
161
174
 
162
175
  try {
@@ -20,7 +20,7 @@ import { type MuxerStats, Teleport } from '@dxos/teleport';
20
20
  import { type BlobStore, BlobSync } from '@dxos/teleport-extension-object-sync';
21
21
  import { ReplicatorExtension } from '@dxos/teleport-extension-replicator';
22
22
  import { trace } from '@dxos/tracing';
23
- import { ComplexMap } from '@dxos/util';
23
+ import { CallbackCollection, ComplexMap, type AsyncCallback } from '@dxos/util';
24
24
 
25
25
  import { AuthExtension, type AuthProvider, type AuthVerifier } from './auth';
26
26
 
@@ -41,12 +41,16 @@ export type SpaceProtocolOptions = {
41
41
 
42
42
  blobStore: BlobStore;
43
43
 
44
+ onFeed?: (feed: FeedWrapper<FeedMessage>) => Promise<void>;
45
+
44
46
  /**
45
47
  * Called when new session is authenticated.
46
48
  * Additional extensions can be added here.
47
49
  */
48
50
  onSessionAuth?: (session: Teleport) => void;
49
51
  onAuthFailure?: (session: Teleport) => void;
52
+
53
+ disableP2pReplication?: boolean;
50
54
  };
51
55
 
52
56
  /**
@@ -61,6 +65,8 @@ export class SpaceProtocol {
61
65
 
62
66
  public readonly blobSync: BlobSync;
63
67
 
68
+ private readonly _disableP2pReplication: boolean;
69
+
64
70
  @logInfo
65
71
  @trace.info()
66
72
  private readonly _topic: Promise<PublicKey>;
@@ -79,6 +85,8 @@ export class SpaceProtocol {
79
85
 
80
86
  private _connection?: SwarmConnection;
81
87
 
88
+ public readonly feedAdded = new CallbackCollection<AsyncCallback<FeedWrapper<FeedMessage>>>();
89
+
82
90
  get sessions(): ReadonlyMap<PublicKey, SpaceProtocolSession> {
83
91
  return this._sessions;
84
92
  }
@@ -92,7 +100,15 @@ export class SpaceProtocol {
92
100
  return this._swarmIdentity.peerKey;
93
101
  }
94
102
 
95
- constructor({ topic, swarmIdentity, networkManager, onSessionAuth, onAuthFailure, blobStore }: SpaceProtocolOptions) {
103
+ constructor({
104
+ topic,
105
+ swarmIdentity,
106
+ networkManager,
107
+ onSessionAuth,
108
+ onAuthFailure,
109
+ blobStore,
110
+ disableP2pReplication,
111
+ }: SpaceProtocolOptions) {
96
112
  this._spaceKey = topic;
97
113
  this._networkManager = networkManager;
98
114
  this._swarmIdentity = swarmIdentity;
@@ -102,16 +118,20 @@ export class SpaceProtocol {
102
118
 
103
119
  // TODO(burdon): Async race condition? Move to start?
104
120
  this._topic = subtleCrypto.digest('SHA-256', topic.asBuffer()).then(discoveryKey).then(PublicKey.from);
121
+
122
+ this._disableP2pReplication = disableP2pReplication ?? false;
105
123
  }
106
124
 
107
125
  // TODO(burdon): Create abstraction for Space (e.g., add keys and have provider).
108
- addFeed(feed: FeedWrapper<FeedMessage>) {
126
+ async addFeed(feed: FeedWrapper<FeedMessage>) {
109
127
  log('addFeed', { key: feed.key });
110
128
 
111
129
  this._feeds.add(feed);
112
130
  for (const session of this._sessions.values()) {
113
131
  session.replicator.addFeed(feed);
114
132
  }
133
+
134
+ await this.feedAdded.callSerial(feed);
115
135
  }
116
136
 
117
137
  // TODO(burdon): Rename open? Common open/close interfaces for all services?
@@ -160,6 +180,7 @@ export class SpaceProtocol {
160
180
  onSessionAuth: this._onSessionAuth,
161
181
  onAuthFailure: this._onAuthFailure,
162
182
  blobSync: this.blobSync,
183
+ disableP2pReplication: this._disableP2pReplication,
163
184
  });
164
185
  this._sessions.set(wireParams.remotePeerId, session);
165
186
 
@@ -185,6 +206,8 @@ export type SpaceProtocolSessionParams = {
185
206
  onSessionAuth?: (session: Teleport) => void;
186
207
 
187
208
  onAuthFailure?: (session: Teleport) => void;
209
+
210
+ disableP2pReplication?: boolean;
188
211
  };
189
212
 
190
213
  export enum AuthStatus {
@@ -201,6 +224,8 @@ export class SpaceProtocolSession implements WireProtocol {
201
224
  @logInfo
202
225
  private readonly _wireParams: WireProtocolParams;
203
226
 
227
+ private readonly _disableP2pReplication: boolean;
228
+
204
229
  private readonly _onSessionAuth?: (session: Teleport) => void;
205
230
  private readonly _onAuthFailure?: (session: Teleport) => void;
206
231
  private readonly _swarmIdentity: SwarmIdentity;
@@ -223,7 +248,14 @@ export class SpaceProtocolSession implements WireProtocol {
223
248
  }
224
249
 
225
250
  // TODO(dmaretskyi): Allow to pass in extra extensions.
226
- constructor({ wireParams, swarmIdentity, onSessionAuth, onAuthFailure, blobSync }: SpaceProtocolSessionParams) {
251
+ constructor({
252
+ wireParams,
253
+ swarmIdentity,
254
+ onSessionAuth,
255
+ onAuthFailure,
256
+ blobSync,
257
+ disableP2pReplication,
258
+ }: SpaceProtocolSessionParams) {
227
259
  this._wireParams = wireParams;
228
260
  this._swarmIdentity = swarmIdentity;
229
261
  this._onSessionAuth = onSessionAuth;
@@ -231,6 +263,8 @@ export class SpaceProtocolSession implements WireProtocol {
231
263
  this._blobSync = blobSync;
232
264
 
233
265
  this._teleport = new Teleport(wireParams);
266
+
267
+ this._disableP2pReplication = disableP2pReplication ?? false;
234
268
  }
235
269
 
236
270
  get stream() {
@@ -256,7 +290,10 @@ export class SpaceProtocolSession implements WireProtocol {
256
290
  },
257
291
  }),
258
292
  );
259
- this._teleport.addExtension('dxos.mesh.teleport.replicator', this.replicator);
293
+
294
+ if (!this._disableP2pReplication) {
295
+ this._teleport.addExtension('dxos.mesh.teleport.replicator', this.replicator);
296
+ }
260
297
  this._teleport.addExtension('dxos.mesh.teleport.blobsync', this._blobSync.createExtension());
261
298
  }
262
299
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Event, synchronized, trackLeaks } from '@dxos/async';
5
+ import { Event, scheduleMicroTask, synchronized, trackLeaks } from '@dxos/async';
6
6
  import { type Context, LifecycleState, Resource } from '@dxos/context';
7
7
  import { type DelegateInvitationCredential, type FeedInfo, type MemberInfo } from '@dxos/credentials';
8
8
  import { subtleCrypto } from '@dxos/crypto';
@@ -93,8 +93,8 @@ export class Space extends Resource {
93
93
  const sparse = info.assertion.designation === AdmittedFeed.Designation.DATA;
94
94
 
95
95
  if (!info.key.equals(params.genesisFeed.key)) {
96
- queueMicrotask(async () => {
97
- this.protocol.addFeed(await params.feedProvider(info.key, { sparse }));
96
+ scheduleMicroTask(this._ctx, async () => {
97
+ await this.protocol.addFeed(await params.feedProvider(info.key, { sparse }));
98
98
  });
99
99
  }
100
100
  });
@@ -119,7 +119,6 @@ export class Space extends Resource {
119
119
 
120
120
  // Start replicating the genesis feed.
121
121
  this.protocol = params.protocol;
122
- this.protocol.addFeed(params.genesisFeed);
123
122
  }
124
123
 
125
124
  @logInfo
@@ -192,6 +191,7 @@ export class Space extends Resource {
192
191
  // Order is important.
193
192
  await this._controlPipeline.start();
194
193
  await this.protocol.start();
194
+ await this.protocol.addFeed(await this._feedProvider(this._genesisFeedKey));
195
195
 
196
196
  log('opened');
197
197
  }
@@ -7,13 +7,15 @@ import { type Message, NetworkAdapter, type PeerId } from '@dxos/automerge/autom
7
7
  import { invariant } from '@dxos/invariant';
8
8
  import { log } from '@dxos/log';
9
9
 
10
+ export type TestConnectionStateProvider = () => 'on' | 'off';
11
+
10
12
  export class TestAdapter extends NetworkAdapter {
11
- static createPair() {
13
+ static createPair(connectionStateProvider: TestConnectionStateProvider = () => 'on') {
12
14
  const adapter1: TestAdapter = new TestAdapter({
13
- send: (message: Message) => sleep(10).then(() => adapter2.receive(message)),
15
+ send: (message: Message) => connectionStateProvider() === 'on' && sleep(10).then(() => adapter2.receive(message)),
14
16
  });
15
17
  const adapter2: TestAdapter = new TestAdapter({
16
- send: (message: Message) => sleep(10).then(() => adapter1.receive(message)),
18
+ send: (message: Message) => connectionStateProvider() === 'on' && sleep(10).then(() => adapter1.receive(message)),
17
19
  });
18
20
 
19
21
  return [adapter1, adapter2];