@dxos/client-services 0.1.19 → 0.1.21

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 (49) hide show
  1. package/dist/lib/browser/index.mjs +306 -129
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +333 -152
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  8. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  9. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
  10. package/dist/types/src/packlets/network/index.d.ts +2 -0
  11. package/dist/types/src/packlets/network/index.d.ts.map +1 -0
  12. package/dist/types/src/packlets/network/network-service.d.ts +10 -0
  13. package/dist/types/src/packlets/network/network-service.d.ts.map +1 -0
  14. package/dist/types/src/packlets/services/service-context.d.ts +2 -1
  15. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  16. package/dist/types/src/packlets/services/service-definitions.d.ts +2 -1
  17. package/dist/types/src/packlets/services/service-definitions.d.ts.map +1 -1
  18. package/dist/types/src/packlets/services/service-host.d.ts +4 -1
  19. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  20. package/dist/types/src/packlets/services/service-host.test.d.ts +2 -0
  21. package/dist/types/src/packlets/services/service-host.test.d.ts.map +1 -0
  22. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +4 -2
  23. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  24. package/dist/types/src/packlets/spaces/data-space.d.ts +13 -6
  25. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  26. package/dist/types/src/packlets/spaces/spaces-service.d.ts +6 -1
  27. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  28. package/dist/types/src/packlets/storage/storage.d.ts +0 -2
  29. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  30. package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
  31. package/package.json +28 -34
  32. package/src/packlets/deprecated/space.ts +1 -1
  33. package/src/packlets/devtools/spaces.ts +1 -1
  34. package/src/packlets/identity/identity-manager.ts +3 -9
  35. package/src/packlets/identity/identity.test.ts +4 -33
  36. package/src/packlets/identity/identity.ts +26 -18
  37. package/src/packlets/invitations/space-invitations-handler.test.ts +53 -1
  38. package/src/packlets/invitations/space-invitations-handler.ts +9 -5
  39. package/src/packlets/network/index.ts +5 -0
  40. package/src/packlets/network/network-service.ts +24 -0
  41. package/src/packlets/services/service-context.ts +9 -2
  42. package/src/packlets/services/service-definitions.ts +5 -1
  43. package/src/packlets/services/service-host.test.ts +31 -0
  44. package/src/packlets/services/service-host.ts +13 -9
  45. package/src/packlets/spaces/data-space-manager.ts +46 -9
  46. package/src/packlets/spaces/data-space.ts +28 -10
  47. package/src/packlets/spaces/spaces-service.ts +21 -0
  48. package/src/packlets/storage/storage.ts +4 -41
  49. package/src/packlets/vault/worker-session.ts +1 -0
@@ -6,12 +6,11 @@ import assert from 'node:assert';
6
6
 
7
7
  import { Event } from '@dxos/async';
8
8
  import { createCredentialSignerWithKey, CredentialGenerator } from '@dxos/credentials';
9
- import { MetadataStore, NoopDataPipelineController, SpaceManager, SwarmIdentity } from '@dxos/echo-db';
9
+ import { MetadataStore, SpaceManager, SwarmIdentity } from '@dxos/echo-db';
10
10
  import { Keyring } from '@dxos/keyring';
11
11
  import { PublicKey } from '@dxos/keys';
12
12
  import { log } from '@dxos/log';
13
13
  import { AdmittedFeed, IdentityRecord, SpaceRecord } from '@dxos/protocols/proto/dxos/halo/credentials';
14
- import { Presence } from '@dxos/teleport-extension-presence';
15
14
  import { deferFunction } from '@dxos/util';
16
15
 
17
16
  import { Identity } from '../identity';
@@ -55,6 +54,7 @@ export class IdentityManager {
55
54
  await this._metadataStore.load();
56
55
 
57
56
  const identityRecord = this._metadataStore.getIdentityRecord();
57
+ log('identity record', { identityRecord });
58
58
  if (identityRecord) {
59
59
  this._identity = await this._constructIdentity(identityRecord);
60
60
  await this._identity.open();
@@ -192,14 +192,8 @@ export class IdentityManager {
192
192
  controlFeedKey: spaceRecord.writeControlFeedKey,
193
193
  dataFeedKey: spaceRecord.writeDataFeedKey
194
194
  },
195
- dataPipelineControllerProvider: () => new NoopDataPipelineController(),
196
195
  swarmIdentity,
197
- presence: new Presence({
198
- localPeerId: swarmIdentity.peerKey,
199
- announceInterval: 1_000,
200
- offlineTimeout: 30_000,
201
- identityKey
202
- })
196
+ onNetworkConnection: () => {}
203
197
  });
