@dxos/client-services 0.5.9-main.b89a78b → 0.5.9-main.b8d8fee

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 (39) hide show
  1. package/dist/lib/browser/{chunk-SMW6LWWU.mjs → chunk-2EFXBSRZ.mjs} +731 -589
  2. package/dist/lib/browser/chunk-2EFXBSRZ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/packlets/testing/index.mjs +10 -9
  6. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/{chunk-D6I2MQJI.cjs → chunk-2SS7JAIR.cjs} +947 -809
  8. package/dist/lib/node/chunk-2SS7JAIR.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +42 -42
  10. package/dist/lib/node/meta.json +1 -1
  11. package/dist/lib/node/packlets/testing/index.cjs +16 -15
  12. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  13. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts +19 -0
  14. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts.map +1 -0
  15. package/dist/types/src/packlets/identity/identity-service.d.ts +14 -7
  16. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  17. package/dist/types/src/packlets/identity/identity.d.ts +4 -1
  18. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  19. package/dist/types/src/packlets/services/service-host.d.ts +1 -1
  20. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  21. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +5 -3
  22. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  23. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  24. package/dist/types/src/packlets/testing/test-builder.d.ts +8 -6
  25. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  26. package/dist/types/src/version.d.ts +1 -1
  27. package/package.json +36 -36
  28. package/src/packlets/identity/default-space-state-machine.ts +44 -0
  29. package/src/packlets/identity/identity-service.test.ts +35 -5
  30. package/src/packlets/identity/identity-service.ts +50 -8
  31. package/src/packlets/identity/identity.ts +25 -2
  32. package/src/packlets/services/service-host.ts +13 -40
  33. package/src/packlets/spaces/data-space-manager.test.ts +46 -1
  34. package/src/packlets/spaces/data-space-manager.ts +54 -25
  35. package/src/packlets/spaces/data-space.ts +10 -5
  36. package/src/packlets/testing/test-builder.ts +11 -6
  37. package/src/version.ts +1 -1
  38. package/dist/lib/browser/chunk-SMW6LWWU.mjs.map +0 -7
  39. package/dist/lib/node/chunk-D6I2MQJI.cjs.map +0 -7
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { type CredentialProcessor, getCredentialAssertion } from '@dxos/credentials';
6
+ import { SpaceId, type PublicKey } from '@dxos/keys';
7
+ import { log } from '@dxos/log';
8
+ import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
9
+
10
+ type DefaultSpaceStateMachineParams = {
11
+ identityKey: PublicKey;
12
+ onUpdate?: () => void;
13
+ };
14
+
15
+ /**
16
+ * Processes device invitation credentials.
17
+ */
18
+ export class DefaultSpaceStateMachine implements CredentialProcessor {
19
+ private _spaceId: SpaceId | undefined;
20
+
21
+ constructor(private readonly _params: DefaultSpaceStateMachineParams) {}
22
+
23
+ public get spaceId(): SpaceId | undefined {
24
+ return this._spaceId;
25
+ }
26
+
27
+ async processCredential(credential: Credential) {
28
+ const assertion = getCredentialAssertion(credential);
29
+ switch (assertion['@type']) {
30
+ case 'dxos.halo.credentials.DefaultSpace': {
31
+ if (!credential.subject.id.equals(this._params.identityKey)) {
32
+ log.warn('Invalid default space credential', { expectedIdentity: this._params.identityKey, credential });
33
+ return;
34
+ }
35
+ if (!SpaceId.isValid(assertion.spaceId)) {
36
+ log.warn('Invalid default space id', { id: assertion.spaceId });
37
+ return;
38
+ }
39
+ this._spaceId = assertion.spaceId;
40
+ break;
41
+ }
42
+ }
43
+ }
44
+ }
@@ -24,11 +24,7 @@ describe('IdentityService', () => {
24
24
  beforeEach(async () => {
25
25
  serviceContext = await createServiceContext();
26
26
  await serviceContext.open(new Context());
27
- identityService = new IdentityServiceImpl(
28
- (options) => serviceContext.createIdentity(options),
29
- serviceContext.identityManager,
30
- serviceContext.keyring,
31
- );
27
+ identityService = createIdentityService(serviceContext);
32
28
  });
33
29
 
34
30
  afterEach(async () => {
@@ -95,3 +91,37 @@ describe('IdentityService', () => {
95
91
  });
96
92
  });
97
93
  });
