@dxos/client-services 0.5.9-main.ea1d25b → 0.5.9-main.f099efe
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-LKSDZ2AB.mjs → chunk-H3XJK6ZN.mjs} +1632 -966
- package/dist/lib/browser/chunk-H3XJK6ZN.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +13 -4
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +20 -13
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-FBXXJHAL.cjs → chunk-JRDM7NQS.cjs} +1868 -1209
- package/dist/lib/node/chunk-JRDM7NQS.cjs.map +7 -0
- package/dist/lib/node/index.cjs +53 -44
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +26 -19
- package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
- package/dist/types/src/packlets/identity/contacts-service.d.ts +14 -0
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/default-space-state-machine.d.ts +19 -0
- package/dist/types/src/packlets/identity/default-space-state-machine.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/identity-service.d.ts +14 -7
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +4 -1
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -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 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/automerge-space-state.d.ts +4 -1
- package/dist/types/src/packlets/spaces/automerge-space-state.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +15 -4
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +9 -9
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +23 -0
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +5 -2
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/index.d.ts +1 -0
- package/dist/types/src/packlets/storage/index.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/profile-archive.d.ts +14 -0
- package/dist/types/src/packlets/storage/profile-archive.d.ts.map +1 -0
- package/dist/types/src/packlets/testing/test-builder.d.ts +8 -6
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/package.json +36 -36
- package/src/packlets/identity/contacts-service.ts +85 -0
- package/src/packlets/identity/default-space-state-machine.ts +44 -0
- package/src/packlets/identity/identity-service.test.ts +35 -5
- package/src/packlets/identity/identity-service.ts +76 -8
- package/src/packlets/identity/identity.ts +25 -2
- package/src/packlets/invitations/invitations-handler.ts +13 -5
- package/src/packlets/invitations/space-invitation-protocol.ts +11 -32
- package/src/packlets/services/service-context.ts +1 -4
- package/src/packlets/services/service-host.ts +23 -42
- package/src/packlets/spaces/automerge-space-state.ts +11 -2
- package/src/packlets/spaces/data-space-manager.test.ts +46 -1
- package/src/packlets/spaces/data-space-manager.ts +136 -33
- package/src/packlets/spaces/data-space.ts +85 -152
- package/src/packlets/spaces/epoch-migrations.ts +135 -0
- package/src/packlets/spaces/spaces-service.ts +54 -4
- package/src/packlets/storage/index.ts +1 -0
- package/src/packlets/storage/profile-archive.ts +97 -0
- package/src/packlets/testing/test-builder.ts +12 -10
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-LKSDZ2AB.mjs.map +0 -7
- package/dist/lib/node/chunk-FBXXJHAL.cjs.map +0 -7
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import { Event, synchronized, trackLeaks } from '@dxos/async';
|
|
6
6
|
import { type Doc } from '@dxos/automerge/automerge';
|
|
7
|
-
import { type AutomergeUrl } from '@dxos/automerge/automerge-repo';
|
|
8
|
-
import {
|
|
7
|
+
import { type AutomergeUrl, type DocHandle } from '@dxos/automerge/automerge-repo';
|
|
8
|
+
import { PropertiesType } from '@dxos/client-protocol';
|
|
9
|
+
import { Context, cancelWithContext } from '@dxos/context';
|
|
9
10
|
import {
|
|
11
|
+
getCredentialAssertion,
|
|
10
12
|
type CredentialSigner,
|
|
11
13
|
type DelegateInvitationCredential,
|
|
12
|
-
|
|
14
|
+
createAdmissionCredentials,
|
|
13
15
|
type MemberInfo,
|
|
14
16
|
} from '@dxos/credentials';
|
|
15
|
-
import { type EchoHost } from '@dxos/echo-db';
|
|
17
|
+
import { convertLegacyReferences, findInlineObjectOfType, type EchoHost } from '@dxos/echo-db';
|
|
16
18
|
import {
|
|
17
19
|
AuthStatus,
|
|
18
20
|
type MetadataStore,
|
|
@@ -21,25 +23,33 @@ import {
|
|
|
21
23
|
type SpaceProtocol,
|
|
22
24
|
type SpaceProtocolSession,
|
|
23
25
|
} from '@dxos/echo-pipeline';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
+
import { CredentialServerExtension } from '@dxos/echo-pipeline';
|
|
27
|
+
import {
|
|
28
|
+
LEGACY_TYPE_PROPERTIES,
|
|
29
|
+
SpaceDocVersion,
|
|
30
|
+
encodeReference,
|
|
31
|
+
type ObjectStructure,
|
|
32
|
+
type SpaceDoc,
|
|
33
|
+
} from '@dxos/echo-protocol';
|
|
34
|
+
import { TYPE_PROPERTIES, generateEchoId, getTypeReference } from '@dxos/echo-schema';
|
|
35
|
+
import { type FeedStore, writeMessages } from '@dxos/feed-store';
|
|
26
36
|
import { invariant } from '@dxos/invariant';
|
|
27
37
|
import { type Keyring } from '@dxos/keyring';
|
|
28
38
|
import { PublicKey } from '@dxos/keys';
|
|
29
39
|
import { log } from '@dxos/log';
|
|
30
|
-
import { trace as Trace } from '@dxos/protocols';
|
|
40
|
+
import { trace as Trace, AlreadyJoinedError } from '@dxos/protocols';
|
|
31
41
|
import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
32
42
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
33
43
|
import { type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
34
|
-
import { type Credential, type ProfileDocument
|
|
44
|
+
import { SpaceMember, type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
35
45
|
import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
36
46
|
import { type PeerState } from '@dxos/protocols/proto/dxos/mesh/presence';
|
|
37
47
|
import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
|
|
38
48
|
import { type Timeframe } from '@dxos/timeframe';
|
|
39
49
|
import { trace } from '@dxos/tracing';
|
|
40
|
-
import { ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
|
|
50
|
+
import { ComplexMap, assignDeep, deferFunction, forEachAsync } from '@dxos/util';
|
|
41
51
|
|
|
42
|
-
import { DataSpace
|
|
52
|
+
import { DataSpace } from './data-space';
|
|
43
53
|
import { spaceGenesis } from './genesis';
|
|
44
54
|
import { createAuthProvider } from '../identity';
|
|
45
55
|
import { type InvitationsManager } from '../invitations';
|
|
@@ -47,6 +57,9 @@ import { type InvitationsManager } from '../invitations';
|
|
|
47
57
|
const PRESENCE_ANNOUNCE_INTERVAL = 10_000;
|
|
48
58
|
const PRESENCE_OFFLINE_TIMEOUT = 20_000;
|
|
49
59
|
|
|
60
|
+
// Space properties key for default metadata.
|
|
61
|
+
const DEFAULT_SPACE_KEY = '__DEFAULT__';
|
|
62
|
+
|
|
50
63
|
export interface SigningContext {
|
|
51
64
|
identityKey: PublicKey;
|
|
52
65
|
deviceKey: PublicKey;
|
|
@@ -73,6 +86,14 @@ export type AcceptSpaceOptions = {
|
|
|
73
86
|
dataTimeframe?: Timeframe;
|
|
74
87
|
};
|
|
75
88
|
|
|
89
|
+
export type AdmitMemberOptions = {
|
|
90
|
+
spaceKey: PublicKey;
|
|
91
|
+
identityKey: PublicKey;
|
|
92
|
+
role: SpaceMember.Role;
|
|
93
|
+
profile?: ProfileDocument;
|
|
94
|
+
delegationCredentialId?: PublicKey;
|
|
95
|
+
};
|
|
96
|
+
|
|
76
97
|
export type DataSpaceManagerRuntimeParams = {
|
|
77
98
|
spaceMemberPresenceAnnounceInterval?: number;
|
|
78
99
|
spaceMemberPresenceOfflineTimeout?: number;
|
|
@@ -88,8 +109,6 @@ export class DataSpaceManager {
|
|
|
88
109
|
|
|
89
110
|
private _isOpen = false;
|
|
90
111
|
private readonly _instanceId = PublicKey.random().toHex();
|
|
91
|
-
private readonly _spaceMemberPresenceAnnounceInterval: number;
|
|
92
|
-
private readonly _spaceMemberPresenceOfflineTimeout: number;
|
|
93
112
|
|
|
94
113
|
constructor(
|
|
95
114
|
private readonly _spaceManager: SpaceManager,
|
|
@@ -99,15 +118,8 @@ export class DataSpaceManager {
|
|
|
99
118
|
private readonly _feedStore: FeedStore<FeedMessage>,
|
|
100
119
|
private readonly _echoHost: EchoHost,
|
|
101
120
|
private readonly _invitationsManager: InvitationsManager,
|
|
102
|
-
|
|
121
|
+
private readonly _params?: DataSpaceManagerRuntimeParams,
|
|
103
122
|
) {
|
|
104
|
-
const {
|
|
105
|
-
spaceMemberPresenceAnnounceInterval = PRESENCE_ANNOUNCE_INTERVAL,
|
|
106
|
-
spaceMemberPresenceOfflineTimeout = PRESENCE_OFFLINE_TIMEOUT,
|
|
107
|
-
} = params ?? {};
|
|
108
|
-
this._spaceMemberPresenceAnnounceInterval = spaceMemberPresenceAnnounceInterval;
|
|
109
|
-
this._spaceMemberPresenceOfflineTimeout = spaceMemberPresenceOfflineTimeout;
|
|
110
|
-
|
|
111
123
|
trace.diagnostic({
|
|
112
124
|
id: 'spaces',
|
|
113
125
|
name: 'Spaces',
|
|
@@ -117,7 +129,7 @@ export class DataSpaceManager {
|
|
|
117
129
|
const rootHandle = rootUrl ? this._echoHost.automergeRepo.find(rootUrl as AutomergeUrl) : undefined;
|
|
118
130
|
const rootDoc = rootHandle?.docSync() as Doc<SpaceDoc> | undefined;
|
|
119
131
|
|
|
120
|
-
const properties = rootDoc &&
|
|
132
|
+
const properties = rootDoc && findInlineObjectOfType(rootDoc, TYPE_PROPERTIES);
|
|
121
133
|
|
|
122
134
|
return {
|
|
123
135
|
key: space.key.toHex(),
|
|
@@ -157,12 +169,6 @@ export class DataSpaceManager {
|
|
|
157
169
|
this._isOpen = true;
|
|
158
170
|
this.updated.emit();
|
|
159
171
|
|
|
160
|
-
for (const space of this._spaces.values()) {
|
|
161
|
-
if (space.state !== SpaceState.INACTIVE) {
|
|
162
|
-
space.initializeDataPipelineAsync();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
172
|
log.trace('dxos.echo.data-space-manager.open', Trace.end({ id: this._instanceId }));
|
|
167
173
|
}
|
|
168
174
|
|
|
@@ -174,6 +180,7 @@ export class DataSpaceManager {
|
|
|
174
180
|
for (const space of this._spaces.values()) {
|
|
175
181
|
await space.close();
|
|
176
182
|
}
|
|
183
|
+
this._spaces.clear();
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
/**
|
|
@@ -197,6 +204,7 @@ export class DataSpaceManager {
|
|
|
197
204
|
|
|
198
205
|
const root = await this._echoHost.createSpaceRoot(spaceKey);
|
|
199
206
|
const space = await this._constructSpace(metadata);
|
|
207
|
+
await space.open();
|
|
200
208
|
|
|
201
209
|
const credentials = await spaceGenesis(this._keyring, this._signingContext, space.inner, root.url);
|
|
202
210
|
await this._metadataStore.addSpace(metadata);
|
|
@@ -211,6 +219,61 @@ export class DataSpaceManager {
|
|
|
211
219
|
return space;
|
|
212
220
|
}
|
|
213
221
|
|
|
222
|
+
async isDefaultSpace(space: DataSpace): Promise<boolean> {
|
|
223
|
+
if (!space.databaseRoot) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
switch (space.databaseRoot.getVersion()) {
|
|
227
|
+
case SpaceDocVersion.CURRENT: {
|
|
228
|
+
const [_, properties] = findInlineObjectOfType(space.databaseRoot.docSync()!, TYPE_PROPERTIES) ?? [];
|
|
229
|
+
return properties?.data?.[DEFAULT_SPACE_KEY] === this._signingContext.identityKey.toHex();
|
|
230
|
+
}
|
|
231
|
+
case SpaceDocVersion.LEGACY: {
|
|
232
|
+
const convertedDoc = await convertLegacyReferences(space.databaseRoot.docSync()!);
|
|
233
|
+
const [_, properties] = findInlineObjectOfType(convertedDoc, LEGACY_TYPE_PROPERTIES) ?? [];
|
|
234
|
+
return properties?.data?.[DEFAULT_SPACE_KEY] === this._signingContext.identityKey.toHex();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
default:
|
|
238
|
+
log.warn('unknown space version', { version: space.databaseRoot.getVersion(), spaceId: space.id });
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async createDefaultSpace() {
|
|
244
|
+
const space = await this.createSpace();
|
|
245
|
+
const document = await this._getSpaceRootDocument(space);
|
|
246
|
+
|
|
247
|
+
// TODO(dmaretskyi): Better API for low-level data access.
|
|
248
|
+
const properties: ObjectStructure = {
|
|
249
|
+
system: {
|
|
250
|
+
type: encodeReference(getTypeReference(PropertiesType)!),
|
|
251
|
+
},
|
|
252
|
+
data: {
|
|
253
|
+
[DEFAULT_SPACE_KEY]: this._signingContext.identityKey.toHex(),
|
|
254
|
+
},
|
|
255
|
+
meta: {
|
|
256
|
+
keys: [],
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const propertiesId = generateEchoId();
|
|
261
|
+
document.change((doc: SpaceDoc) => {
|
|
262
|
+
assignDeep(doc, ['objects', propertiesId], properties);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await this._echoHost.flush();
|
|
266
|
+
return space;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private async _getSpaceRootDocument(space: DataSpace): Promise<DocHandle<SpaceDoc>> {
|
|
270
|
+
const automergeIndex = space.automergeSpaceState.rootUrl;
|
|
271
|
+
invariant(automergeIndex);
|
|
272
|
+
const document = this._echoHost.automergeRepo.find<SpaceDoc>(automergeIndex as any);
|
|
273
|
+
await document.whenReady();
|
|
274
|
+
return document;
|
|
275
|
+
}
|
|
276
|
+
|
|
214
277
|
// TODO(burdon): Rename join space.
|
|
215
278
|
@synchronized
|
|
216
279
|
async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
|
|
@@ -226,6 +289,7 @@ export class DataSpaceManager {
|
|
|
226
289
|
};
|
|
227
290
|
|
|
228
291
|
const space = await this._constructSpace(metadata);
|
|
292
|
+
await space.open();
|
|
229
293
|
await this._metadataStore.addSpace(metadata);
|
|
230
294
|
space.initializeDataPipelineAsync();
|
|
231
295
|
|
|
@@ -233,6 +297,35 @@ export class DataSpaceManager {
|
|
|
233
297
|
return space;
|
|
234
298
|
}
|
|
235
299
|
|
|
300
|
+
async admitMember(options: AdmitMemberOptions): Promise<Credential> {
|
|
301
|
+
const space = this._spaceManager.spaces.get(options.spaceKey);
|
|
302
|
+
invariant(space);
|
|
303
|
+
|
|
304
|
+
if (space.spaceState.getMemberRole(options.identityKey) !== SpaceMember.Role.REMOVED) {
|
|
305
|
+
throw new AlreadyJoinedError();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// TODO(burdon): Check if already admitted.
|
|
309
|
+
const credentials: FeedMessage.Payload[] = await createAdmissionCredentials(
|
|
310
|
+
this._signingContext.credentialSigner,
|
|
311
|
+
options.identityKey,
|
|
312
|
+
space.key,
|
|
313
|
+
space.genesisFeedKey,
|
|
314
|
+
options.role,
|
|
315
|
+
space.spaceState.membershipChainHeads,
|
|
316
|
+
options.profile,
|
|
317
|
+
options.delegationCredentialId,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// TODO(dmaretskyi): Refactor.
|
|
321
|
+
invariant(credentials[0].credential);
|
|
322
|
+
const spaceMemberCredential = credentials[0].credential.credential;
|
|
323
|
+
invariant(getCredentialAssertion(spaceMemberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
|
|
324
|
+
await writeMessages(space.controlPipeline.writer, credentials);
|
|
325
|
+
|
|
326
|
+
return spaceMemberCredential;
|
|
327
|
+
}
|
|
328
|
+
|
|
236
329
|
/**
|
|
237
330
|
* Wait until the space data pipeline is fully initialized.
|
|
238
331
|
* Used by invitation handler.
|
|
@@ -248,14 +341,27 @@ export class DataSpaceManager {
|
|
|
248
341
|
);
|
|
249
342
|
}
|
|
250
343
|
|
|
344
|
+
public async requestSpaceAdmissionCredential(spaceKey: PublicKey): Promise<Credential> {
|
|
345
|
+
return this._spaceManager.requestSpaceAdmissionCredential({
|
|
346
|
+
spaceKey,
|
|
347
|
+
identityKey: this._signingContext.identityKey,
|
|
348
|
+
timeout: 15_000,
|
|
349
|
+
swarmIdentity: {
|
|
350
|
+
peerKey: this._signingContext.deviceKey,
|
|
351
|
+
credentialProvider: createAuthProvider(this._signingContext.credentialSigner),
|
|
352
|
+
credentialAuthenticator: async () => true,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
251
357
|
private async _constructSpace(metadata: SpaceMetadata) {
|
|
252
358
|
log('construct space', { metadata });
|
|
253
359
|
const gossip = new Gossip({
|
|
254
360
|
localPeerId: this._signingContext.deviceKey,
|
|
255
361
|
});
|
|
256
362
|
const presence = new Presence({
|
|
257
|
-
announceInterval: this.
|
|
258
|
-
offlineTimeout: this.
|
|
363
|
+
announceInterval: this._params?.spaceMemberPresenceAnnounceInterval ?? PRESENCE_ANNOUNCE_INTERVAL,
|
|
364
|
+
offlineTimeout: this._params?.spaceMemberPresenceOfflineTimeout ?? PRESENCE_OFFLINE_TIMEOUT,
|
|
259
365
|
identityKey: this._signingContext.identityKey,
|
|
260
366
|
gossip,
|
|
261
367
|
});
|
|
@@ -277,6 +383,7 @@ export class DataSpaceManager {
|
|
|
277
383
|
credentialAuthenticator: deferFunction(() => dataSpace.authVerifier.verifier),
|
|
278
384
|
},
|
|
279
385
|
onAuthorizedConnection: (session) => {
|
|
386
|
+
session.addExtension('dxos.mesh.teleport.admission-discovery', new CredentialServerExtension(space));
|
|
280
387
|
session.addExtension(
|
|
281
388
|
'dxos.mesh.teleport.gossip',
|
|
282
389
|
gossip.createExtension({ remotePeerId: session.remotePeerId }),
|
|
@@ -336,10 +443,6 @@ export class DataSpaceManager {
|
|
|
336
443
|
}
|
|
337
444
|
});
|
|
338
445
|
|
|
339
|
-
if (metadata.state !== SpaceState.INACTIVE) {
|
|
340
|
-
await dataSpace.open();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
446
|
if (metadata.controlTimeframe) {
|
|
344
447
|
dataSpace.inner.controlPipeline.state.setTargetTimeframe(metadata.controlTimeframe);
|
|
345
448
|
}
|
|
@@ -2,27 +2,25 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Event,
|
|
5
|
+
import { Event, Mutex, scheduleTask, sleep, synchronized, trackLeaks } from '@dxos/async';
|
|
6
6
|
import { AUTH_TIMEOUT } from '@dxos/client-protocol';
|
|
7
7
|
import { Context, ContextDisposedError, cancelWithContext } from '@dxos/context';
|
|
8
|
+
import type { SpecificCredential } from '@dxos/credentials';
|
|
8
9
|
import { timed, warnAfterTimeout } from '@dxos/debug';
|
|
9
|
-
import { type EchoHost } from '@dxos/echo-db';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
createIdFromSpaceKey,
|
|
13
|
-
createMappedFeedWriter,
|
|
14
|
-
type MetadataStore,
|
|
15
|
-
type Space,
|
|
16
|
-
} from '@dxos/echo-pipeline';
|
|
17
|
-
import { type ObjectStructure, type SpaceDoc } from '@dxos/echo-protocol';
|
|
18
|
-
import { TYPE_PROPERTIES } from '@dxos/echo-schema';
|
|
10
|
+
import { type EchoHost, type DatabaseRoot } from '@dxos/echo-db';
|
|
11
|
+
import { createMappedFeedWriter, type MetadataStore, type Space } from '@dxos/echo-pipeline';
|
|
12
|
+
import { SpaceDocVersion } from '@dxos/echo-protocol';
|
|
19
13
|
import { type FeedStore } from '@dxos/feed-store';
|
|
20
|
-
import { failedInvariant
|
|
14
|
+
import { failedInvariant } from '@dxos/invariant';
|
|
21
15
|
import { type Keyring } from '@dxos/keyring';
|
|
22
16
|
import { PublicKey } from '@dxos/keys';
|
|
23
17
|
import { log } from '@dxos/log';
|
|
24
18
|
import { CancelledError, SystemError } from '@dxos/protocols';
|
|
25
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type CreateEpochRequest,
|
|
21
|
+
SpaceState,
|
|
22
|
+
type Space as SpaceProto,
|
|
23
|
+
} from '@dxos/protocols/proto/dxos/client/services';
|
|
26
24
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
27
25
|
import { type SpaceCache } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
28
26
|
import {
|
|
@@ -36,10 +34,11 @@ import { type GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gos
|
|
|
36
34
|
import { type Gossip, type Presence } from '@dxos/teleport-extension-gossip';
|
|
37
35
|
import { Timeframe } from '@dxos/timeframe';
|
|
38
36
|
import { trace } from '@dxos/tracing';
|
|
39
|
-
import { ComplexSet
|
|
37
|
+
import { ComplexSet } from '@dxos/util';
|
|
40
38
|
|
|
41
39
|
import { AutomergeSpaceState } from './automerge-space-state';
|
|
42
40
|
import { type SigningContext } from './data-space-manager';
|
|
41
|
+
import { runEpochMigration } from './epoch-migrations';
|
|
43
42
|
import { NotarizationPlugin } from './notarization-plugin';
|
|
44
43
|
import { TrustedKeySetAuthVerifier } from '../identity';
|
|
45
44
|
|
|
@@ -100,8 +99,12 @@ export class DataSpace {
|
|
|
100
99
|
// TODO(dmaretskyi): Move into Space?
|
|
101
100
|
private readonly _automergeSpaceState = new AutomergeSpaceState((rootUrl) => this._onNewAutomergeRoot(rootUrl));
|
|
102
101
|
|
|
102
|
+
private readonly _epochProcessingMutex = new Mutex();
|
|
103
|
+
|
|
103
104
|
private _state = SpaceState.CLOSED;
|
|
104
105
|
|
|
106
|
+
private _databaseRoot: DatabaseRoot | null = null;
|
|
107
|
+
|
|
105
108
|
/**
|
|
106
109
|
* Error for _state === SpaceState.ERROR.
|
|
107
110
|
*/
|
|
@@ -183,6 +186,10 @@ export class DataSpace {
|
|
|
183
186
|
return this._automergeSpaceState;
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
get databaseRoot(): DatabaseRoot | null {
|
|
190
|
+
return this._databaseRoot;
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
@trace.info({ depth: null })
|
|
187
194
|
private get _automergeInfo() {
|
|
188
195
|
return {
|
|
@@ -193,13 +200,17 @@ export class DataSpace {
|
|
|
193
200
|
|
|
194
201
|
@synchronized
|
|
195
202
|
async open() {
|
|
196
|
-
|
|
203
|
+
if (this._state === SpaceState.CLOSED) {
|
|
204
|
+
await this._open();
|
|
205
|
+
}
|
|
197
206
|
}
|
|
198
207
|
|
|
199
208
|
private async _open() {
|
|
209
|
+
await this._presence.open();
|
|
200
210
|
await this._gossip.open();
|
|
201
211
|
await this._notarizationPlugin.open();
|
|
202
212
|
await this._inner.spaceState.addCredentialProcessor(this._notarizationPlugin);
|
|
213
|
+
await this._automergeSpaceState.open();
|
|
203
214
|
await this._inner.spaceState.addCredentialProcessor(this._automergeSpaceState);
|
|
204
215
|
await this._inner.open(new Context());
|
|
205
216
|
this._state = SpaceState.CONTROL_ONLY;
|
|
@@ -225,10 +236,11 @@ export class DataSpace {
|
|
|
225
236
|
|
|
226
237
|
await this._inner.close();
|
|
227
238
|
await this._inner.spaceState.removeCredentialProcessor(this._automergeSpaceState);
|
|
239
|
+
await this._automergeSpaceState.close();
|
|
228
240
|
await this._inner.spaceState.removeCredentialProcessor(this._notarizationPlugin);
|
|
229
241
|
await this._notarizationPlugin.close();
|
|
230
242
|
|
|
231
|
-
await this._presence.
|
|
243
|
+
await this._presence.close();
|
|
232
244
|
await this._gossip.close();
|
|
233
245
|
}
|
|
234
246
|
|
|
@@ -279,12 +291,15 @@ export class DataSpace {
|
|
|
279
291
|
// Allow other tasks to run before loading the data pipeline.
|
|
280
292
|
await sleep(1);
|
|
281
293
|
|
|
294
|
+
const ready = this.stateUpdate.waitForCondition(() => this._state === SpaceState.READY);
|
|
295
|
+
|
|
282
296
|
this._automergeSpaceState.startProcessingRootDocs();
|
|
283
297
|
|
|
284
|
-
//
|
|
285
|
-
await
|
|
298
|
+
// TODO(dmaretskyi): Change so `initializeDataPipeline` doesn't wait for the space to be READY, but rather any state with a valid root.
|
|
299
|
+
await ready;
|
|
300
|
+
}
|
|
286
301
|
|
|
287
|
-
|
|
302
|
+
private async _enterReadyState() {
|
|
288
303
|
await this._callbacks.beforeReady?.();
|
|
289
304
|
|
|
290
305
|
this._state = SpaceState.READY;
|
|
@@ -371,11 +386,10 @@ export class DataSpace {
|
|
|
371
386
|
|
|
372
387
|
private _onNewAutomergeRoot(rootUrl: string) {
|
|
373
388
|
log('loading automerge root doc for space', { space: this.key, rootUrl });
|
|
374
|
-
|
|
375
|
-
// Workaround for https://github.com/automerge/automerge-repo/pull/292
|
|
376
|
-
this._echoHost.replicateDocument(rootUrl);
|
|
389
|
+
|
|
377
390
|
const handle = this._echoHost.automergeRepo.find(rootUrl as any);
|
|
378
391
|
|
|
392
|
+
// TODO(dmaretskyi): Make this single-threaded (but doc loading should still be parallel to not block epoch processing).
|
|
379
393
|
queueMicrotask(async () => {
|
|
380
394
|
try {
|
|
381
395
|
await warnAfterTimeout(5_000, 'Automerge root doc load timeout (DataSpace)', async () => {
|
|
@@ -385,6 +399,10 @@ export class DataSpace {
|
|
|
385
399
|
return;
|
|
386
400
|
}
|
|
387
401
|
|
|
402
|
+
// Ensure only one root is processed at a time.
|
|
403
|
+
using _guard = await this._epochProcessingMutex.acquire();
|
|
404
|
+
|
|
405
|
+
// Attaching space keys to legacy documents.
|
|
388
406
|
const doc = handle.docSync() ?? failedInvariant();
|
|
389
407
|
if (!doc.access?.spaceKey) {
|
|
390
408
|
handle.change((doc: any) => {
|
|
@@ -394,10 +412,17 @@ export class DataSpace {
|
|
|
394
412
|
|
|
395
413
|
// TODO(dmaretskyi): Close roots.
|
|
396
414
|
// TODO(dmaretskyi): How do we handle changing to the next EPOCH?
|
|
397
|
-
|
|
398
|
-
|
|
415
|
+
const root = await this._echoHost.openSpaceRoot(handle.url);
|
|
416
|
+
this._databaseRoot = root;
|
|
417
|
+
if (root.getVersion() !== SpaceDocVersion.CURRENT) {
|
|
418
|
+
if (this._state !== SpaceState.REQUIRES_MIGRATION) {
|
|
419
|
+
this._state = SpaceState.REQUIRES_MIGRATION;
|
|
420
|
+
this.stateUpdate.emit();
|
|
421
|
+
}
|
|
399
422
|
} else {
|
|
400
|
-
|
|
423
|
+
if (this._state !== SpaceState.READY) {
|
|
424
|
+
await this._enterReadyState();
|
|
425
|
+
}
|
|
401
426
|
}
|
|
402
427
|
} catch (err) {
|
|
403
428
|
if (err instanceof ContextDisposedError) {
|
|
@@ -420,131 +445,51 @@ export class DataSpace {
|
|
|
420
445
|
await this.inner.controlPipeline.writer.write({ credential: { credential } });
|
|
421
446
|
}
|
|
422
447
|
|
|
423
|
-
async createEpoch(options?: CreateEpochOptions) {
|
|
424
|
-
|
|
425
|
-
switch (options?.migration) {
|
|
426
|
-
case undefined:
|
|
427
|
-
case CreateEpochRequest.Migration.NONE:
|
|
428
|
-
{
|
|
429
|
-
// TODO(dmaretskyi): Unify epoch construction.
|
|
430
|
-
epoch = {
|
|
431
|
-
previousId: this._automergeSpaceState.lastEpoch?.id,
|
|
432
|
-
number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
|
|
433
|
-
timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
|
|
434
|
-
automergeRoot: this._automergeSpaceState.lastEpoch?.subject.assertion?.automergeRoot,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
break;
|
|
438
|
-
case CreateEpochRequest.Migration.INIT_AUTOMERGE:
|
|
439
|
-
{
|
|
440
|
-
const document = this._echoHost.automergeRepo.create();
|
|
441
|
-
// TODO(dmaretskyi): Unify epoch construction.
|
|
442
|
-
epoch = {
|
|
443
|
-
previousId: this._automergeSpaceState.lastEpoch?.id,
|
|
444
|
-
number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
|
|
445
|
-
timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
|
|
446
|
-
automergeRoot: document.url,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
break;
|
|
450
|
-
case CreateEpochRequest.Migration.PRUNE_AUTOMERGE_ROOT_HISTORY:
|
|
451
|
-
{
|
|
452
|
-
const currentRootUrl = this._automergeSpaceState.rootUrl;
|
|
453
|
-
const rootHandle = this._echoHost.automergeRepo.find(currentRootUrl as any);
|
|
454
|
-
await cancelWithContext(this._ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
|
|
455
|
-
const newRoot = this._echoHost.automergeRepo.create(rootHandle.docSync());
|
|
456
|
-
invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
|
|
457
|
-
// TODO(dmaretskyi): Unify epoch construction.
|
|
458
|
-
epoch = {
|
|
459
|
-
previousId: this._automergeSpaceState.lastEpoch?.id,
|
|
460
|
-
number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
|
|
461
|
-
timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
|
|
462
|
-
automergeRoot: newRoot.url,
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
break;
|
|
466
|
-
case CreateEpochRequest.Migration.FRAGMENT_AUTOMERGE_ROOT:
|
|
467
|
-
{
|
|
468
|
-
log.info('Fragmenting');
|
|
469
|
-
|
|
470
|
-
const currentRootUrl = this._automergeSpaceState.rootUrl;
|
|
471
|
-
const rootHandle = this._echoHost.automergeRepo.find<SpaceDoc>(currentRootUrl as any);
|
|
472
|
-
await cancelWithContext(this._ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
|
|
473
|
-
|
|
474
|
-
// Find properties object.
|
|
475
|
-
const objects = Object.entries((rootHandle.docSync() as SpaceDoc).objects!);
|
|
476
|
-
const properties = findPropertiesObject(rootHandle.docSync() as SpaceDoc);
|
|
477
|
-
const otherObjects = objects.filter(([key]) => key !== properties?.[0]);
|
|
478
|
-
invariant(properties, 'Properties not found');
|
|
479
|
-
|
|
480
|
-
// Create a new space doc with the properties object.
|
|
481
|
-
const newSpaceDoc: SpaceDoc = { ...rootHandle.docSync(), objects: Object.fromEntries([properties]) };
|
|
482
|
-
const newRoot = this._echoHost.automergeRepo.create(newSpaceDoc);
|
|
483
|
-
invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
|
|
484
|
-
|
|
485
|
-
// Create new automerge documents for all objects.
|
|
486
|
-
const docLoader = new AutomergeDocumentLoaderImpl(
|
|
487
|
-
await createIdFromSpaceKey(this.key),
|
|
488
|
-
this._echoHost.automergeRepo,
|
|
489
|
-
this.key,
|
|
490
|
-
);
|
|
491
|
-
await docLoader.loadSpaceRootDocHandle(this._ctx, { rootUrl: newRoot.url });
|
|
492
|
-
|
|
493
|
-
otherObjects.forEach(([key, value]) => {
|
|
494
|
-
const handle = docLoader.createDocumentForObject(key);
|
|
495
|
-
handle.change((doc: any) => {
|
|
496
|
-
assignDeep(doc, ['objects', key], value);
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
// TODO(mykola): Delete old root.
|
|
448
|
+
async createEpoch(options?: CreateEpochOptions): Promise<SpecificCredential<Epoch> | null> {
|
|
449
|
+
const ctx = this._ctx.derive();
|
|
501
450
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
|
|
506
|
-
timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
|
|
507
|
-
automergeRoot: newRoot.url,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
break;
|
|
511
|
-
case CreateEpochRequest.Migration.REPLACE_AUTOMERGE_ROOT:
|
|
512
|
-
{
|
|
513
|
-
invariant(options.newAutomergeRoot);
|
|
514
|
-
// TODO(dmaretskyi): Unify epoch construction.
|
|
515
|
-
epoch = {
|
|
516
|
-
previousId: this._automergeSpaceState.lastEpoch?.id,
|
|
517
|
-
number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
|
|
518
|
-
timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
|
|
519
|
-
automergeRoot: options.newAutomergeRoot,
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
break;
|
|
451
|
+
// Preserving existing behavior.
|
|
452
|
+
if (!options?.migration) {
|
|
453
|
+
return null;
|
|
523
454
|
}
|
|
524
455
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
456
|
+
const { newRoot } = await runEpochMigration(ctx, {
|
|
457
|
+
repo: this._echoHost.automergeRepo,
|
|
458
|
+
spaceId: this.id,
|
|
459
|
+
spaceKey: this.key,
|
|
460
|
+
migration: options.migration,
|
|
461
|
+
currentRoot: this._automergeSpaceState.rootUrl ?? null,
|
|
462
|
+
newAutomergeRoot: options.newAutomergeRoot,
|
|
463
|
+
});
|
|
528
464
|
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
465
|
+
const epoch: Epoch = {
|
|
466
|
+
previousId: this._automergeSpaceState.lastEpoch?.id,
|
|
467
|
+
number: (this._automergeSpaceState.lastEpoch?.subject.assertion.number ?? -1) + 1,
|
|
468
|
+
timeframe: this._automergeSpaceState.lastEpoch?.subject.assertion.timeframe ?? new Timeframe(),
|
|
469
|
+
automergeRoot: newRoot ?? this._automergeSpaceState.rootUrl,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const credential = (await this._signingContext.credentialSigner.createCredential({
|
|
473
|
+
subject: this.key,
|
|
474
|
+
assertion: {
|
|
475
|
+
'@type': 'dxos.halo.credentials.Epoch',
|
|
476
|
+
...epoch,
|
|
538
477
|
},
|
|
478
|
+
})) as SpecificCredential<Epoch>;
|
|
479
|
+
|
|
480
|
+
const receipt = await this.inner.controlPipeline.writer.write({
|
|
481
|
+
credential: { credential },
|
|
539
482
|
});
|
|
540
483
|
|
|
541
484
|
await this.inner.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
|
|
542
485
|
await this._echoHost.updateIndexes();
|
|
486
|
+
|
|
487
|
+
return credential;
|
|
543
488
|
}
|
|
544
489
|
|
|
545
490
|
@synchronized
|
|
546
491
|
async activate() {
|
|
547
|
-
if (
|
|
492
|
+
if (![SpaceState.CLOSED, SpaceState.INACTIVE].includes(this._state)) {
|
|
548
493
|
return;
|
|
549
494
|
}
|
|
550
495
|
|
|
@@ -558,25 +503,13 @@ export class DataSpace {
|
|
|
558
503
|
if (this._state === SpaceState.INACTIVE) {
|
|
559
504
|
return;
|
|
560
505
|
}
|
|
561
|
-
|
|
562
506
|
// Unregister from data service.
|
|
563
507
|
await this._metadataStore.setSpaceState(this.key, SpaceState.INACTIVE);
|
|
564
|
-
|
|
508
|
+
if (this._state !== SpaceState.CLOSED) {
|
|
509
|
+
await this._close();
|
|
510
|
+
}
|
|
565
511
|
this._state = SpaceState.INACTIVE;
|
|
566
512
|
log('new state', { state: SpaceState[this._state] });
|
|
567
513
|
this.stateUpdate.emit();
|
|
568
514
|
}
|
|
569
515
|
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Assumes properties are at root.
|
|
573
|
-
*/
|
|
574
|
-
export const findPropertiesObject = (spaceDoc: SpaceDoc): [string, ObjectStructure] | undefined => {
|
|
575
|
-
for (const id in spaceDoc.objects ?? {}) {
|
|
576
|
-
const obj = spaceDoc.objects![id];
|
|
577
|
-
if (obj.system.type?.itemId === TYPE_PROPERTIES) {
|
|
578
|
-
return [id, obj];
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return undefined;
|
|
582
|
-
};
|