204
198
  }
205
199
  }
@@ -5,14 +5,7 @@
5
5
  import expect from 'expect';
6
6
 
7
7
  import { CredentialGenerator, verifyCredential } from '@dxos/credentials';
8
- import {
9
- MOCK_AUTH_PROVIDER,
10
- MOCK_AUTH_VERIFIER,
11
- NoopDataPipelineController,
12
- Space,
13
- SpaceProtocol,
14
- valueEncoding
15
- } from '@dxos/echo-db';
8
+ import { MOCK_AUTH_PROVIDER, MOCK_AUTH_VERIFIER, Space, SpaceProtocol, valueEncoding } from '@dxos/echo-db';
16
9
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
17
10
  import { Keyring } from '@dxos/keyring';
18
11
  import { PublicKey } from '@dxos/keys';
@@ -21,7 +14,6 @@ import { MemoryTransportFactory, NetworkManager } from '@dxos/network-manager';
21
14
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
15
  import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
23
16
  import { createStorage, StorageType } from '@dxos/random-access-storage';
24
- import { Presence } from '@dxos/teleport-extension-presence';
25
17
  import { afterTest, describe, test } from '@dxos/test';
26
18
 
27
19
  import { Identity } from './identity';
@@ -61,12 +53,6 @@ describe('identity/identity', () => {
61
53
  networkManager: new NetworkManager({
62
54
  signalManager: new MemorySignalManager(new MemorySignalManagerContext()),
63
55
  transportFactory: MemoryTransportFactory
64
- }),
65
- presence: new Presence({
66
- localPeerId: deviceKey,
67
- announceInterval: 30,
68
- offlineTimeout: 200,
69
- identityKey
70
56
  })
71
57
  });
72
58
 
@@ -76,8 +62,7 @@ describe('identity/identity', () => {
76
62
  genesisFeed: controlFeed,
77
63
  controlFeed,
78
64
  dataFeed,
79
- feedProvider: (feedKey) => feedStore.openFeed(feedKey),
80
- dataPipelineControllerProvider: () => new NoopDataPipelineController()
65
+ feedProvider: (feedKey) => feedStore.openFeed(feedKey)
81
66
  });
82
67
 
