@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.
Files changed (66) hide show
  1. package/dist/lib/browser/{chunk-LKSDZ2AB.mjs → chunk-H3XJK6ZN.mjs} +1632 -966
  2. package/dist/lib/browser/chunk-H3XJK6ZN.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +13 -4
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +20 -13
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-FBXXJHAL.cjs → chunk-JRDM7NQS.cjs} +1868 -1209
  9. package/dist/lib/node/chunk-JRDM7NQS.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +53 -44
  11. package/dist/lib/node/index.cjs.map +1 -1
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +26 -19
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/identity/contacts-service.d.ts +14 -0
  16. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -0
  17. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts +19 -0
  18. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts.map +1 -0
  19. package/dist/types/src/packlets/identity/identity-service.d.ts +14 -7
  20. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  21. package/dist/types/src/packlets/identity/identity.d.ts +4 -1
  22. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  23. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  24. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  25. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  26. package/dist/types/src/packlets/services/service-host.d.ts +1 -1
  27. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  28. package/dist/types/src/packlets/spaces/automerge-space-state.d.ts +4 -1
  29. package/dist/types/src/packlets/spaces/automerge-space-state.d.ts.map +1 -1
  30. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +15 -4
  31. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  32. package/dist/types/src/packlets/spaces/data-space.d.ts +9 -9
  33. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  34. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +23 -0
  35. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -0
  36. package/dist/types/src/packlets/spaces/spaces-service.d.ts +5 -2
  37. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  38. package/dist/types/src/packlets/storage/index.d.ts +1 -0
  39. package/dist/types/src/packlets/storage/index.d.ts.map +1 -1
  40. package/dist/types/src/packlets/storage/profile-archive.d.ts +14 -0
  41. package/dist/types/src/packlets/storage/profile-archive.d.ts.map +1 -0
  42. package/dist/types/src/packlets/testing/test-builder.d.ts +8 -6
  43. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  44. package/dist/types/src/version.d.ts +1 -1
  45. package/package.json +36 -36
  46. package/src/packlets/identity/contacts-service.ts +85 -0
  47. package/src/packlets/identity/default-space-state-machine.ts +44 -0
  48. package/src/packlets/identity/identity-service.test.ts +35 -5
  49. package/src/packlets/identity/identity-service.ts +76 -8
  50. package/src/packlets/identity/identity.ts +25 -2
  51. package/src/packlets/invitations/invitations-handler.ts +13 -5
  52. package/src/packlets/invitations/space-invitation-protocol.ts +11 -32
  53. package/src/packlets/services/service-context.ts +1 -4
  54. package/src/packlets/services/service-host.ts +23 -42
  55. package/src/packlets/spaces/automerge-space-state.ts +11 -2
  56. package/src/packlets/spaces/data-space-manager.test.ts +46 -1
  57. package/src/packlets/spaces/data-space-manager.ts +136 -33
  58. package/src/packlets/spaces/data-space.ts +85 -152
  59. package/src/packlets/spaces/epoch-migrations.ts +135 -0
  60. package/src/packlets/spaces/spaces-service.ts +54 -4
  61. package/src/packlets/storage/index.ts +1 -0
  62. package/src/packlets/storage/profile-archive.ts +97 -0
  63. package/src/packlets/testing/test-builder.ts +12 -10
  64. package/src/version.ts +1 -1
  65. package/dist/lib/browser/chunk-LKSDZ2AB.mjs.map +0 -7
  66. 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 { cancelWithContext, Context } from '@dxos/context';
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
- getCredentialAssertion,
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 { type SpaceDoc } from '@dxos/echo-protocol';
25
- import { type FeedStore } from '@dxos/feed-store';
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, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
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, findPropertiesObject } from './data-space';
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
- params?: DataSpaceManagerRuntimeParams,
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 && findPropertiesObject(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._spaceMemberPresenceAnnounceInterval,
258
- offlineTimeout: this._spaceMemberPresenceOfflineTimeout,
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, asyncTimeout, scheduleTask, sleep, synchronized, trackLeaks } from '@dxos/async';
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
- AutomergeDocumentLoaderImpl,
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, invariant } from '@dxos/invariant';
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 { CreateEpochRequest, SpaceState, type Space as SpaceProto } from '@dxos/protocols/proto/dxos/client/services';
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, assignDeep } from '@dxos/util';
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
- await this._open();
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.destroy();
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
- // Wait for the first epoch.
285
- await cancelWithContext(this._ctx, this.automergeSpaceState.ensureEpochInitialized());
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
- log('data pipeline ready');
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
- // Override share policy = true for the root document.
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
- if (!this._echoHost.roots.has(handle.documentId)) {
398
- await this._echoHost.openSpaceRoot(handle.url);
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
- log.warn('echo database root already exists', { space: this.key, rootUrl });
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
- let epoch: Epoch | undefined;
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
- // TODO(dmaretskyi): Unify epoch construction.
503
- epoch = {
504
- previousId: this._automergeSpaceState.lastEpoch?.id,
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
- if (!epoch) {
526
- return;
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 receipt = await this.inner.controlPipeline.writer.write({
530
- credential: {
531
- credential: await this._signingContext.credentialSigner.createCredential({
532
- subject: this.key,
533
- assertion: {
534
- '@type': 'dxos.halo.credentials.Epoch',
535
- ...epoch,
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 (this._state !== SpaceState.INACTIVE) {
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
- await this._close();
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
- };