94
+
95
+ describe('open', () => {
96
+ test('identity without default space fixed', async () => {
97
+ const serviceContext = await createServiceContext();
98
+ await serviceContext.open(new Context());
99
+ const identity = await serviceContext.createIdentity();
100
+ const identityService = createIdentityService(serviceContext);
101
+ const getDataSpaces = () => [...(serviceContext.dataSpaceManager?.spaces?.values() ?? [])];
102
+ expect(getDataSpaces().length).to.eq(0);
103
+ expect(identity.defaultSpaceId).to.be.undefined;
104
+ await identityService.open();
105
+ expect(getDataSpaces()[0].id === identity.defaultSpaceId).to.be.true;
106
+ });
107
+
108
+ test('identity without default space credential fixed', async () => {
109
+ const serviceContext = await createServiceContext();
110
+ await serviceContext.open(new Context());
111
+ const identity = await serviceContext.createIdentity();
112
+ const space = await serviceContext.dataSpaceManager!.createDefaultSpace();
113
+ const identityService = createIdentityService(serviceContext);
114
+ expect(identity.defaultSpaceId).to.be.undefined;
115
+ await identityService.open();
116
+ expect(identity.defaultSpaceId === space.id).to.be.true;
117
+ });
118
+ });
119
+
120
+ const createIdentityService = (serviceContext: ServiceContext) => {
121
+ return new IdentityServiceImpl(
122
+ serviceContext.identityManager,
123
+ serviceContext.keyring,
124
+ () => serviceContext.dataSpaceManager!,
125
+ (options) => serviceContext.createIdentity(options),
126
+ );
127
+ };
@@ -3,36 +3,59 @@
3
3
  //
4
4
 
5
5
  import { Stream } from '@dxos/codec-protobuf';
6
+ import { Resource } from '@dxos/context';
6
7
  import { signPresentation } from '@dxos/credentials';
7
8
  import { todo } from '@dxos/debug';
8
9
  import { invariant } from '@dxos/invariant';
9
10
  import { type Keyring } from '@dxos/keyring';
10
11
  import {
11
12
  type CreateIdentityRequest,
12
- type Identity,
13
+ type Identity as IdentityProto,
13
14
  type IdentityService,
14
15
  type QueryIdentityResponse,
15
16
  type RecoverIdentityRequest,
16
17
  type SignPresentationRequest,
18
+ SpaceState,
17
19
  } from '@dxos/protocols/proto/dxos/client/services';