83
68
  const identity = new Identity({
@@ -174,12 +159,6 @@ describe('identity/identity', () => {
174
159
  networkManager: new NetworkManager({
175
160
  signalManager: new MemorySignalManager(signalContext),
176
161
  transportFactory: MemoryTransportFactory
177
- }),
178
- presence: new Presence({
179
- localPeerId: deviceKey,
180
- announceInterval: 30,
181
- offlineTimeout: 200,
182
- identityKey
183
162
  })
184
163
  });
185
164
 
@@ -189,8 +168,7 @@ describe('identity/identity', () => {
189
168
  genesisFeed: controlFeed,
190
169
  controlFeed,
191
170
  dataFeed,
192
- feedProvider: (feedKey) => feedStore.openFeed(feedKey),
193
- dataPipelineControllerProvider: () => new NoopDataPipelineController()
171
+ feedProvider: (feedKey) => feedStore.openFeed(feedKey)
194
172
  });
195
173
 
196
174
  const identity = (identity1 = new Identity({
@@ -261,12 +239,6 @@ describe('identity/identity', () => {
261
239
  networkManager: new NetworkManager({
262
240
  signalManager: new MemorySignalManager(signalContext),
263
241
  transportFactory: MemoryTransportFactory
264
- }),
265
- presence: new Presence({
266
- localPeerId: deviceKey,
267
- announceInterval: 30,
268
- offlineTimeout: 200,
269
- identityKey
270
242
  })
271
243
  });
272
244
 
@@ -276,8 +248,7 @@ describe('identity/identity', () => {
276
248
  genesisFeed: await feedStore.openFeed(genesisFeedKey),
277
249
  controlFeed,
278
250
  dataFeed,
279
- feedProvider: (feedKey) => feedStore.openFeed(feedKey),
280
- dataPipelineControllerProvider: () => new NoopDataPipelineController()
251
+ feedProvider: (feedKey) => feedStore.openFeed(feedKey)
281
252
  });
282
253
 
283
254
  const identity = (identity2 = new Identity({
@@ -10,7 +10,8 @@ import {
10
10
  CredentialSigner,
11
11
  createCredentialSignerWithKey,
12
12
  createCredentialSignerWithChain,
13
- ProfileStateMachine
13
+ ProfileStateMachine,
14
+ CredentialConsumer
14
15
  } from '@dxos/credentials';
15
16
  import { Signer } from '@dxos/crypto';
16
17
  import { Space } from '@dxos/echo-db';
@@ -42,8 +43,8 @@ export type IdentityParams = {
42
43
  export class Identity {
43
44
  public readonly space: Space;
44
45
  private readonly _signer: Signer;
45
- private readonly _deviceStateMachine: DeviceStateMachine;
46
- private readonly _profileStateMachine: ProfileStateMachine;
46
+ private readonly _deviceStateMachine: CredentialConsumer<DeviceStateMachine>;
47
+ private readonly _profileStateMachine: CredentialConsumer<ProfileStateMachine>;
47
48
  public readonly authVerifier: HaloAuthVerifier;
48
49
 
49
50
  public readonly identityKey: PublicKey;
@@ -58,16 +59,19 @@ export class Identity {
58
59
  this.identityKey = identityKey;
59
60
  this.deviceKey = deviceKey;
60
61
 
61
- this._deviceStateMachine = new DeviceStateMachine(this.identityKey, this.deviceKey);
62
- this._profileStateMachine = new ProfileStateMachine(this.identityKey);
63
-
64
- // Process halo-specific credentials.
65
- this.space.onCredentialProcessed.set(async (credential) => {
66
- // Save device keychain credential when processed by the space state machine.
67
- await this._deviceStateMachine.process(credential);
68
- await this._profileStateMachine.process(credential);
69
- this.stateUpdate.emit();
70
- });
62
+ this._deviceStateMachine = this.space.spaceState.registerProcessor(
63
+ new DeviceStateMachine({
64
+ identityKey: this.identityKey,
65
+ deviceKey: this.deviceKey,
66
+ onUpdate: () => this.stateUpdate.emit()
67
+ })
68
+ );
69
+ this._profileStateMachine = this.space.spaceState.registerProcessor(
70
+ new ProfileStateMachine({
71
+ identityKey: this.identityKey,
72
+ onUpdate: () => this.stateUpdate.emit()
73
+ })
74
+ );
71
75
 
72
76
  this.authVerifier = new HaloAuthVerifier({
73
77
  trustedDevicesProvider: () => this.authorizedDeviceKeys,
@@ -78,26 +82,30 @@ export class Identity {
78
82
 
79
83
  // TODO(burdon): Expose state object?
80
84
  get authorizedDeviceKeys(): ComplexSet<PublicKey> {
81
- return this._deviceStateMachine.authorizedDeviceKeys;
85
+ return this._deviceStateMachine.processor.authorizedDeviceKeys;
82
86
  }
83
87
 
84
88
  async open() {
89
+ await this._deviceStateMachine.open();
90
+ await this._profileStateMachine.open();
85
91
  await this.space.open();
86
92
  }
87
93
 
88
94
  async close() {
89
95
  await this.authVerifier.close();
96
+ await this._deviceStateMachine.close();
97
+ await this._profileStateMachine.close();
90
98
  await this.space.close();
91
99
  }
92
100
 
93
101
  async ready() {
94
- await this._deviceStateMachine.deviceChainReady.wait();
102
+ await this._deviceStateMachine.processor.deviceChainReady.wait();
95
103
 
96
104
  // TODO(dmaretskyi): Should we also wait for our feeds to be admitted?
97
105
  }
98
106
 
99
107
  get profileDocument(): ProfileDocument | undefined {
100
- return this._profileStateMachine.profile;
108
+ return this._profileStateMachine.processor.profile;
101
109
  }
102
110
 
103
111
  /**
@@ -128,10 +136,10 @@ export class Identity {
128
136
  * Requires identity to be ready.
129
137
  */
130
138
  getIdentityCredentialSigner(): CredentialSigner {
131
- assert(this._deviceStateMachine.deviceCredentialChain, 'Device credential chain is not ready.');
139
+ assert(this._deviceStateMachine.processor.deviceCredentialChain, 'Device credential chain is not ready.');
132
140
  return createCredentialSignerWithChain(
133
141
  this._signer,
134
- this._deviceStateMachine.deviceCredentialChain,
142
+ this._deviceStateMachine.processor.deviceCredentialChain,
135
143
  this.deviceKey
136
144
  );
137
145
  }
@@ -5,12 +5,13 @@
5
5
  import { expect } from 'chai';
6
6
  import assert from 'node:assert';
7
7
 
8
- import { asyncChain, Trigger } from '@dxos/async';
8
+ import { asyncChain, latch, Trigger } from '@dxos/async';
9
9
  import { raise } from '@dxos/debug';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { ObjectModel } from '@dxos/object-model';
12
12
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
13
13
  import { describe, test, afterTest } from '@dxos/test';
14
+ import { range } from '@dxos/util';
14
15
 
15
16
  import { ServiceContext } from '../services';
16
17
  import { createIdentity, createPeers, syncItems } from '../testing';
@@ -157,4 +158,55 @@ describe('services/space-invitations-handler', () => {
157
158
  await cancelled.wait();
158
159
  await space1.close();
159
160
  });
161
+
162
+ test('test multi-use invitation', async () => {
163
+ const GUEST_COUNT = 3;
164
+ const [host, ...guests] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(
165
+ createPeers(GUEST_COUNT + 1)
166
+ );
167
+
168
+ const hostSpace = await host.dataSpaceManager!.createSpace();
169
+ const swarmKey = PublicKey.random();
170
+ const hostObservable = await host.spaceInvitations!.createInvitation(hostSpace, {
171
+ swarmKey,
172
+ type: Invitation.Type.MULTIUSE_TESTING
173
+ });
174
+
175
+ const [done, count] = latch({ count: GUEST_COUNT });
176
+ hostObservable.subscribe({
177
+ onConnecting: async (invitation2: Invitation) => {},
178
+ onConnected: async (invitation2: Invitation) => {},
179
+ onSuccess: () => {
180
+ count();
181
+ },
182
+ onCancelled: () => {},
183
+ onTimeout: (err: Error) => raise(new Error(err.message)),
184
+ onError: (err: Error) => raise(new Error(err.message))
185
+ });
186
+
187
+ await Promise.all(
188
+ range(GUEST_COUNT).map(async (idx) => {
189
+ const observable = await guests[idx].spaceInvitations!.acceptInvitation({
190
+ swarmKey,
191
+ type: Invitation.Type.MULTIUSE_TESTING
192
+ });
193
+ const success = new Trigger();
194
+ observable.subscribe({
195
+ onConnecting: async (invitation2: Invitation) => {},
196
+ onConnected: async (invitation2: Invitation) => {},
197
+ onSuccess: () => {
198
+ success.wake();
199
+ },
200
+ onCancelled: () => raise(new Error()),
201
+ onTimeout: (err: Error) => raise(new Error(err.message)),
202
+ onError: (err: Error) => raise(new Error(err.message))
203
+ });
204
+ await success.wait({ timeout: 300 });
205
+ })
206
+ );
207
+ await done();
208
+
209
+ await hostObservable.cancel();
210
+ await hostSpace.close();
211
+ });
160
212
  });
@@ -87,10 +87,9 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
87
87
  let authenticationCode: string;
88
88
  let authenticationRetry = 0;
89
89
 
90
- const complete = new Trigger<PublicKey>();
91
-
92
90
  // Called for every connecting peer.
93
91
  const createExtension = (): HostSpaceInvitationExtension => {
92
+ const complete = new Trigger<PublicKey>();
94
93
  let guestProfile: ProfileDocument | undefined;
95
94
 
96
95
  const hostInvitationExtension = new HostSpaceInvitationExtension({
@@ -127,7 +126,10 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
127
126
  requestAdmission: async ({ identityKey, deviceKey, controlFeedKey, dataFeedKey }) => {
128
127
  try {
129
128
  // Check authenticated.
130
- if (invitation.type !== Invitation.Type.INTERACTIVE_TESTING) {
129
+ if (
130
+ invitation.type !== Invitation.Type.INTERACTIVE_TESTING &&
131
+ invitation.type !== Invitation.Type.MULTIUSE_TESTING
132
+ ) {
131
133
  if (invitation.authenticationCode === undefined || invitation.authenticationCode !== authenticationCode) {
132
134
  throw new Error(`invalid authentication code: ${authenticationCode}`);
133
135
  }
@@ -178,8 +180,10 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
178
180
  observable.callback.onError(err);
179
181
  }
180
182
  } finally {
181
- await sleep(ON_CLOSE_DELAY);
182
- await ctx.dispose();
183
+ if (type !== Invitation.Type.MULTIUSE_TESTING) {
184
+ await sleep(ON_CLOSE_DELAY);
185
+ await ctx.dispose();
186
+ }
183
187
  }
184
188
  });
185
189
  }
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './network-service';
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { Stream } from '@dxos/codec-protobuf';
6
+ import { NetworkManager } from '@dxos/network-manager';
7
+ import { NetworkService, NetworkStatus, SetNetworkOptionsRequest } from '@dxos/protocols/proto/dxos/client/services';
8
+
9
+ export class NetworkServiceImpl implements NetworkService {
10
+ constructor(private readonly networkManager: NetworkManager) {}
11
+
12
+ subscribeToNetworkStatus() {
13
+ return new Stream<NetworkStatus>(({ next }) => {
14
+ const unsubscribe = this.networkManager.connectionStateChanged.on((state) => next({ state }));
15
+ next({ state: this.networkManager.connectionState });
16
+
17
+ return unsubscribe;
18
+ });
19
+ }
20
+
21
+ async setNetworkOptions(request: SetNetworkOptionsRequest) {
22
+ await this.networkManager.setConnectionState(request.state);
23
+ }
24
+ }
@@ -11,7 +11,8 @@ import {
11
11
  DataServiceSubscriptions,
12
12
  MetadataStore,
13
13
  SpaceManager,
14
- SigningContext
14
+ SigningContext,
15
+ SnapshotStore
15
16
  } from '@dxos/echo-db';
16
17
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
17
18
  import { Keyring } from '@dxos/keyring';
@@ -34,6 +35,7 @@ export class ServiceContext {
34
35
  public readonly initialized = new Trigger();
35
36
  public readonly dataServiceSubscriptions = new DataServiceSubscriptions();
36
37
  public readonly metadataStore: MetadataStore;
38
+ public readonly snapshotStore: SnapshotStore;
37
39
  public readonly feedStore: FeedStore<FeedMessage>;
38
40
  public readonly keyring: Keyring;
39
41
  public readonly spaceManager: SpaceManager;
@@ -52,6 +54,8 @@ export class ServiceContext {
52
54
  ) {
53
55
  // TODO(burdon): Move strings to constants.
54
56
  this.metadataStore = new MetadataStore(storage.createDirectory('metadata'));
57
+ this.snapshotStore = new SnapshotStore(storage.createDirectory('snapshots'));
58
+
55
59
  this.keyring = new Keyring(storage.createDirectory('keyring'));
56
60
  this.feedStore = new FeedStore<FeedMessage>({
57
61
  factory: new FeedFactory<FeedMessage>({
@@ -79,6 +83,7 @@ export class ServiceContext {
79
83
 
80
84
  async open() {
81
85
  log('opening...');
86
+ await this.networkManager.open();
82
87
  await this.spaceManager.open();
83
88
  await this.identityManager.open();
84
89
  if (this.identityManager.identity) {
@@ -114,6 +119,7 @@ export class ServiceContext {
114
119
 
115
120
  // Called when identity is created.
116
121
  private async _initialize() {
122
+ log('initializing spaces...');
117
123
  const identity = this.identityManager.identity ?? failUndefined();
118
124
  const signingContext: SigningContext = {
119
125
  credentialProvider: MOCK_AUTH_PROVIDER,
@@ -130,7 +136,8 @@ export class ServiceContext {
130
136
  this.dataServiceSubscriptions,
131
137
  this.keyring,
132
138
  signingContext,
133
- this.modelFactory
139
+ this.modelFactory,
140
+ this.snapshotStore
134
141
  );
135
142
  await this.dataSpaceManager.open();
136
143
  this.spaceInvitations = new SpaceInvitationsHandler(
@@ -8,7 +8,8 @@ import {
8
8
  HaloInvitationsService,
9
9
  SpaceInvitationsService,
10
10
  SpacesService,
11
- DevicesService
11
+ DevicesService,
12
+ NetworkService
12
13
  } from '@dxos/protocols/proto/dxos/client/services';
13
14
  import { DevtoolsHost, TracingService } from '@dxos/protocols/proto/dxos/devtools/host';
14
15
  import { DataService } from '@dxos/protocols/proto/dxos/echo/service';
@@ -25,6 +26,8 @@ export type ClientServices = {
25
26
  SpaceInvitationsService: SpaceInvitationsService;
26
27
  SpacesService: SpacesService;
27
28
 
29
+ NetworkService: NetworkService;
30
+
28
31
  // TODO(burdon): Deprecated.
29
32
  SpaceService: SpaceService;
30
33
  DataService: DataService;
@@ -53,6 +56,7 @@ export const clientServiceBundle = createServiceBundle<ClientServices>({
53
56
  DevicesService: schema.getService('dxos.client.services.DevicesService'),
54
57
  SpaceInvitationsService: schema.getService('dxos.client.services.SpaceInvitationsService'),
55
58
  SpacesService: schema.getService('dxos.client.services.SpacesService'),
59
+ NetworkService: schema.getService('dxos.client.services.NetworkService'),
56
60
 
57
61
  // TODO(burdon): Deprecated.
58
62
  SpaceService: schema.getService('dxos.client.SpaceService'),
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { latch } from '@dxos/async';
6
+ import { Config } from '@dxos/config';
7
+ import { MemorySignalManagerContext } from '@dxos/messaging';
8
+ import { afterTest, describe, test } from '@dxos/test';
9
+
10
+ import { createServiceHost } from '../testing';
11
+
12
+ describe('ClientServicesHost', () => {
13
+ test('queryCredentials', async () => {
14
+ const host = createServiceHost(new Config(), new MemorySignalManagerContext());
15
+ await host.open();
16
+ afterTest(() => host.close());
17
+
18
+ await host.services.ProfileService.createProfile({});
19
+ const { publicKey: spaceKey } = await host.services.SpaceService.createSpace();
20
+
21
+ const stream = host.services.SpacesService.queryCredentials({ spaceKey });
22
+ const [done, tick] = latch({ count: 3 });
23
+ stream.subscribe((credential) => {
24
+ tick();
25
+ // console.log(credential);
26
+ });
27
+ afterTest(() => stream.close());
28
+
29
+ await done();
30
+ });
31
+ });
@@ -9,12 +9,14 @@ import { log } from '@dxos/log';
9
9
  import { ModelFactory } from '@dxos/model-factory';
10
10
  import { NetworkManager } from '@dxos/network-manager';
11
11
  import { ObjectModel } from '@dxos/object-model';
12
+ import { Storage } from '@dxos/random-access-storage';
12
13
  import { TextModel } from '@dxos/text-model';
13
14
 
14
15
  import { SpaceServiceImpl, ProfileServiceImpl, SystemServiceImpl, TracingServiceImpl } from '../deprecated';
15
16
  import { DevtoolsServiceImpl, DevtoolsHostEvents } from '../devtools';
16
17
  import { DevicesServiceImpl } from '../identity/devices-service-impl';
17
18
  import { HaloInvitationsServiceImpl, SpaceInvitationsServiceImpl } from '../invitations';
19
+ import { NetworkServiceImpl } from '../network';
18
20
  import { SpacesServiceImpl } from '../spaces';
19
21
  import { createStorageObjects } from '../storage';
20
22
  import { ServiceContext } from './service-context';
@@ -31,6 +33,7 @@ type ClientServicesHostParams = {
31
33
  config: Config;
32
34
  modelFactory?: ModelFactory;
33
35
  networkManager: NetworkManager;
36
+ storage?: Storage;
34
37
  };
35
38
 
36
39
  /**
@@ -43,16 +46,19 @@ export class ClientServicesHost implements ClientServicesProvider {
43
46
  private readonly _config: Config;
44
47
  private readonly _modelFactory: ModelFactory;
45
48
  private readonly _networkManager: NetworkManager;
49
+ private _storage: Storage;
46
50
 
47
51
  constructor({
48
52
  config,
49
53
  modelFactory = createDefaultModelFactory(),
50
54
  // TODO(burdon): Create ApolloLink abstraction (see Client).
51
- networkManager
55
+ networkManager,
56
+ storage = createStorageObjects(config.get('runtime.client.storage', {})!).storage
52
57
  }: ClientServicesHostParams) {
53
58
  this._config = config;
54
59
  this._modelFactory = modelFactory;
55
60
  this._networkManager = networkManager;
61
+ this._storage = storage;
56
62
  }
57
63
 
58
64
  get descriptors() {
@@ -64,11 +70,10 @@ export class ClientServicesHost implements ClientServicesProvider {
64
70
  }
65
71
 
66
72
  async open() {
67
- await this._initialize();
68
- const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
69
- log('opening...', { deviceKey });
73
+ log('opening...');
70
74
  await this._initialize();
71
75
  await this._serviceContext.open();
76
+ const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
72
77
  log('opened', { deviceKey });
73
78
  }
74
79
 
@@ -81,11 +86,8 @@ export class ClientServicesHost implements ClientServicesProvider {
81
86
 
82
87
  // TODO(burdon): Move into open.
83
88
  async _initialize() {
84
- // TODO(dmaretskyi): Remove keyStorage.
85
- const { storage } = createStorageObjects(this._config.get('runtime.client.storage', {})!);
86
-
87
89
  // TODO(burdon): Break into components.
88
- this._serviceContext = new ServiceContext(storage, this._networkManager, this._modelFactory);
90
+ this._serviceContext = new ServiceContext(this._storage, this._networkManager, this._modelFactory);
89
91
 
90
92
  // TODO(burdon): Start to think of DMG (dynamic services).
91
93
  this._serviceRegistry = new ServiceRegistry<ClientServices>(clientServiceBundle, {
@@ -102,10 +104,12 @@ export class ClientServicesHost implements ClientServicesProvider {
102
104
  () => this._serviceContext.dataSpaceManager ?? raise(new Error('SpaceManager not initialized'))
103
105
  ),
104
106
 
105
- SpacesService: new SpacesServiceImpl(),
107
+ SpacesService: new SpacesServiceImpl(this._serviceContext.spaceManager),
106
108
 
107
109
  DataService: new DataServiceImpl(this._serviceContext.dataServiceSubscriptions),
108
110
 
111
+ NetworkService: new NetworkServiceImpl(this._serviceContext.networkManager),
112
+
109
113
  // TODO(burdon): Move to new protobuf definitions.
110
114
  ProfileService: new ProfileServiceImpl(this._serviceContext),
111
115
  SpaceService: new SpaceServiceImpl(this._serviceContext),
@@ -2,15 +2,18 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Event, synchronized } from '@dxos/async';
5
+ import { Event, synchronized, trackLeaks } from '@dxos/async';
6
+ import { Context } from '@dxos/context';
6
7
  import {
7
8
  AcceptSpaceOptions,
8
9
  DataServiceSubscriptions,
9
10
  MetadataStore,
10
11
  SigningContext,
12
+ SnapshotStore,
11
13
  Space,
14
+ spaceGenesis,
12
15
  SpaceManager,
13
- spaceGenesis
16
+ SnapshotManager
14
17
  } from '@dxos/echo-db';
15
18
  import { Keyring } from '@dxos/keyring';
16
19
  import { PublicKey } from '@dxos/keys';
@@ -22,7 +25,10 @@ import { ComplexMap } from '@dxos/util';
22
25
 
23
26
  import { DataSpace } from './data-space';
24
27
 
28
+ @trackLeaks('open', 'close')
25
29
  export class DataSpaceManager {
30
+ private readonly _ctx = new Context();
31
+
26
32
  public readonly updated = new Event();
27
33
 
28
34
  private readonly _spaces = new ComplexMap<PublicKey, DataSpace>(PublicKey.hash);
@@ -33,7 +39,8 @@ export class DataSpaceManager {
33
39
  private readonly _dataServiceSubscriptions: DataServiceSubscriptions,
34
40
  private readonly _keyring: Keyring,
35
41
  private readonly _signingContext: SigningContext,
36
- private readonly _modelFactory: ModelFactory
42
+ private readonly _modelFactory: ModelFactory,
43
+ private readonly _snapshotStore: SnapshotStore
37
44
  ) {}
38
45
 
39
46
  // TODO(burdon): Remove.
@@ -44,18 +51,30 @@ export class DataSpaceManager {
44
51
  @synchronized
45
52
  async open() {
46
53
  await this._metadataStore.load();
54
+ log('metadata loaded', { spaces: this._metadataStore.spaces.length });
47
55
 
48
56
  for (const spaceMetadata of this._metadataStore.spaces) {
49
- await this._constructSpace(spaceMetadata);
57
+ log('load space', { spaceMetadata });
58
+ const space = await this._constructSpace(spaceMetadata);
59
+ if (spaceMetadata.latestTimeframe) {
60
+ log('waiting for latest timeframe', { spaceMetadata });
61
+ await space.dataPipelineController.waitUntilTimeframe(spaceMetadata.latestTimeframe);
62
+ }
50
63
  }
51
64
  }
52
65
 
53
66
  @synchronized
54
- async close() {}
67
+ async close() {
68
+ await this._ctx.dispose();
69
+ for (const space of this._spaces.values()) {
70
+ await space.close();
71
+ }
72
+ }
55
73
 
56
74
  /**
57
75
  * Creates a new space writing the genesis credentials to the control feed.
58
76
  */
77
+ @synchronized
59
78
  async createSpace() {
60
79
  const spaceKey = await this._keyring.createKey();
61
80
  const controlFeedKey = await this._keyring.createKey();
@@ -78,6 +97,7 @@ export class DataSpaceManager {
78
97
  }
79
98
 
80
99
  // TODO(burdon): Rename join space.
100
+ @synchronized
81
101
  async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
82
102
  const metadata: SpaceMetadata = {
83
103
  key: opts.spaceKey,
@@ -99,6 +119,8 @@ export class DataSpaceManager {
99
119
  offlineTimeout: 30_000,
100
120
  identityKey: this._signingContext.identityKey
101
121
  });
122
+ const snapshotManager = new SnapshotManager(this._snapshotStore);
123
+
102
124
  const space: Space = await this._spaceManager.constructSpace({
103
125
  metadata,
104
126
  swarmIdentity: {
@@ -106,11 +128,26 @@ export class DataSpaceManager {
106
128
  credentialProvider: this._signingContext.credentialProvider,
107
129
  credentialAuthenticator: this._signingContext.credentialAuthenticator
108
130
  },
109
- dataPipelineControllerProvider: () => dataSpace.dataPipelineController,
110
- presence
131
+ onNetworkConnection: (session) => {
132
+ session.addExtension(
133
+ 'dxos.mesh.teleport.presence',
134
+ presence.createExtension({ remotePeerId: session.remotePeerId })
135
+ );
136
+ session.addExtension('dxos.mesh.teleport.objectsync', snapshotManager.objectSync.createExtension());
137
+ }
138
+ });
139
+
140
+ const dataSpace = new DataSpace({
141
+ inner: space,
142
+ modelFactory: this._modelFactory,
143
+ metadataStore: this._metadataStore,
144
+ snapshotManager,
145
+ presence,
146
+ memberKey: this._signingContext.identityKey,
147
+ snapshotId: metadata.snapshot
111
148
  });
112
- const dataSpace = new DataSpace(space, this._modelFactory, this._signingContext.identityKey, presence);
113
- await space.open();
149
+
150
+ await dataSpace.open();
114
151
  this._dataServiceSubscriptions.registerSpace(space.key, dataSpace.database.createDataServiceHost());
115
152
  this._spaces.set(metadata.key, dataSpace);
116
153
  return dataSpace;