@dxos/client-services 0.6.5 → 0.6.6-main.e1a6e1f

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 (52) hide show
  1. package/dist/lib/browser/{chunk-GIAH3RXX.mjs → chunk-DR3GOD3O.mjs} +907 -442
  2. package/dist/lib/browser/chunk-DR3GOD3O.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +28 -13
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +21 -4
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-NDXK2NIM.cjs → chunk-DRNEKKQP.cjs} +1065 -607
  9. package/dist/lib/node/chunk-DRNEKKQP.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +77 -62
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +27 -8
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  16. package/dist/types/src/packlets/services/service-context.d.ts +6 -1
  17. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  18. package/dist/types/src/packlets/services/service-host.d.ts +1 -0
  19. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  20. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +29 -12
  21. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  22. package/dist/types/src/packlets/spaces/data-space.d.ts +7 -0
  23. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  24. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +35 -0
  25. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -0
  26. package/dist/types/src/packlets/spaces/index.d.ts +1 -0
  27. package/dist/types/src/packlets/spaces/index.d.ts.map +1 -1
  28. package/dist/types/src/packlets/testing/invitation-utils.d.ts +2 -0
  29. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  30. package/dist/types/src/packlets/testing/test-builder.d.ts +3 -1
  31. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  32. package/dist/types/src/packlets/worker/worker-runtime.d.ts +8 -3
  33. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  34. package/dist/types/src/version.d.ts +1 -1
  35. package/dist/types/src/version.d.ts.map +1 -1
  36. package/package.json +38 -36
  37. package/src/packlets/invitations/invitations-handler.test.ts +1 -0
  38. package/src/packlets/invitations/invitations-handler.ts +12 -0
  39. package/src/packlets/invitations/space-invitation-protocol.test.ts +23 -1
  40. package/src/packlets/services/service-context.ts +44 -12
  41. package/src/packlets/services/service-host.ts +26 -4
  42. package/src/packlets/spaces/data-space-manager.test.ts +6 -0
  43. package/src/packlets/spaces/data-space-manager.ts +80 -36
  44. package/src/packlets/spaces/data-space.ts +36 -2
  45. package/src/packlets/spaces/edge-feed-replicator.ts +249 -0
  46. package/src/packlets/spaces/index.ts +1 -0
  47. package/src/packlets/testing/invitation-utils.ts +2 -2
  48. package/src/packlets/testing/test-builder.ts +20 -12
  49. package/src/packlets/worker/worker-runtime.ts +32 -10
  50. package/src/version.ts +1 -5
  51. package/dist/lib/browser/chunk-GIAH3RXX.mjs.map +0 -7
  52. package/dist/lib/node/chunk-NDXK2NIM.cjs.map +0 -7
@@ -6,24 +6,25 @@ import { Event, synchronized, trackLeaks } from '@dxos/async';
6
6
  import { type Doc } from '@dxos/automerge/automerge';
7
7
  import { type AutomergeUrl, type DocHandle } from '@dxos/automerge/automerge-repo';
8
8
  import { PropertiesType } from '@dxos/client-protocol';
9
- import { Context, cancelWithContext } from '@dxos/context';
9
+ import { LifecycleState, Resource, cancelWithContext } from '@dxos/context';
10
10
  import {
11
+ createAdmissionCredentials,
11
12
  getCredentialAssertion,
12
13
  type CredentialSigner,
13
14
  type DelegateInvitationCredential,
14
- createAdmissionCredentials,
15
15
  type MemberInfo,
16
16
  } from '@dxos/credentials';
17
- import { convertLegacyReferences, findInlineObjectOfType, type EchoHost } from '@dxos/echo-db';
17
+ import { convertLegacyReferences, findInlineObjectOfType, type EchoEdgeReplicator, type EchoHost } from '@dxos/echo-db';
18
18
  import {
19
19
  AuthStatus,
20
+ CredentialServerExtension,
21
+ type MeshEchoReplicator,
20
22
  type MetadataStore,
21
23
  type Space,
22
24
  type SpaceManager,
23
25
  type SpaceProtocol,
24
26
  type SpaceProtocolSession,
25
27
  } from '@dxos/echo-pipeline';
