@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.
- package/dist/lib/browser/{chunk-GIAH3RXX.mjs → chunk-DR3GOD3O.mjs} +907 -442
- package/dist/lib/browser/chunk-DR3GOD3O.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +28 -13
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +21 -4
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-NDXK2NIM.cjs → chunk-DRNEKKQP.cjs} +1065 -607
- package/dist/lib/node/chunk-DRNEKKQP.cjs.map +7 -0
- package/dist/lib/node/index.cjs +77 -62
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +27 -8
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +6 -1
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +1 -0
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +29 -12
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +7 -0
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +35 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/index.d.ts +1 -0
- package/dist/types/src/packlets/spaces/index.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts +2 -0
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +3 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts +8 -3
- package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/package.json +38 -36
- package/src/packlets/invitations/invitations-handler.test.ts +1 -0
- package/src/packlets/invitations/invitations-handler.ts +12 -0
- package/src/packlets/invitations/space-invitation-protocol.test.ts +23 -1
- package/src/packlets/services/service-context.ts +44 -12
- package/src/packlets/services/service-host.ts +26 -4
- package/src/packlets/spaces/data-space-manager.test.ts +6 -0
- package/src/packlets/spaces/data-space-manager.ts +80 -36
- package/src/packlets/spaces/data-space.ts +36 -2
- package/src/packlets/spaces/edge-feed-replicator.ts +249 -0
- package/src/packlets/spaces/index.ts +1 -0
- package/src/packlets/testing/invitation-utils.ts +2 -2
- package/src/packlets/testing/test-builder.ts +20 -12
- package/src/packlets/worker/worker-runtime.ts +32 -10
- package/src/version.ts +1 -5
- package/dist/lib/browser/chunk-GIAH3RXX.mjs.map +0 -7
- 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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
364
|
-
offlineTimeout: this.
|
|
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.
|
|
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.
|
|
439
|
-
if (this.
|
|
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 {
|
|
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
|
+
});
|
|
@@ -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
|
-
|
|
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() {
|