18
20
  import { type Presentation, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
19
21
 
22
+ import { type Identity } from './identity';
20
23
  import { type CreateIdentityOptions, type IdentityManager } from './identity-manager';
24
+ import { type DataSpaceManager } from '../spaces';
21
25
 
22
- export class IdentityServiceImpl implements IdentityService {
26
+ export class IdentityServiceImpl extends Resource implements IdentityService {
23
27
  constructor(
24
- private readonly _createIdentity: (params: CreateIdentityOptions) => Promise<Identity>,
25
28
  private readonly _identityManager: IdentityManager,
26
29
  private readonly _keyring: Keyring,
30
+ private readonly _dataSpaceManagerProvider: () => DataSpaceManager,
31
+ private readonly _createIdentity: (params: CreateIdentityOptions) => Promise<Identity>,
27
32
  private readonly _onProfileUpdate?: (profile: ProfileDocument | undefined) => Promise<void>,
28
- ) {}
33
+ ) {
34
+ super();
35
+ }
36
+
37
+ protected override async _open() {
38
+ const identity = this._identityManager.identity;
39
+ if (identity && !identity.defaultSpaceId) {
40
+ await this._fixIdentityWithoutDefaultSpace(identity);
41
+ }
42
+ }
29
43
 
30
- async createIdentity(request: CreateIdentityRequest): Promise<Identity> {
44
+ async createIdentity(request: CreateIdentityRequest): Promise<IdentityProto> {
31
45
  await this._createIdentity({ displayName: request.profile?.displayName, deviceProfile: request.deviceProfile });
46
+ const dataSpaceManager = this._dataSpaceManagerProvider();
47
+ await this._createDefaultSpace(dataSpaceManager);
32
48
  return this._getIdentity()!;
33
49
  }
34
50
 
35
- async recoverIdentity(request: RecoverIdentityRequest): Promise<Identity> {
51
+ private async _createDefaultSpace(dataSpaceManager: DataSpaceManager) {
52
+ const space = await dataSpaceManager!.createDefaultSpace();
53
+ const identity = this._identityManager.identity;
54
+ invariant(identity);
55
+ await identity.updateDefaultSpace(space.id);
56
+ }
57
+
58
+ async recoverIdentity(request: RecoverIdentityRequest): Promise<IdentityProto> {
36
59
  return todo();
37
60
  }
38
61
 
@@ -45,7 +68,7 @@ export class IdentityServiceImpl implements IdentityService {
45
68
  });
46
69
  }
47
70
 
48
- private _getIdentity(): Identity | undefined {
71
+ private _getIdentity(): IdentityProto | undefined {
49
72
  if (!this._identityManager.identity) {
50
73
  return undefined;
51
74
  }
@@ -57,7 +80,7 @@ export class IdentityServiceImpl implements IdentityService {
57
80
  };
58
81
  }
59
82
 
60
- async updateProfile(profile: ProfileDocument): Promise<Identity> {
83
+ async updateProfile(profile: ProfileDocument): Promise<IdentityProto> {
61
84
  invariant(this._identityManager.identity, 'Identity not initialized.');
62
85
  await this._identityManager.updateProfile(profile);
63
86
  await this._onProfileUpdate?.(this._identityManager.identity.profileDocument);
@@ -75,4 +98,23 @@ export class IdentityServiceImpl implements IdentityService {
75
98
  nonce,
76
99
  });
77
100
  }
101
+
102
+ private async _fixIdentityWithoutDefaultSpace(identity: Identity) {
103
+ let hasDefaultSpace = false;
104
+ const dataSpaceManager = this._dataSpaceManagerProvider();
105
+ for (const space of dataSpaceManager.spaces.values()) {
106
+ if (space.state === SpaceState.CLOSED) {
107
+ await space.open();
108
+ await space.initializeDataPipeline();
109
+ }
110
+ if (await dataSpaceManager.isDefaultSpace(space)) {
111
+ await identity.updateDefaultSpace(space.id);
112
+ hasDefaultSpace = true;
113
+ break;
114
+ }
115
+ }
116
+ if (!hasDefaultSpace) {
117
+ await this._createDefaultSpace(dataSpaceManager);
118
+ }
119
+ }
78
120
  }
@@ -16,7 +16,7 @@ import { type Signer } from '@dxos/crypto';
16
16
  import { type Space } from '@dxos/echo-pipeline';
17
17
  import { writeMessages } from '@dxos/feed-store';
18
18
  import { invariant } from '@dxos/invariant';
19
- import { PublicKey } from '@dxos/keys';
19
+ import { PublicKey, type SpaceId } from '@dxos/keys';
20
20
  import { log } from '@dxos/log';
21
21
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
22
  import {
@@ -26,10 +26,12 @@ import {
26
26
  } from '@dxos/protocols/proto/dxos/halo/credentials';
27
27
  import { type DeviceAdmissionRequest } from '@dxos/protocols/proto/dxos/halo/invitations';
28
28
  import { type Presence } from '@dxos/teleport-extension-gossip';
29
+ import { Timeframe } from '@dxos/timeframe';
29
30
  import { trace } from '@dxos/tracing';
30
31
  import { type ComplexMap, ComplexSet } from '@dxos/util';
31
32
 
32
33
  import { TrustedKeySetAuthVerifier } from './authenticator';
34
+ import { DefaultSpaceStateMachine } from './default-space-state-machine';
33
35
 
34
36
  export type IdentityParams = {
35
37
  identityKey: PublicKey;
@@ -49,6 +51,7 @@ export class Identity {
49
51
  private readonly _presence?: Presence;
50
52
  private readonly _deviceStateMachine: DeviceStateMachine;
51
53
  private readonly _profileStateMachine: ProfileStateMachine;
54
+ private readonly _defaultSpaceStateMachine: DefaultSpaceStateMachine;
52
55
  public readonly authVerifier: TrustedKeySetAuthVerifier;
53
56
 
54
57
  public readonly identityKey: PublicKey;
@@ -75,6 +78,10 @@ export class Identity {
75
78
  identityKey: this.identityKey,
76
79
  onUpdate: () => this.stateUpdate.emit(),
77
80
  });
81
+ this._defaultSpaceStateMachine = new DefaultSpaceStateMachine({
82
+ identityKey: this.identityKey,
83
+ onUpdate: () => this.stateUpdate.emit(),
84
+ });
78
85
 
79
86
  this.authVerifier = new TrustedKeySetAuthVerifier({
80
87
  trustedKeysProvider: () => new ComplexSet(PublicKey.hash, this.authorizedDeviceKeys.keys()),
@@ -88,17 +95,24 @@ export class Identity {
88
95
  return this._deviceStateMachine.authorizedDeviceKeys;
89
96
  }
90
97
 
98
+ get defaultSpaceId(): SpaceId | undefined {
99
+ return this._defaultSpaceStateMachine.spaceId;
100
+ }
101
+
91
102
  @trace.span()
92
103
  async open(ctx: Context) {
104
+ await this._presence?.open();
93
105
  await this.space.spaceState.addCredentialProcessor(this._deviceStateMachine);
94
106
  await this.space.spaceState.addCredentialProcessor(this._profileStateMachine);
107
+ await this.space.spaceState.addCredentialProcessor(this._defaultSpaceStateMachine);
95
108
  await this.space.open(ctx);
96
109
  }
97
110
 
98
111
  @trace.span()
99
112
  async close(ctx: Context) {
100
- await this._presence?.destroy();
113
+ await this._presence?.close();
101
114
  await this.authVerifier.close();
115
+ await this.space.spaceState.removeCredentialProcessor(this._defaultSpaceStateMachine);
102
116
  await this.space.spaceState.removeCredentialProcessor(this._profileStateMachine);
103
117
  await this.space.spaceState.removeCredentialProcessor(this._deviceStateMachine);
104
118
  await this.space.close();
@@ -157,6 +171,15 @@ export class Identity {
157
171
  return createCredentialSignerWithKey(this._signer, this.deviceKey);
158
172
  }
159
173
 
174
+ async updateDefaultSpace(spaceId: SpaceId) {
175
+ const credential = await this.getDeviceCredentialSigner().createCredential({
176
+ subject: this.identityKey,
177
+ assertion: { '@type': 'dxos.halo.credentials.DefaultSpace', spaceId },
178
+ });
179
+ const receipt = await this.controlPipeline.writer.write({ credential: { credential } });
180
+ await this.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
181
+ }
182
+
160
183
  async admitDevice({ deviceKey, controlFeedKey, dataFeedKey }: DeviceAdmissionRequest) {
161
184
  log('Admitting device:', {
162
185
  identityKey: this.identityKey,
@@ -3,11 +3,9 @@
3
3
  //
4
4
 
5
5
  import { Event, synchronized } from '@dxos/async';
6
- import { clientServiceBundle, defaultKey, type ClientServices, PropertiesType } from '@dxos/client-protocol';
6
+ import { clientServiceBundle, type ClientServices } from '@dxos/client-protocol';
7
7
  import { type Config } from '@dxos/config';
8
8
  import { Context } from '@dxos/context';
9
- import { type ObjectStructure, encodeReference, type SpaceDoc } from '@dxos/echo-protocol';
10
- import { getTypeReference } from '@dxos/echo-schema';
11
9
  import { invariant } from '@dxos/invariant';
12
10
  import { PublicKey } from '@dxos/keys';
13
11
  import { type LevelDB } from '@dxos/kv-store';
@@ -18,7 +16,6 @@ import { trace } from '@dxos/protocols';
18
16
  import { SystemStatus } from '@dxos/protocols/proto/dxos/client/services';
19
17
  import { type Storage } from '@dxos/random-access-storage';
20
18
  import { TRACE_PROCESSOR, trace as Trace } from '@dxos/tracing';
21
- import { assignDeep } from '@dxos/util';
22
19
  import { WebsocketRpcClient } from '@dxos/websocket-rpc';
23
20
 
24
21
  import { ServiceContext, type ServiceContextRuntimeParams } from './service-context';
@@ -86,7 +83,7 @@ export class ClientServicesHost {
86
83
  private _devtoolsProxy?: WebsocketRpcClient<{}, ClientServices>;
87
84
 
88
85
  private _serviceContext!: ServiceContext;
89
- private readonly _runtimeParams?: ServiceContextRuntimeParams;
86
+ private readonly _runtimeParams: ServiceContextRuntimeParams;
90
87
  private diagnosticsBroadcastHandler: CollectDiagnosticsBroadcastHandler;
91
88
 
92
89
  @Trace.info()
@@ -109,7 +106,7 @@ export class ClientServicesHost {
109
106
  this._storage = storage;
110
107
  this._level = level;
111
108
  this._callbacks = callbacks;
112
- this._runtimeParams = runtimeParams;
109
+ this._runtimeParams = runtimeParams ?? {};
113
110
 
114
111
  if (config) {
115
112
  this.initialize({ config, transportFactory, signalManager });
@@ -254,15 +251,17 @@ export class ClientServicesHost {
254
251
  this._runtimeParams,
255
252
  );
256
253
 
254
+ const identityService = new IdentityServiceImpl(
255
+ this._serviceContext.identityManager,
256
+ this._serviceContext.keyring,
257
+ () => this._serviceContext.dataSpaceManager!,
258
+ (params) => this._createIdentity(params),
259
+ (profile) => this._serviceContext.broadcastProfileUpdate(profile),
260
+ );
261
+
257
262
  this._serviceRegistry.setServices({
258
263
  SystemService: this._systemService,
259
-
260
- IdentityService: new IdentityServiceImpl(
261
- (params) => this._createIdentity(params),
262
- this._serviceContext.identityManager,
263
- this._serviceContext.keyring,
264
- (profile) => this._serviceContext.broadcastProfileUpdate(profile),
265
- ),
264
+ IdentityService: identityService,
266
265
 
267
266
  InvitationsService: new InvitationsServiceImpl(this._serviceContext.invitationsManager),
268
267
 
@@ -294,6 +293,7 @@ export class ClientServicesHost {
294
293
  });
295
294
 
296
295
  await this._serviceContext.open(ctx);
296
+ await identityService.open();
297
297
 
298
298
  const devtoolsProxy = this._config?.get('runtime.client.devtoolsProxy');
299
299
  if (devtoolsProxy) {
@@ -349,34 +349,7 @@ export class ClientServicesHost {
349
349
 
350
350
  private async _createIdentity(params: CreateIdentityOptions) {
351
351
  const identity = await this._serviceContext.createIdentity(params);
352
-
353
- // Setup default space.
354
352
  await this._serviceContext.initialized.wait();
355
- const space = await this._serviceContext.dataSpaceManager!.createSpace();
356
-
357
- const automergeIndex = space.automergeSpaceState.rootUrl;
358
- invariant(automergeIndex);
359
- const document = this._serviceContext.echoHost.automergeRepo.find<SpaceDoc>(automergeIndex as any);
360
- await document.whenReady();
361
-
362
- // TODO(dmaretskyi): Better API for low-level data access.
363
- const properties: ObjectStructure = {
364
- system: {
365
- type: encodeReference(getTypeReference(PropertiesType)!),
366
- },
367
- data: {
368
- [defaultKey]: identity.identityKey.toHex(),
369
- },
370
- meta: {
371
- keys: [],
372
- },
373
- };
374
- const propertiesId = PublicKey.random().toHex();
375
- document.change((doc: SpaceDoc) => {
376
- assignDeep(doc, ['objects', propertiesId], properties);
377
- });
378
-
379
- await this._serviceContext.echoHost.flush();
380
353
  return identity;
381
354
  }
382
355
  }
@@ -12,7 +12,7 @@ import { log } from '@dxos/log';
12
12
  import { SpaceState } from '@dxos/protocols/proto/dxos/client/services';
13
13
  import { describe, openAndClose, test } from '@dxos/test';
14
14
 
15
- import { TestBuilder } from '../testing';
15
+ import { TestBuilder, type TestPeer } from '../testing';
16
16
 
17
17
  describe('DataSpaceManager', () => {
18
18
  test('create space', async () => {
@@ -168,5 +168,50 @@ describe('DataSpaceManager', () => {
168
168
  500,
169
169
  );
170
170
  });
171
+
172
+ test('activate opens a lazily loaded space', async () => {
173
+ const builder = new TestBuilder();
174
+
175
+ const peer = builder.createPeer();
176
+ await peer.createIdentity();
177
+ await openAndClose(peer.echoHost, peer.dataSpaceManager);
178
+
179
+ await peer.dataSpaceManager.createSpace();
180
+ await reloadDataSpaces(peer);
181
+
182
+ const space = getFirstSpace(peer);
183
+ expect(space.state).to.equal(SpaceState.CLOSED);
184
+ await space.activate();
185
+ await asyncTimeout(
186
+ space.stateUpdate.waitForCondition(() => space.state === SpaceState.READY),
187
+ 500,
188
+ );
189
+ });
190
+
191
+ test('deactivate lazily loaded space ', async () => {
192
+ const builder = new TestBuilder();
193
+
194
+ const peer = builder.createPeer();
195
+ await peer.createIdentity();
196
+ await openAndClose(peer.echoHost, peer.dataSpaceManager);
197
+
198
+ await peer.dataSpaceManager.createSpace();
199
+ await reloadDataSpaces(peer);
200
+
201
+ await getFirstSpace(peer).deactivate();
202
+
203
+ await reloadDataSpaces(peer);
204
+
205
+ expect(getFirstSpace(peer).state).to.eq(SpaceState.INACTIVE);
206
+ });
171
207
  });
208
+
209
+ const reloadDataSpaces = async (peer: TestPeer) => {
210
+ await peer.dataSpaceManager.close();
211
+ await peer.dataSpaceManager.open();
212
+ };
213
+
214
+ const getFirstSpace = (peer: TestPeer) => {
215
+ return Array.from(peer.dataSpaceManager.spaces.values())[0];
216
+ };
172
217
  });
@@ -4,7 +4,8 @@
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';
7
+ import { type DocHandle, type AutomergeUrl } from '@dxos/automerge/automerge-repo';
8
+ import { PropertiesType } from '@dxos/client-protocol';
8
9
  import { cancelWithContext, Context } from '@dxos/context';
9
10
  import {
10
11
  type CredentialSigner,
@@ -21,7 +22,8 @@ import {
21
22
  type SpaceProtocol,
22
23
  type SpaceProtocolSession,
23
24
  } from '@dxos/echo-pipeline';
24
- import { type SpaceDoc } from '@dxos/echo-protocol';
25
+ import { encodeReference, type ObjectStructure, type SpaceDoc } from '@dxos/echo-protocol';
26
+ import { getTypeReference } from '@dxos/echo-schema';
25
27
  import { type FeedStore } from '@dxos/feed-store';
26
28
  import { invariant } from '@dxos/invariant';
27
29
  import { type Keyring } from '@dxos/keyring';
@@ -37,7 +39,7 @@ import { type PeerState } from '@dxos/protocols/proto/dxos/mesh/presence';
37
39
  import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
38
40
  import { type Timeframe } from '@dxos/timeframe';
39
41
  import { trace } from '@dxos/tracing';
40
- import { ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
42
+ import { assignDeep, ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
41
43
 
42
44
  import { DataSpace, findPropertiesObject } from './data-space';
43
45
  import { spaceGenesis } from './genesis';
@@ -47,6 +49,9 @@ import { type InvitationsManager } from '../invitations';
47
49
  const PRESENCE_ANNOUNCE_INTERVAL = 10_000;
48
50
  const PRESENCE_OFFLINE_TIMEOUT = 20_000;
49
51
 
52
+ // Space properties key for default metadata.
53
+ const DEFAULT_SPACE_KEY = '__DEFAULT__';
54
+
50
55
  export interface SigningContext {
51
56
  identityKey: PublicKey;
52
57
  deviceKey: PublicKey;
@@ -88,8 +93,6 @@ export class DataSpaceManager {
88
93
 
89
94
  private _isOpen = false;
90
95
  private readonly _instanceId = PublicKey.random().toHex();
91
- private readonly _spaceMemberPresenceAnnounceInterval: number;
92
- private readonly _spaceMemberPresenceOfflineTimeout: number;
93
96
 
94
97
  constructor(
95
98
  private readonly _spaceManager: SpaceManager,
@@ -99,15 +102,8 @@ export class DataSpaceManager {
99
102
  private readonly _feedStore: FeedStore<FeedMessage>,
100
103
  private readonly _echoHost: EchoHost,
101
104
  private readonly _invitationsManager: InvitationsManager,
102
- params?: DataSpaceManagerRuntimeParams,
105
+ private readonly _params?: DataSpaceManagerRuntimeParams,
103
106
  ) {
104
- const {
105
- spaceMemberPresenceAnnounceInterval = PRESENCE_ANNOUNCE_INTERVAL,
106
- spaceMemberPresenceOfflineTimeout = PRESENCE_OFFLINE_TIMEOUT,
107
- } = params ?? {};
108
- this._spaceMemberPresenceAnnounceInterval = spaceMemberPresenceAnnounceInterval;
109
- this._spaceMemberPresenceOfflineTimeout = spaceMemberPresenceOfflineTimeout;
110
-
111
107
  trace.diagnostic({
112
108
  id: 'spaces',
113
109
  name: 'Spaces',
@@ -157,12 +153,6 @@ export class DataSpaceManager {
157
153
  this._isOpen = true;
158
154
  this.updated.emit();
159
155
 
160
- for (const space of this._spaces.values()) {
161
- if (space.state !== SpaceState.INACTIVE) {
162
- space.initializeDataPipelineAsync();
163
- }
164
- }
165
-
166
156
  log.trace('dxos.echo.data-space-manager.open', Trace.end({ id: this._instanceId }));
167
157
  }
168
158
 
@@ -174,6 +164,7 @@ export class DataSpaceManager {
174
164
  for (const space of this._spaces.values()) {
175
165
  await space.close();
176
166
  }
167
+ this._spaces.clear();
177
168
  }
178
169
 
179
170
  /**
@@ -197,6 +188,7 @@ export class DataSpaceManager {
197
188
 
198
189
  const root = await this._echoHost.createSpaceRoot(spaceKey);
199
190
  const space = await this._constructSpace(metadata);
191
+ await space.open();
200
192
 
201
193
  const credentials = await spaceGenesis(this._keyring, this._signingContext, space.inner, root.url);
202
194
  await this._metadataStore.addSpace(metadata);
@@ -211,6 +203,46 @@ export class DataSpaceManager {
211
203
  return space;
212
204
  }
213
205
 
206
+ async isDefaultSpace(space: DataSpace): Promise<boolean> {
207
+ const rootDoc = await this._getSpaceRootDocument(space);
208
+ const [_, properties] = findPropertiesObject(rootDoc.docSync()) ?? [];
209
+ return properties?.data?.[DEFAULT_SPACE_KEY] === this._signingContext.identityKey.toHex();
210
+ }
211
+
212
+ async createDefaultSpace() {
213
+ const space = await this.createSpace();
214
+ const document = await this._getSpaceRootDocument(space);
215
+
216
+ // TODO(dmaretskyi): Better API for low-level data access.
217
+ const properties: ObjectStructure = {
218
+ system: {
219
+ type: encodeReference(getTypeReference(PropertiesType)!),
220
+ },
221
+ data: {
222
+ [DEFAULT_SPACE_KEY]: this._signingContext.identityKey.toHex(),
223
+ },
224
+ meta: {
225
+ keys: [],
226
+ },
227
+ };
228
+
229
+ const propertiesId = PublicKey.random().toHex();
230
+ document.change((doc: SpaceDoc) => {
231
+ assignDeep(doc, ['objects', propertiesId], properties);
232
+ });
233
+
234
+ await this._echoHost.flush();
235
+ return space;
236
+ }
237
+
238
+ private async _getSpaceRootDocument(space: DataSpace): Promise<DocHandle<SpaceDoc>> {
239
+ const automergeIndex = space.automergeSpaceState.rootUrl;
240
+ invariant(automergeIndex);
241
+ const document = this._echoHost.automergeRepo.find<SpaceDoc>(automergeIndex as any);
242
+ await document.whenReady();
243
+ return document;
244
+ }
245
+
214
246
  // TODO(burdon): Rename join space.
215
247
  @synchronized
216
248
  async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
@@ -226,6 +258,7 @@ export class DataSpaceManager {
226
258
  };
227
259
 
228
260
  const space = await this._constructSpace(metadata);
261
+ await space.open();
229
262
  await this._metadataStore.addSpace(metadata);
230
263
  space.initializeDataPipelineAsync();
231
264
 
@@ -254,8 +287,8 @@ export class DataSpaceManager {
254
287
  localPeerId: this._signingContext.deviceKey,
255
288
  });
256
289
  const presence = new Presence({
257
- announceInterval: this._spaceMemberPresenceAnnounceInterval,
258
- offlineTimeout: this._spaceMemberPresenceOfflineTimeout,
290
+ announceInterval: this._params?.spaceMemberPresenceAnnounceInterval ?? PRESENCE_ANNOUNCE_INTERVAL,
291
+ offlineTimeout: this._params?.spaceMemberPresenceOfflineTimeout ?? PRESENCE_OFFLINE_TIMEOUT,
259
292
  identityKey: this._signingContext.identityKey,
260
293
  gossip,
261
294
  });
@@ -336,10 +369,6 @@ export class DataSpaceManager {
336
369
  }
337
370
  });
338
371
 
339
- if (metadata.state !== SpaceState.INACTIVE) {
340
- await dataSpace.open();
341
- }
342
-
343
372
  if (metadata.controlTimeframe) {
344
373
  dataSpace.inner.controlPipeline.state.setTargetTimeframe(metadata.controlTimeframe);
345
374
  }
@@ -193,10 +193,13 @@ export class DataSpace {
193
193
 
194
194
  @synchronized
195
195
  async open() {
196
- await this._open();
196
+ if (this._state === SpaceState.CLOSED) {
197
+ await this._open();
198
+ }
197
199
  }
198
200
 
199
201
  private async _open() {
202
+ await this._presence.open();
200
203
  await this._gossip.open();
201
204
  await this._notarizationPlugin.open();
202
205
  await this._inner.spaceState.addCredentialProcessor(this._notarizationPlugin);
@@ -228,7 +231,7 @@ export class DataSpace {
228
231
  await this._inner.spaceState.removeCredentialProcessor(this._notarizationPlugin);
229
232
  await this._notarizationPlugin.close();
230
233
 
231
- await this._presence.destroy();
234
+ await this._presence.close();
232
235
  await this._gossip.close();
233
236
  }
234
237
 
@@ -453,6 +456,7 @@ export class DataSpace {
453
456
  const rootHandle = this._echoHost.automergeRepo.find(currentRootUrl as any);
454
457
  await cancelWithContext(this._ctx, asyncTimeout(rootHandle.whenReady(), 10_000));
455
458
  const newRoot = this._echoHost.automergeRepo.create(rootHandle.docSync());
459
+ await this._echoHost.automergeRepo.flush([newRoot.documentId]);
456
460
  invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
457
461
  // TODO(dmaretskyi): Unify epoch construction.
458
462
  epoch = {
@@ -544,7 +548,7 @@ export class DataSpace {
544
548
 
545
549
  @synchronized
546
550
  async activate() {
547
- if (this._state !== SpaceState.INACTIVE) {
551
+ if (![SpaceState.CLOSED, SpaceState.INACTIVE].includes(this._state)) {
548
552
  return;
549
553
  }
550
554
 
@@ -558,10 +562,11 @@ export class DataSpace {
558
562
  if (this._state === SpaceState.INACTIVE) {
559
563
  return;
560
564
  }
561
-
562
565
  // Unregister from data service.
563
566
  await this._metadataStore.setSpaceState(this.key, SpaceState.INACTIVE);
564
- await this._close();
567
+ if (this._state !== SpaceState.CLOSED) {
568
+ await this._close();
569
+ }
565
570
  this._state = SpaceState.INACTIVE;
566
571
  log('new state', { state: SpaceState[this._state] });
567
572
  this.stateUpdate.emit();