26
- import { CredentialServerExtension } from '@dxos/echo-pipeline';
27
28
  import {
28
29
  LEGACY_TYPE_PROPERTIES,
29
30
  SpaceDocVersion,
@@ -32,18 +33,20 @@ import {
32
33
  type SpaceDoc,
33
34
  } from '@dxos/echo-protocol';
34
35
  import { TYPE_PROPERTIES, generateEchoId, getTypeReference } from '@dxos/echo-schema';
35
- import { type FeedStore, writeMessages } from '@dxos/feed-store';
36
+ import type { EdgeConnection } from '@dxos/edge-client';
37
+ import { writeMessages, type FeedStore } from '@dxos/feed-store';
36
38
  import { invariant } from '@dxos/invariant';
37
39
  import { type Keyring } from '@dxos/keyring';
38
40
  import { PublicKey } from '@dxos/keys';
39
41
  import { log } from '@dxos/log';
40
- import { trace as Trace, AlreadyJoinedError } from '@dxos/protocols';
42
+ import { AlreadyJoinedError, trace as Trace } from '@dxos/protocols';
41
43
  import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
42
44
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
43
45
  import { type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
44
46
  import { SpaceMember, type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
45
47
  import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
46
48
  import { type PeerState } from '@dxos/protocols/proto/dxos/mesh/presence';
49
+ import { type Teleport } from '@dxos/teleport';
47
50
  import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
48
51
  import { type Timeframe } from '@dxos/timeframe';
49
52
  import { trace } from '@dxos/tracing';
@@ -94,32 +97,60 @@ export type AdmitMemberOptions = {
94
97
  delegationCredentialId?: PublicKey;
95
98
  };
96
99
 
100
+ export type DataSpaceManagerParams = {
101
+ spaceManager: SpaceManager;
102
+ metadataStore: MetadataStore;
103
+ keyring: Keyring;
104
+ signingContext: SigningContext;
105
+ feedStore: FeedStore<FeedMessage>;
106
+ echoHost: EchoHost;
107
+ invitationsManager: InvitationsManager;
108
+ edgeConnection?: EdgeConnection;
109
+ meshReplicator?: MeshEchoReplicator;
110
+ echoEdgeReplicator?: EchoEdgeReplicator;
111
+ runtimeParams?: DataSpaceManagerRuntimeParams;
112
+ };
113
+
97
114
  export type DataSpaceManagerRuntimeParams = {
98
115
  spaceMemberPresenceAnnounceInterval?: number;
99
116
  spaceMemberPresenceOfflineTimeout?: number;
100
117
  };
101
118
 
102
119
  @trackLeaks('open', 'close')
103
- export class DataSpaceManager {
104
- private readonly _ctx = new Context();
105
-
120
+ export class DataSpaceManager extends Resource {
106
121
  public readonly updated = new Event();
107
122
 
108
123
  private readonly _spaces = new ComplexMap<PublicKey, DataSpace>(PublicKey.hash);
109
124
 
110
- private _isOpen = false;
111
125
  private readonly _instanceId = PublicKey.random().toHex();
112
126
 
113
- constructor(
114
- private readonly _spaceManager: SpaceManager,
115
- private readonly _metadataStore: MetadataStore,
116
- private readonly _keyring: Keyring,
117
- private readonly _signingContext: SigningContext,
118
- private readonly _feedStore: FeedStore<FeedMessage>,
119
- private readonly _echoHost: EchoHost,
120
- private readonly _invitationsManager: InvitationsManager,
121
- private readonly _params?: DataSpaceManagerRuntimeParams,
122
- ) {
127
+ private readonly _spaceManager: SpaceManager;
128
+ private readonly _metadataStore: MetadataStore;
129
+ private readonly _keyring: Keyring;
130
+ private readonly _signingContext: SigningContext;
131
+ private readonly _feedStore: FeedStore<FeedMessage>;
132
+ private readonly _echoHost: EchoHost;
133
+ private readonly _invitationsManager: InvitationsManager;
134
+ private readonly _edgeConnection?: EdgeConnection = undefined;
135
+ private readonly _meshReplicator?: MeshEchoReplicator = undefined;
136
+ private readonly _echoEdgeReplicator?: EchoEdgeReplicator = undefined;
137
+ private readonly _runtimeParams?: DataSpaceManagerRuntimeParams = undefined;
138
+
139
+ constructor(params: DataSpaceManagerParams) {
140
+ super();
141
+
142
+ this._spaceManager = params.spaceManager;
143
+ this._metadataStore = params.metadataStore;
144
+ this._keyring = params.keyring;
145
+ this._signingContext = params.signingContext;
146
+ this._feedStore = params.feedStore;
147
+ this._echoHost = params.echoHost;
148
+ this._meshReplicator = params.meshReplicator;
149
+ this._invitationsManager = params.invitationsManager;
150
+ this._edgeConnection = params.edgeConnection;
151
+ this._echoEdgeReplicator = params.echoEdgeReplicator;
152
+ this._runtimeParams = params.runtimeParams;
153
+
123
154
  trace.diagnostic({
124
155
  id: 'spaces',
125
156
  name: 'Spaces',
@@ -152,7 +183,7 @@ export class DataSpaceManager {
152
183
  }
153
184
 
154
185
  @synchronized
155
- async open() {
186
+ protected override async _open() {
156
187
  log('open');
157
188
  log.trace('dxos.echo.data-space-manager.open', Trace.begin({ id: this._instanceId }));
158
189
  log('metadata loaded', { spaces: this._metadataStore.spaces.length });
@@ -166,17 +197,14 @@ export class DataSpaceManager {
166
197
  }
167
198
  });
168
199
 
169
- this._isOpen = true;
170
200
  this.updated.emit();
171
201
 
172
202
  log.trace('dxos.echo.data-space-manager.open', Trace.end({ id: this._instanceId }));
173
203
  }
174
204
 
175
205
  @synchronized
176
- async close() {
206
+ protected override async _close() {
177
207
  log('close');
178
- this._isOpen = false;
179
- await this._ctx.dispose();
180
208
  for (const space of this._spaces.values()) {
181
209
  await space.close();
182
210
  }
@@ -188,7 +216,7 @@ export class DataSpaceManager {
188
216
  */
189
217
  @synchronized
190
218
  async createSpace() {
191
- invariant(this._isOpen, 'Not open.');
219
+ invariant(this._lifecycleState === LifecycleState.OPEN, 'Not open.');
192
220
  const spaceKey = await this._keyring.createKey();
193
221
  const controlFeedKey = await this._keyring.createKey();
194
222
  const dataFeedKey = await this._keyring.createKey();
@@ -278,7 +306,7 @@ export class DataSpaceManager {
278
306
  @synchronized
279
307
  async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
280
308
  log('accept space', { opts });
281
- invariant(this._isOpen, 'Not open.');
309
+ invariant(this._lifecycleState === LifecycleState.OPEN, 'Not open.');
282
310
  invariant(!this._spaces.has(opts.spaceKey), 'Space already exists.');
283
311
 
284
312
  const metadata: SpaceMetadata = {
@@ -360,8 +388,8 @@ export class DataSpaceManager {
360
388
  localPeerId: this._signingContext.deviceKey,
361
389
  });
362
390
  const presence = new Presence({
363
- announceInterval: this._params?.spaceMemberPresenceAnnounceInterval ?? PRESENCE_ANNOUNCE_INTERVAL,
364
- offlineTimeout: this._params?.spaceMemberPresenceOfflineTimeout ?? PRESENCE_OFFLINE_TIMEOUT,
391
+ announceInterval: this._runtimeParams?.spaceMemberPresenceAnnounceInterval ?? PRESENCE_ANNOUNCE_INTERVAL,
392
+ offlineTimeout: this._runtimeParams?.spaceMemberPresenceOfflineTimeout ?? PRESENCE_OFFLINE_TIMEOUT,
365
393
  identityKey: this._signingContext.identityKey,
366
394
  gossip,
367
395
  });
@@ -394,11 +422,7 @@ export class DataSpaceManager {
394
422
  gossip.createExtension({ remotePeerId: session.remotePeerId }),
395
423
  );
396
424
  session.addExtension('dxos.mesh.teleport.notarization', dataSpace.notarizationPlugin.createExtension());
397
- await this._echoHost.authorizeDevice(space.key, session.remotePeerId);
398
- if (!session.isOpen) {
399
- return;
400
- }
401
- session.addExtension('dxos.mesh.teleport.automerge', this._echoHost.createReplicationExtension());
425
+ await this._connectEchoMeshReplicator(space, session);
402
426
  } catch (err: any) {
403
427
  log.warn('error on authorized connection', { err });
404
428
  await session.close(err);
@@ -435,8 +459,8 @@ export class DataSpaceManager {
435
459
  log('before space ready', { space: space.key });
436
460
  },
437
461
  afterReady: async () => {
438
- log('after space ready', { space: space.key, open: this._isOpen });
439
- if (this._isOpen) {
462
+ log('after space ready', { space: space.key, open: this._lifecycleState === LifecycleState.OPEN });
463
+ if (this._lifecycleState === LifecycleState.OPEN) {
440
464
  await this._createDelegatedInvitations(dataSpace, [...space.spaceState.invitations.entries()]);
441
465
  this._handleMemberRoleChanges(presence, space.protocol, [...space.spaceState.members.values()]);
442
466
  this.updated.emit();
@@ -447,6 +471,13 @@ export class DataSpaceManager {
447
471
  },
448
472
  },
449
473
  cache: metadata.cache,
474
+ edgeConnection: this._edgeConnection,
475
+ });
476
+ dataSpace.postOpen.append(async () => {
477
+ await this._echoEdgeReplicator?.connectToSpace(dataSpace.id);
478
+ });
479
+ dataSpace.preClose.append(async () => {
480
+ await this._echoEdgeReplicator?.disconnectFromSpace(dataSpace.id);
450
481
  });
451
482
 
452
483
  presence.newPeer.on((peerState) => {
@@ -463,6 +494,19 @@ export class DataSpaceManager {
463
494
  return dataSpace;
464
495
  }
465
496
 
497
+ private async _connectEchoMeshReplicator(space: Space, session: Teleport) {
498
+ const replicator = this._meshReplicator;
499
+ if (!replicator) {
500
+ log.warn('p2p automerge replication disabled', { space: space.key });
501
+ return;
502
+ }
503
+ await replicator.authorizeDevice(space.key, session.remotePeerId);
504
+ // session ended during device authorization
505
+ if (session.isOpen) {
506
+ session.addExtension('dxos.mesh.teleport.automerge', replicator.createExtension());
507
+ }
508
+ }
509
+
466
510
  private _handleMemberRoleChanges(presence: Presence, spaceProtocol: SpaceProtocol, memberInfo: MemberInfo[]): void {
467
511
  let closedSessions = 0;
468
512
  for (const member of memberInfo) {
@@ -10,7 +10,8 @@ import { timed, warnAfterTimeout } from '@dxos/debug';
10
10
  import { type EchoHost, type DatabaseRoot } from '@dxos/echo-db';
11
11
  import { createMappedFeedWriter, type MetadataStore, type Space } from '@dxos/echo-pipeline';
12
12
  import { SpaceDocVersion } from '@dxos/echo-protocol';
13
- import { type FeedStore } from '@dxos/feed-store';
13
+ import type { EdgeConnection } from '@dxos/edge-client';
14
+ import { type FeedStore, type FeedWrapper } from '@dxos/feed-store';
14
15
  import { failedInvariant } from '@dxos/invariant';
15
16
  import { type Keyring } from '@dxos/keyring';
16
17
  import { PublicKey } from '@dxos/keys';
@@ -34,10 +35,11 @@ import { type GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gos
34
35
  import { type Gossip, type Presence } from '@dxos/teleport-extension-gossip';
35
36
  import { Timeframe } from '@dxos/timeframe';
36
37
  import { trace } from '@dxos/tracing';
37
- import { ComplexSet } from '@dxos/util';
38
+ import { CallbackCollection, ComplexSet, type AsyncCallback } from '@dxos/util';
38
39
 
39
40
  import { AutomergeSpaceState } from './automerge-space-state';
40
41
  import { type SigningContext } from './data-space-manager';
42
+ import { EdgeFeedReplicator } from './edge-feed-replicator';
41
43
  import { runEpochMigration } from './epoch-migrations';
42
44
  import { NotarizationPlugin } from './notarization-plugin';
43
45
  import { TrustedKeySetAuthVerifier } from '../identity';
@@ -71,6 +73,7 @@ export type DataSpaceParams = {
71
73
  signingContext: SigningContext;
72
74
  callbacks?: DataSpaceCallbacks;
73
75
  cache?: SpaceCache;
76
+ edgeConnection?: EdgeConnection;
74
77
  };
75
78
 
76
79
  export type CreateEpochOptions = {
@@ -95,6 +98,7 @@ export class DataSpace {
95
98
  private readonly _callbacks: DataSpaceCallbacks;
96
99
  private readonly _cache?: SpaceCache = undefined;
97
100
  private readonly _echoHost: EchoHost;
101
+ private readonly _edgeFeedReplicator?: EdgeFeedReplicator = undefined;
98
102
 
99
103
  // TODO(dmaretskyi): Move into Space?
100
104
  private readonly _automergeSpaceState = new AutomergeSpaceState((rootUrl) => this._onNewAutomergeRoot(rootUrl));
@@ -113,6 +117,9 @@ export class DataSpace {
113
117
  public readonly authVerifier: TrustedKeySetAuthVerifier;
114
118
  public readonly stateUpdate = new Event();
115
119
 
120
+ public readonly postOpen = new CallbackCollection<AsyncCallback<void>>();
121
+ public readonly preClose = new CallbackCollection<AsyncCallback<void>>();
122
+
116
123
  public metrics: SpaceProto.Metrics = {};
117
124
 
118
125
  constructor(params: DataSpaceParams) {
@@ -142,6 +149,10 @@ export class DataSpace {
142
149
 
143
150
  this._cache = params.cache;
144
151
 
152
+ if (params.edgeConnection) {
153
+ this._edgeFeedReplicator = new EdgeFeedReplicator({ messenger: params.edgeConnection, spaceId: this.id });
154
+ }
155
+
145
156
  this._state = params.initialState;
146
157
  log('new state', { state: SpaceState[this._state] });
147
158
  }
@@ -212,12 +223,22 @@ export class DataSpace {
212
223
  await this._inner.spaceState.addCredentialProcessor(this._notarizationPlugin);
213
224
  await this._automergeSpaceState.open();
214
225
  await this._inner.spaceState.addCredentialProcessor(this._automergeSpaceState);
226
+
227
+ if (this._edgeFeedReplicator) {
228
+ this.inner.protocol.feedAdded.append(this._onFeedAdded);
229
+ }
230
+
215
231
  await this._inner.open(new Context());
232
+
233
+ await this._edgeFeedReplicator?.open();
234
+
216
235
  this._state = SpaceState.SPACE_CONTROL_ONLY;
217
236
  log('new state', { state: SpaceState[this._state] });
218
237
  this.stateUpdate.emit();
219
238
  this.metrics = {};
220
239
  this.metrics.open = new Date();
240
+
241
+ await this.postOpen.callSerial();
221
242
  }
222
243
 
223
244
  @synchronized
@@ -227,11 +248,20 @@ export class DataSpace {
227
248
 
228
249
  private async _close() {
229
250
  await this._callbacks.beforeClose?.();
251
+
252
+ await this.preClose.callSerial();
253
+
230
254
  this._state = SpaceState.SPACE_CLOSED;
231
255
  log('new state', { state: SpaceState[this._state] });
232
256
  await this._ctx.dispose();
233
257
  this._ctx = new Context();
234
258
 
259
+ if (this._edgeFeedReplicator) {
260
+ this.inner.protocol.feedAdded.remove(this._onFeedAdded);
261
+ }
262
+
263
+ await this._edgeFeedReplicator?.close();
264
+
235
265
  await this.authVerifier.close();
236
266
 
237
267
  await this._inner.close();
@@ -513,6 +543,10 @@ export class DataSpace {
513
543
  log('new state', { state: SpaceState[this._state] });
514
544
  this.stateUpdate.emit();
515
545
  }
546
+
547
+ private _onFeedAdded = async (feed: FeedWrapper<any>) => {
548
+ await this._edgeFeedReplicator!.addFeed(feed);
549
+ };
516
550
  }
517
551
 
518
552
  type CreateEpochResult = {
@@ -0,0 +1,249 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { decode as decodeCbor, encode as encodeCbor } from 'cbor-x';
6
+
7
+ import { Event, Mutex, scheduleMicroTask } from '@dxos/async';
8
+ import { Resource, type Context } from '@dxos/context';
9
+ import { type EdgeConnection } from '@dxos/edge-client';
10
+ import { type FeedWrapper } from '@dxos/feed-store';
11
+ import { invariant } from '@dxos/invariant';
12
+ import { PublicKey, type SpaceId } from '@dxos/keys';
13
+ import { log } from '@dxos/log';
14
+ import { EdgeService } from '@dxos/protocols';
15
+ import { buf } from '@dxos/protocols/buf';
16
+ import { MessageSchema as RouterMessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
17
+ import type { FeedBlock, ProtocolMessage } from '@dxos/protocols/feed-replication';
18
+ import { ComplexMap, arrayToBuffer, bufferToArray, defaultMap, rangeFromTo } from '@dxos/util';
19
+
20
+ export type EdgeFeedReplicatorParams = {
21
+ messenger: EdgeConnection;
22
+ spaceId: SpaceId;
23
+ };
24
+
25
+ export class EdgeFeedReplicator extends Resource {
26
+ private readonly _messenger: EdgeConnection;
27
+ private readonly _spaceId: SpaceId;
28
+ private readonly _feeds = new ComplexMap<PublicKey, FeedWrapper<any>>(PublicKey.hash);
29
+
30
+ private _connectionCtx?: Context = undefined;
31
+ private _connected = false;
32
+ /**
33
+ * Feed length at service.
34
+ */
35
+ private _remoteLength = new ComplexMap<PublicKey, number>(PublicKey.hash);
36
+
37
+ /**
38
+ * Protects against concurrent pushes so that remote length is updated consistently.
39
+ */
40
+ private _pushMutex = new ComplexMap<PublicKey, Mutex>(PublicKey.hash);
41
+
42
+ constructor({ messenger, spaceId }: EdgeFeedReplicatorParams) {
43
+ super();
44
+ this._messenger = messenger;
45
+ this._spaceId = spaceId;
46
+ }
47
+
48
+ protected override async _open(): Promise<void> {
49
+ // TODO: handle reconnects
50
+ this._ctx.onDispose(
51
+ this._messenger.addListener(async (message) => {
52
+ if (!message.serviceId) {
53
+ return;
54
+ }
55
+ const [service, ...rest] = message.serviceId.split(':');
56
+ if (service !== 'hypercore-replicator') {
57
+ return;
58
+ }
59
+
60
+ const [spaceId] = rest;
61
+ if (spaceId !== this._spaceId) {
62
+ log('spaceID mismatch', { spaceId, _spaceId: this._spaceId });
63
+ return;
64
+ }
65
+
66
+ const payload = decodeCbor(message.payload!.value) as ProtocolMessage;
67
+ log.info('recv', { from: message.source, payload });
68
+ this._onMessage(payload);
69
+ }),
70
+ );
71
+
72
+ this._connected = true;
73
+ this._connectionCtx = this._ctx.derive();
74
+ for (const feed of this._feeds.values()) {
75
+ await this._replicateFeed(feed);
76
+ }
77
+ }
78
+
79
+ protected override async _close(): Promise<void> {
80
+ this._connected = false;
81
+
82
+ this._connected = false;
83
+ await this._connectionCtx?.dispose();
84
+ this._connectionCtx = undefined;
85
+ this._remoteLength.clear();
86
+ }
87
+
88
+ async addFeed(feed: FeedWrapper<any>) {
89
+ log.info('addFeed', { key: feed.key });
90
+ this._feeds.set(feed.key, feed);
91
+
92
+ if (this._connected) {
93
+ await this._replicateFeed(feed);
94
+ }
95
+ }
96
+
97
+ private _getPushMutex(key: PublicKey) {
98
+ return defaultMap(this._pushMutex, key, () => new Mutex());
99
+ }
100
+
101
+ private async _replicateFeed(feed: FeedWrapper<any>) {
102
+ invariant(this._connectionCtx);
103
+
104
+ await this._sendMessage({
105
+ type: 'get-metadata',
106
+ feedKey: feed.key.toHex(),
107
+ });
108
+
109
+ Event.wrap(feed.core as any, 'append').on(this._connectionCtx, async () => {
110
+ await this._pushBlocksIfNeeded(feed);
111
+ });
112
+ }
113
+
114
+ private async _sendMessage(message: ProtocolMessage) {
115
+ log.info('sending message', { message });
116
+
117
+ invariant(message.feedKey);
118
+ const payloadValue = bufferToArray(encodeCbor(message));
119
+
120
+ await this._messenger.send(
121
+ buf.create(RouterMessageSchema, {
122
+ source: {
123
+ identityKey: this._messenger.identityKey.toHex(),
124
+ peerKey: this._messenger.deviceKey.toHex(),
125
+ },
126
+ serviceId: `${EdgeService.FEED_REPLICATOR}:${this._spaceId}`,
127
+ payload: { value: payloadValue },
128
+ }),
129
+ );
130
+ }
131
+
132
+ private _onMessage(message: ProtocolMessage) {
133
+ log.info('received message', { message });
134
+
135
+ scheduleMicroTask(this._ctx, async () => {
136
+ switch (message.type) {
137
+ case 'metadata': {
138
+ const feedKey = PublicKey.fromHex(message.feedKey);
139
+ const feed = this._feeds.get(feedKey);
140
+ if (!feed) {
141
+ log.warn('Feed not found', { feedKey });
142
+ return;
143
+ }
144
+
145
+ using _guard = await this._getPushMutex(feed.key).acquire();
146
+
147
+ this._remoteLength.set(feedKey, message.length);
148
+
149
+ if (message.length > feed.length) {
150
+ await this._sendMessage({
151
+ type: 'request',
152
+ feedKey: feedKey.toHex(),
153
+ range: { from: feed.length, to: message.length },
154
+ });
155
+ } else if (message.length < feed.length) {
156
+ await this._pushBlocks(feed, message.length, feed.length);
157
+ }
158
+
159
+ break;
160
+ }
161
+
162
+ case 'data': {
163
+ const feedKey = PublicKey.fromHex(message.feedKey);
164
+ const feed = this._feeds.get(feedKey);
165
+ if (!feed) {
166
+ log.warn('Feed not found', { feedKey });
167
+ return;
168
+ }
169
+
170
+ await this._integrateBlocks(feed, message.blocks);
171
+ break;
172
+ }
173
+
174
+ default: {
175
+ log.warn('Unknown message', { ...message });
176
+ }
177
+ }
178
+ });
179
+ }
180
+
181
+ private async _pushBlocks(feed: FeedWrapper<any>, from: number, to: number) {
182
+ log.info('pushing blocks', { feed: feed.key.toHex(), from, to });
183
+
184
+ const blocks: FeedBlock[] = await Promise.all(
185
+ rangeFromTo(from, to).map(async (index) => {
186
+ const data = await feed.get(index, { valueEncoding: 'binary' });
187
+ invariant(data instanceof Uint8Array);
188
+ const proof = await feed.proof(index);
189
+
190
+ return {
191
+ index,
192
+ data,
193
+ nodes: proof.nodes,
194
+ signature: proof.signature,
195
+ } satisfies FeedBlock;
196
+ }),
197
+ );
198
+
199
+ await this._sendMessage({
200
+ type: 'data',
201
+ feedKey: feed.key.toHex(),
202
+ blocks,
203
+ });
204
+ this._remoteLength.set(feed.key, to);
205
+ }
206
+
207
+ private async _integrateBlocks(feed: FeedWrapper<any>, blocks: FeedBlock[]) {
208
+ log.info('integrating blocks', { feed: feed.key.toHex(), blocks: blocks.length });
209
+
210
+ for (const block of blocks) {
211
+ if (feed.has(block.index)) {
212
+ continue;
213
+ }
214
+ const blockBuffer = bufferizeBlock(block);
215
+
216
+ await feed.putBuffer(
217
+ block.index,
218
+ blockBuffer.data,
219
+ { nodes: blockBuffer.nodes, signature: blockBuffer.signature },
220
+ null,
221
+ );
222
+ }
223
+ }
224
+
225
+ private async _pushBlocksIfNeeded(feed: FeedWrapper<any>) {
226
+ using _guard = await this._getPushMutex(feed.key).acquire();
227
+
228
+ if (!this._remoteLength.has(feed.key)) {
229
+ return;
230
+ }
231
+
232
+ const remoteLength = this._remoteLength.get(feed.key)!;
233
+ if (remoteLength < feed.length) {
234
+ await this._pushBlocks(feed, remoteLength, feed.length);
235
+ }
236
+ }
237
+ }
238
+
239
+ // hypercore requires buffers
240
+ const bufferizeBlock = (block: FeedBlock) => ({
241
+ index: block.index,
242
+ data: arrayToBuffer(block.data),
243
+ nodes: block.nodes.map((node) => ({
244
+ index: node.index,
245
+ hash: arrayToBuffer(node.hash),
246
+ size: node.size,
247
+ })),
248
+ signature: arrayToBuffer(block.signature),
249
+ });
@@ -5,3 +5,4 @@
5
5
  export * from './data-space-manager';
6
6
  export * from './data-space';
7
7
  export * from './spaces-service';
8
+ export * from './edge-feed-replicator';
@@ -207,7 +207,7 @@ export const performInvitation = ({
207
207
  return [hostComplete.wait(), guestComplete.wait()];
208
208
  };
209
209
 
210
- const createInvitation = async (
210
+ export const createInvitation = async (
211
211
  host: ServiceContext | InvitationHost,
212
212
  options?: Partial<Invitation>,
213
213
  ): Promise<CancellableInvitation> => {
@@ -226,7 +226,7 @@ const createInvitation = async (
226
226
  return host.share(options);
227
227
  };
228
228
 
229
- const acceptInvitation = (
229
+ export const acceptInvitation = (
230
230
  guest: ServiceContext | InvitationGuest,
231
231
  invitation: Invitation,
232
232
  guestDeviceProfile?: DeviceProfileDocument,
@@ -7,7 +7,7 @@ import { Context } from '@dxos/context';
7
7
  import { createCredentialSignerWithChain, CredentialGenerator } from '@dxos/credentials';
8
8
  import { failUndefined } from '@dxos/debug';
9
9
  import { EchoHost } from '@dxos/echo-db';
10
- import { MetadataStore, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
10
+ import { MetadataStore, SnapshotStore, SpaceManager, valueEncoding, MeshEchoReplicator } from '@dxos/echo-pipeline';
11
11
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
12
12
  import { Keyring } from '@dxos/keyring';
13
13
  import { type LevelDB } from '@dxos/kv-store';
@@ -51,7 +51,7 @@ export const createServiceContext = async ({
51
51
  const level = createTestLevel();
52
52
  await level.open();
53
53
 
54
- return new ServiceContext(storage, level, networkManager, signalManager, {
54
+ return new ServiceContext(storage, level, networkManager, signalManager, undefined, {
55
55
  invitationConnectionDefaultParams: { controlHeartbeatInterval: 200 },
56
56
  ...runtimeParams,
57
57
  });
@@ -107,6 +107,7 @@ export type TestPeerProps = {
107
107
  signingContext?: SigningContext;
108
108
  blobStore?: BlobStore;
109
109
  echoHost?: EchoHost;
110
+ meshEchoReplicator?: MeshEchoReplicator;
110
111
  invitationsManager?: InvitationsManager;
111
112
  };
112
113
 
@@ -183,17 +184,24 @@ export class TestPeer {
183
184
  return (this._props.echoHost ??= new EchoHost({ kv: this.level }));
184
185
  }
185
186
 
187
+ get meshEchoReplicator() {
188
+ return (this._props.meshEchoReplicator ??= new MeshEchoReplicator());
189
+ }
190
+
186
191
  get dataSpaceManager(): DataSpaceManager {
187
- return (this._props.dataSpaceManager ??= new DataSpaceManager(
188
- this.spaceManager,
189
- this.metadataStore,
190
- this.keyring,
191
- this.identity,
192
- this.feedStore,
193
- this.echoHost,
194
- this.invitationsManager,
195
- this._opts.dataSpaceParams,
196
- ));
192
+ return (this._props.dataSpaceManager ??= new DataSpaceManager({
193
+ spaceManager: this.spaceManager,
194
+ metadataStore: this.metadataStore,
195
+ keyring: this.keyring,
196
+ signingContext: this.identity,
197
+ feedStore: this.feedStore,
198
+ echoHost: this.echoHost,
199
+ invitationsManager: this.invitationsManager,
200
+ edgeConnection: undefined,
201
+ meshReplicator: this.meshEchoReplicator,
202
+ echoEdgeReplicator: undefined,
203
+ runtimeParams: this._opts.dataSpaceParams,
204
+ }));
197
205
  }
198
206
 
199
207
  get invitationsManager() {