@dxos/client-services 0.1.31-next.ea8f0bc → 0.1.32-next.77d513e

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 (57) hide show
  1. package/dist/lib/browser/{chunk-ZIQUYEJS.mjs → chunk-E3FD43YN.mjs} +460 -213
  2. package/dist/lib/browser/chunk-E3FD43YN.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 +64 -6
  6. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/index.cjs +462 -215
  8. package/dist/lib/node/index.cjs.map +3 -3
  9. package/dist/lib/node/meta.json +1 -1
  10. package/dist/lib/node/packlets/testing/index.cjs +524 -218
  11. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  12. package/dist/types/src/packlets/identity/identity-manager.d.ts +6 -0
  13. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  14. package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts.map +1 -1
  15. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
  16. package/dist/types/src/packlets/services/service-host.d.ts +1 -0
  17. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  18. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +19 -1
  19. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  20. package/dist/types/src/packlets/spaces/data-space.d.ts +32 -5
  21. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  22. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +29 -1
  23. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  24. package/dist/types/src/packlets/spaces/spaces-service.d.ts +4 -1
  25. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  26. package/dist/types/src/packlets/testing/host-test-builder.d.ts +2 -2
  27. package/dist/types/src/packlets/testing/host-test-builder.d.ts.map +1 -1
  28. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -0
  29. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  30. package/dist/types/src/packlets/tests/text.test.d.ts +2 -0
  31. package/dist/types/src/packlets/tests/text.test.d.ts.map +1 -0
  32. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +1 -0
  33. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -1
  34. package/package.json +31 -30
  35. package/src/packlets/identity/identity-manager.ts +32 -17
  36. package/src/packlets/invitations/device-invitations-handler.ts +5 -3
  37. package/src/packlets/invitations/space-invitations-handler.test.ts +20 -3
  38. package/src/packlets/invitations/space-invitations-handler.ts +15 -8
  39. package/src/packlets/services/service-context.test.ts +6 -5
  40. package/src/packlets/services/service-host.ts +7 -2
  41. package/src/packlets/spaces/data-space-manager.test.ts +77 -2
  42. package/src/packlets/spaces/data-space-manager.ts +87 -37
  43. package/src/packlets/spaces/data-space.ts +103 -16
  44. package/src/packlets/spaces/notarization-plugin.test.ts +3 -1
  45. package/src/packlets/spaces/notarization-plugin.ts +67 -19
  46. package/src/packlets/spaces/spaces-service.test.ts +1 -1
  47. package/src/packlets/spaces/spaces-service.ts +53 -17
  48. package/src/packlets/testing/host-test-builder.ts +2 -2
  49. package/src/packlets/testing/test-builder.ts +45 -5
  50. package/src/packlets/tests/client-services.test.ts +24 -11
  51. package/src/packlets/tests/client.test.ts +3 -6
  52. package/src/packlets/tests/halo-profile.test.ts +5 -5
  53. package/src/packlets/tests/spaces-invitations.test.ts +47 -7
  54. package/src/packlets/tests/spaces.test.ts +86 -9
  55. package/src/packlets/tests/text.test.ts +206 -0
  56. package/src/packlets/vault/vault-resource-lock.ts +10 -1
  57. package/dist/lib/browser/chunk-ZIQUYEJS.mjs.map +0 -7
@@ -28,6 +28,17 @@ describe('services/space-invitations-handler', () => {
28
28
 
29
29
  const space = await peer.dataSpaceManager!.createSpace();
30
30
  expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
31
+
32
+ await space.close();
33
+ });
34
+
35
+ test('genesis & ready', async () => {
36
+ const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
37
+
38
+ const space = await peer.dataSpaceManager!.createSpace();
39
+ expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
40
+
41
+ await peer.dataSpaceManager?.waitUntilSpaceReady(space.key);
31
42
  await space.close();
32
43
  });
33
44
 
@@ -36,7 +47,7 @@ describe('services/space-invitations-handler', () => {
36
47
  const space = await peer.dataSpaceManager!.createSpace();
37
48
  afterTest(() => space.close());
38
49
 
39
- await testLocalDatabase(space.dataPipelineController);
50
+ await testLocalDatabase(space.dataPipeline);
40
51
  });
41
52
 
42
53
  test('invitation with no auth', async () => {
@@ -53,7 +64,10 @@ describe('services/space-invitations-handler', () => {
53
64
  expect(space1).not.to.be.undefined;
54
65
  expect(space2).not.to.be.undefined;
55
66
 
56
- await syncItemsLocal(space1.dataPipelineController, space2.dataPipelineController);
67
+ await host.dataSpaceManager?.waitUntilSpaceReady(space1.key);
68
+ await guest.dataSpaceManager?.waitUntilSpaceReady(space2.key);
69
+
70
+ await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
57
71
 
58
72
  await space1.close();
59
73
  await space2.close();
@@ -116,7 +130,10 @@ describe('services/space-invitations-handler', () => {
116
130
  expect(space1).not.to.be.undefined;
117
131
  expect(space2).not.to.be.undefined;
118
132
 
119
- await syncItemsLocal(space1.dataPipelineController, space2.dataPipelineController);
133
+ await host.dataSpaceManager?.waitUntilSpaceReady(space1.key);
134
+ await guest.dataSpaceManager?.waitUntilSpaceReady(space2.key);
135
+
136
+ await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
120
137
 
121
138
  await space1.close();
122
139
  await space2.close();
@@ -177,7 +177,11 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
177
177
  // Updating credentials complete.
178
178
  complete.wake(deviceKey);
179
179
 
180
- return { credential: spaceMemberCredential };
180
+ return {
181
+ credential: spaceMemberCredential,
182
+ controlTimeframe: space.inner.controlPipeline.state.timeframe,
183
+ dataTimeframe: space.dataPipeline.pipelineState?.timeframe
184
+ };
181
185
  } catch (err) {
182
186
  // TODO(burdon): Generic RPC callback to report error to client.
183
187
  observable.callback.onError(err);
@@ -316,12 +320,13 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
316
320
 
317
321
  // 4. Send admission credentials to host (with local space keys).
318
322
  log('request admission', { guest: this._signingContext.deviceKey });
319
- const { credential } = await extension.rpc.SpaceHostService.requestAdmission({
320
- identityKey: this._signingContext.identityKey,
321
- deviceKey: this._signingContext.deviceKey,
322
- controlFeedKey,
323
- dataFeedKey
324
- });
323
+ const { credential, controlTimeframe, dataTimeframe } =
324
+ await extension.rpc.SpaceHostService.requestAdmission({
325
+ identityKey: this._signingContext.identityKey,
326
+ deviceKey: this._signingContext.deviceKey,
327
+ controlFeedKey,
328
+ dataFeedKey
329
+ });
325
330
 
326
331
  // 4. Create local space.
327
332
  const assertion = getCredentialAssertion(credential);
@@ -330,7 +335,9 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
330
335
 
331
336
  const space = await this._spaceManager.acceptSpace({
332
337
  spaceKey: assertion.spaceKey,
333
- genesisFeedKey: assertion.genesisFeedKey
338
+ genesisFeedKey: assertion.genesisFeedKey,
339
+ controlTimeframe,
340
+ dataTimeframe
334
341
  });
335
342
 
336
343
  // Record credential in our HALO.
@@ -18,8 +18,9 @@ describe('services/ServiceContext', () => {
18
18
  const device2 = createServiceContext({ signalContext: networkContext });
19
19
  await performInvitation(device1.deviceInvitations, device2.deviceInvitations, undefined);
20
20
 
21
+ await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
21
22
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
22
- await syncItemsLocal(space1.dataPipelineController, space2!.dataPipelineController);
23
+ await syncItemsLocal(space1.dataPipeline, space2!.dataPipeline);
23
24
  });
24
25
 
25
26
  test('new space is synchronized on device invitations', async () => {
@@ -31,9 +32,9 @@ describe('services/ServiceContext', () => {
31
32
  await performInvitation(device1.deviceInvitations, device2.deviceInvitations, undefined);
32
33
 
33
34
  const space1 = await device1.dataSpaceManager!.createSpace();
34
- await device2.dataSpaceManager?.updated.waitForCondition(() => !!device2.dataSpaceManager!.spaces.get(space1.key));
35
+ await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
35
36
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
36
- await syncItemsLocal(space1.dataPipelineController, space2!.dataPipelineController);
37
+ await syncItemsLocal(space1.dataPipeline, space2!.dataPipeline);
37
38
  });
38
39
 
39
40
  test('joined space is synchronized on device invitations', async () => {
@@ -49,8 +50,8 @@ describe('services/ServiceContext', () => {
49
50
  const space1 = await identity2.dataSpaceManager!.createSpace();
50
51
  await performInvitation(identity2.spaceInvitations!, device1.spaceInvitations!, space1);
51
52
 
52
- await device2.dataSpaceManager?.updated.waitForCondition(() => !!device2.dataSpaceManager!.spaces.get(space1.key));
53
+ await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
53
54
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
54
- await syncItemsLocal(space1.dataPipelineController, space2!.dataPipelineController);
55
+ await syncItemsLocal(space1.dataPipeline, space2!.dataPipeline);
55
56
  });
56
57
  });
@@ -62,6 +62,7 @@ export class ClientServicesHost {
62
62
  * @internal
63
63
  */
64
64
  _serviceContext!: ServiceContext;
65
+ private _opening = false;
65
66
  private _open = false;
66
67
 
67
68
  constructor({
@@ -83,7 +84,9 @@ export class ClientServicesHost {
83
84
  this._resourceLock = lockKey
84
85
  ? new VaultResourceLock({
85
86
  lockKey,
86
- onAcquire: () => this.open(),
87
+ onAcquire: () => {
88
+ this._opening || this.open();
89
+ },
87
90
  onRelease: () => this.close()
88
91
  })
89
92
  : undefined;
@@ -163,7 +166,8 @@ export class ClientServicesHost {
163
166
  assert(this._storage, 'storage not set');
164
167
  assert(this._networkManager, 'network manager not set');
165
168
 
166
- log('opening...');
169
+ this._opening = true;
170
+ log('opening...', { lockKey: this._resourceLock?.lockKey });
167
171
  await this._resourceLock?.acquire();
168
172
 
169
173
  // TODO(wittjosiah): Make re-entrant.
@@ -213,6 +217,7 @@ export class ClientServicesHost {
213
217
  });
214
218
 
215
219
  await this._serviceContext.open();
220
+ this._opening = false;
216
221
  this._open = true;
217
222
  this._statusUpdate.emit();
218
223
  const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
@@ -4,13 +4,14 @@
4
4
 
5
5
  import { expect } from 'chai';
6
6
 
7
+ import { latch } from '@dxos/async';
7
8
  import { createAdmissionCredentials } from '@dxos/credentials';
8
9
  import { DocumentModel } from '@dxos/document-model';
9
10
  import { AuthStatus, DataServiceSubscriptions } from '@dxos/echo-pipeline';
10
11
  import { writeMessages } from '@dxos/feed-store';
11
12
  import { log } from '@dxos/log';
12
13
  import { ModelFactory } from '@dxos/model-factory';
13
- import { test, describe } from '@dxos/test';
14
+ import { test, describe, afterTest } from '@dxos/test';
14
15
 
15
16
  import { createSigningContext, HostTestBuilder, syncItemsLocal } from '../testing';
16
17
  import { DataSpaceManager } from './data-space-manager';
@@ -31,6 +32,8 @@ describe('DataSpaceManager', () => {
31
32
  peer.feedStore,
32
33
  peer.snapshotStore
33
34
  );
35
+ await dataSpaceManager.open();
36
+ afterTest(() => dataSpaceManager.close());
34
37
  const space = await dataSpaceManager.createSpace();
35
38
 
36
39
  // Process all written mutations.
@@ -57,6 +60,8 @@ describe('DataSpaceManager', () => {
57
60
  peer1.feedStore,
58
61
  peer1.snapshotStore
59
62
  );
63
+ await dataSpaceManager1.open();
64
+ afterTest(() => dataSpaceManager1.close());
60
65
 
61
66
  const peer2 = builder.createPeer();
62
67
  const identity2 = await createSigningContext(peer2.keyring);
@@ -70,6 +75,8 @@ describe('DataSpaceManager', () => {
70
75
  peer2.feedStore,
71
76
  peer1.snapshotStore
72
77
  );
78
+ await dataSpaceManager2.open();
79
+ afterTest(() => dataSpaceManager2.close());
73
80
 
74
81
  const space1 = await dataSpaceManager1.createSpace();
75
82
  await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
@@ -90,6 +97,7 @@ describe('DataSpaceManager', () => {
90
97
  spaceKey: space1.key,
91
98
  genesisFeedKey: space1.inner.genesisFeedKey
92
99
  });
100
+ await dataSpaceManager2.waitUntilSpaceReady(space2.key);
93
101
 
94
102
  log('', {
95
103
  peer1: {
@@ -121,11 +129,78 @@ describe('DataSpaceManager', () => {
121
129
  });
122
130
  log.break();
123
131
 
124
- await syncItemsLocal(space1.dataPipelineController, space2.dataPipelineController);
132
+ await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
125
133
 
126
134
  expect(space1.inner.protocol.sessions.get(identity2.deviceKey)).to.exist;
127
135
  expect(space1.inner.protocol.sessions.get(identity2.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
128
136
  expect(space2.inner.protocol.sessions.get(identity1.deviceKey)).to.exist;
129
137
  expect(space2.inner.protocol.sessions.get(identity1.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
130
138
  });
139
+
140
+ test('pub/sub API', async () => {
141
+ const builder = new HostTestBuilder();
142
+
143
+ const peer1 = builder.createPeer();
144
+ const identity1 = await createSigningContext(peer1.keyring);
145
+ const dataSpaceManager1 = new DataSpaceManager(
146
+ peer1.spaceManager,
147
+ peer1.metadataStore,
148
+ new DataServiceSubscriptions(),
149
+ peer1.keyring,
150
+ identity1,
151
+ new ModelFactory().registerModel(DocumentModel),
152
+ peer1.feedStore,
153
+ peer1.snapshotStore
154
+ );
155
+ await dataSpaceManager1.open();
156
+ afterTest(() => dataSpaceManager1.close());
157
+
158
+ const peer2 = builder.createPeer();
159
+ const identity2 = await createSigningContext(peer2.keyring);
160
+ const dataSpaceManager2 = new DataSpaceManager(
161
+ peer2.spaceManager,
162
+ peer2.metadataStore,
163
+ new DataServiceSubscriptions(),
164
+ peer2.keyring,
165
+ identity2,
166
+ new ModelFactory().registerModel(DocumentModel),
167
+ peer2.feedStore,
168
+ peer1.snapshotStore
169
+ );
170
+ await dataSpaceManager2.open();
171
+ afterTest(() => dataSpaceManager2.close());
172
+
173
+ const space1 = await dataSpaceManager1.createSpace();
174
+ await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
175
+
176
+ // Admit peer2 to space1.
177
+ await writeMessages(
178
+ space1.inner.controlPipeline.writer,
179
+ await createAdmissionCredentials(
180
+ identity1.credentialSigner,
181
+ identity2.identityKey,
182
+ space1.key,
183
+ space1.inner.genesisFeedKey
184
+ )
185
+ );
186
+
187
+ // Accept must be called after admission so that the peer can authenticate for notarization.
188
+ const space2 = await dataSpaceManager2.acceptSpace({
189
+ spaceKey: space1.key,
190
+ genesisFeedKey: space1.inner.genesisFeedKey
191
+ });
192
+
193
+ // Coincidentally, this also waits until a P2P connection is established between peers.
194
+ // TODO(dmaretskyi): Refine this to wait for connection specifically.
195
+ await dataSpaceManager2.waitUntilSpaceReady(space2.key);
196
+
197
+ const [receivedMessage, inc] = latch({ count: 1 });
198
+ space2.listen('test', (message) => {
199
+ expect(message.channelId).to.equal('test');
200
+ inc();
201
+ });
202
+
203
+ await space1.postMessage('test', { '@type': 'google.protobuf.Any', test: true });
204
+ await receivedMessage();
205
+ });
131
206
  });
@@ -5,17 +5,18 @@
5
5
  import assert from 'node:assert';
6
6
 
7
7
  import { Event, synchronized, trackLeaks } from '@dxos/async';
8
- import { Context } from '@dxos/context';
8
+ import { SpaceState } from '@dxos/client';
9
+ import { cancelWithContext, Context } from '@dxos/context';
9
10
  import { getCredentialAssertion } from '@dxos/credentials';
10
11
  import {
12
+ DataServiceSubscriptions,
11
13
  MetadataStore,
12
14
  SigningContext,
15
+ SnapshotManager,
16
+ SnapshotStore,
13
17
  Space,
14
18
  spaceGenesis,
15
- SpaceManager,
16
- DataServiceSubscriptions,
17
- SnapshotManager,
18
- SnapshotStore
19
+ SpaceManager
19
20
  } from '@dxos/echo-pipeline';
20
21
  import { FeedStore } from '@dxos/feed-store';
21
22
  import { Keyring } from '@dxos/keyring';
@@ -24,17 +25,28 @@ import { log } from '@dxos/log';
24
25
  import { ModelFactory } from '@dxos/model-factory';
25
26
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
26
27
  import { SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
27
- import { Presence } from '@dxos/teleport-extension-presence';
28
+ import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
29
+ import { Timeframe } from '@dxos/timeframe';
28
30
  import { ComplexMap, deferFunction } from '@dxos/util';
29
31
 
30
32
  import { createAuthProvider } from '../identity';
31
33
  import { DataSpace } from './data-space';
32
34
 
33
- const DATA_PIPELINE_READY_TIMEOUT = 3_000;
34
-
35
35
  export type AcceptSpaceOptions = {
36
36
  spaceKey: PublicKey;
37
37
  genesisFeedKey: PublicKey;
38
+
39
+ /**
40
+ * Latest known timeframe for the control pipeline.
41
+ * We will try to catch up to this timeframe before starting the data pipeline.
42
+ */
43
+ controlTimeframe?: Timeframe;
44
+
45
+ /**
46
+ * Latest known timeframe for the data pipeline.
47
+ * We will try to catch up to this timeframe before initializing the database.
48
+ */
49
+ dataTimeframe?: Timeframe;
38
50
  };
39
51
 
40
52
  @trackLeaks('open', 'close')
@@ -45,6 +57,8 @@ export class DataSpaceManager {
45
57
 
46
58
  private readonly _spaces = new ComplexMap<PublicKey, DataSpace>(PublicKey.hash);
47
59
 
60
+ private _isOpen = false;
61
+
48
62
  constructor(
49
63
  private readonly _spaceManager: SpaceManager,
50
64
  private readonly _metadataStore: MetadataStore,
@@ -63,29 +77,24 @@ export class DataSpaceManager {
63
77
 
64
78
  @synchronized
65
79
  async open() {
80
+ log('open');
66
81
  await this._metadataStore.load();
67
82
  log('metadata loaded', { spaces: this._metadataStore.spaces.length });
68
83
 
69
84
  for (const spaceMetadata of this._metadataStore.spaces) {
70
85
  log('load space', { spaceMetadata });
71
86
  const space = await this._constructSpace(spaceMetadata);
72
- await space.initializeDataPipeline();
73
- if (spaceMetadata.dataTimeframe) {
74
- log('waiting for latest timeframe', { spaceMetadata });
75
- await space.dataPipelineController.pipelineState!.setTargetTimeframe(spaceMetadata.dataTimeframe);
76
- }
77
- await space.dataPipelineController.pipelineState!.waitUntilReachedTargetTimeframe({
78
- timeout: DATA_PIPELINE_READY_TIMEOUT
79
- });
80
- this._dataServiceSubscriptions.registerSpace(
81
- space.key,
82
- space.dataPipelineController.databaseBackend!.createDataServiceHost()
83
- );
87
+ space.initializeDataPipelineAsync();
84
88
  }
89
+
90
+ this._isOpen = true;
91
+ this.updated.emit();
85
92
  }
86
93
 
87
94
  @synchronized
88
95
  async close() {
96
+ log('close');
97
+ this._isOpen = false;
89
98
  await this._ctx.dispose();
90
99
  for (const space of this._spaces.values()) {
91
100
  await space.close();
@@ -97,6 +106,7 @@ export class DataSpaceManager {
97
106
  */
98
107
  @synchronized
99
108
  async createSpace() {
109
+ assert(this._isOpen, 'Not open.');
100
110
  const spaceKey = await this._keyring.createKey();
101
111
  const controlFeedKey = await this._keyring.createKey();
102
112
  const dataFeedKey = await this._keyring.createKey();
@@ -118,10 +128,6 @@ export class DataSpaceManager {
118
128
  await this._signingContext.recordCredential(memberCredential);
119
129
 
120
130
  await space.initializeDataPipeline();
121
- this._dataServiceSubscriptions.registerSpace(
122
- space.key,
123
- space.dataPipelineController.databaseBackend!.createDataServiceHost()
124
- );
125
131
 
126
132
  this.updated.emit();
127
133
  return space;
@@ -130,31 +136,51 @@ export class DataSpaceManager {
130
136
  // TODO(burdon): Rename join space.
131
137
  @synchronized
132
138
  async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
139
+ log('accept space', { opts });
140
+ assert(this._isOpen, 'Not open.');
133
141
  assert(!this._spaces.has(opts.spaceKey), 'Space already exists.');
134
142
 
135
143
  const metadata: SpaceMetadata = {
136
144
  key: opts.spaceKey,
137
- genesisFeedKey: opts.genesisFeedKey
145
+ genesisFeedKey: opts.genesisFeedKey,
146
+ controlTimeframe: opts.controlTimeframe,
147
+ dataTimeframe: opts.dataTimeframe
138
148
  };
139
149
 
140
150
  const space = await this._constructSpace(metadata);
141
151
  await this._metadataStore.addSpace(metadata);
142
- await space.initializeDataPipeline();
143
- this._dataServiceSubscriptions.registerSpace(
144
- space.key,
145
- space.dataPipelineController.databaseBackend!.createDataServiceHost()
146
- );
152
+
153
+ space.initializeDataPipelineAsync();
147
154
 
148
155
  this.updated.emit();
149
156
  return space;
150
157
  }
151
158
 
159
+ /**
160
+ * Wait until the space data pipeline is fully initialized.
161
+ * Used by invitation handler.
162
+ * TODO(dmaretskyi): Consider removing.
163
+ */
164
+ async waitUntilSpaceReady(spaceKey: PublicKey) {
165
+ await cancelWithContext(
166
+ this._ctx,
167
+ this.updated.waitForCondition(() => {
168
+ const space = this._spaces.get(spaceKey);
169
+ return !!space && space.state === SpaceState.READY;
170
+ })
171
+ );
172
+ }
173
+
152
174
  private async _constructSpace(metadata: SpaceMetadata) {
175
+ log('construct space', { metadata });
176
+ const gossip = new Gossip({
177
+ localPeerId: this._signingContext.deviceKey
178
+ });
153
179
  const presence = new Presence({
154
- localPeerId: this._signingContext.deviceKey,
155
- announceInterval: 1_000,
156
- offlineTimeout: 10_000, // TODO(burdon): Config.
157
- identityKey: this._signingContext.identityKey
180
+ announceInterval: 500,
181
+ offlineTimeout: 5_000, // TODO(burdon): Config.
182
+ identityKey: this._signingContext.identityKey,
183
+ gossip
158
184
  });
159
185
  const snapshotManager = new SnapshotManager(this._snapshotStore);
160
186
 
@@ -171,8 +197,8 @@ export class DataSpaceManager {
171
197
  },
172
198
  onNetworkConnection: (session) => {
173
199
  session.addExtension(
174
- 'dxos.mesh.teleport.presence',
175
- presence.createExtension({ remotePeerId: session.remotePeerId })
200
+ 'dxos.mesh.teleport.gossip',
201
+ gossip.createExtension({ remotePeerId: session.remotePeerId })
176
202
  );
177
203
  session.addExtension('dxos.mesh.teleport.objectsync', snapshotManager.objectSync.createExtension());
178
204
  session.addExtension('dxos.mesh.teleport.notarization', dataSpace.notarizationPlugin.createExtension());
@@ -186,15 +212,39 @@ export class DataSpaceManager {
186
212
  modelFactory: this._modelFactory,
187
213
  metadataStore: this._metadataStore,
188
214
  snapshotManager,
215
+ gossip,
189
216
  presence,
190
217
  memberKey: this._signingContext.identityKey,
191
218
  keyring: this._keyring,
192
219
  feedStore: this._feedStore,
193
220
  signingContext: this._signingContext,
194
- snapshotId: metadata.snapshot
221
+ snapshotId: metadata.snapshot,
222
+ callbacks: {
223
+ beforeReady: async () => {
224
+ log('before space ready', { space: space.key });
225
+ this._dataServiceSubscriptions.registerSpace(
226
+ space.key,
227
+ dataSpace.dataPipeline.databaseBackend!.createDataServiceHost()
228
+ );
229
+ },
230
+ afterReady: async () => {
231
+ log('after space ready', { space: space.key, open: this._isOpen });
232
+ if (this._isOpen) {
233
+ this.updated.emit();
234
+ }
235
+ }
236
+ }
195
237
  });
196
238
 
197
239
  await dataSpace.open();
240
+
241
+ if (metadata.controlTimeframe) {
242
+ dataSpace.inner.controlPipeline.state.setTargetTimeframe(metadata.controlTimeframe);
243
+ }
244
+ if (metadata.dataTimeframe) {
245
+ dataSpace.dataPipeline.setTargetTimeframe(metadata.dataTimeframe);
246
+ }
247
+
198
248
  this._spaces.set(metadata.key, dataSpace);
199
249
  return dataSpace;
200
250
  }