@dxos/client-services 0.1.31 → 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 (52) hide show
  1. package/dist/lib/browser/{chunk-DFO3FW2W.mjs → chunk-E3FD43YN.mjs} +423 -209
  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 +3 -3
  6. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/index.cjs +424 -210
  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 +432 -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 +24 -4
  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.map +1 -1
  25. package/dist/types/src/packlets/testing/host-test-builder.d.ts +2 -2
  26. package/dist/types/src/packlets/testing/host-test-builder.d.ts.map +1 -1
  27. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +1 -0
  28. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -1
  29. package/package.json +30 -30
  30. package/src/packlets/identity/identity-manager.ts +32 -17
  31. package/src/packlets/invitations/device-invitations-handler.ts +5 -3
  32. package/src/packlets/invitations/space-invitations-handler.test.ts +20 -3
  33. package/src/packlets/invitations/space-invitations-handler.ts +15 -8
  34. package/src/packlets/services/service-context.test.ts +6 -5
  35. package/src/packlets/services/service-host.ts +7 -2
  36. package/src/packlets/spaces/data-space-manager.test.ts +17 -2
  37. package/src/packlets/spaces/data-space-manager.ts +76 -30
  38. package/src/packlets/spaces/data-space.ts +90 -15
  39. package/src/packlets/spaces/notarization-plugin.test.ts +3 -1
  40. package/src/packlets/spaces/notarization-plugin.ts +64 -23
  41. package/src/packlets/spaces/spaces-service.test.ts +1 -1
  42. package/src/packlets/spaces/spaces-service.ts +28 -17
  43. package/src/packlets/testing/host-test-builder.ts +2 -2
  44. package/src/packlets/testing/test-builder.ts +2 -2
  45. package/src/packlets/tests/client-services.test.ts +24 -11
  46. package/src/packlets/tests/client.test.ts +3 -6
  47. package/src/packlets/tests/halo-profile.test.ts +5 -5
  48. package/src/packlets/tests/spaces-invitations.test.ts +4 -4
  49. package/src/packlets/tests/spaces.test.ts +20 -11
  50. package/src/packlets/tests/text.test.ts +6 -6
  51. package/src/packlets/vault/vault-resource-lock.ts +10 -1
  52. package/dist/lib/browser/chunk-DFO3FW2W.mjs.map +0 -7
@@ -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;
@@ -11,7 +11,7 @@ import { AuthStatus, DataServiceSubscriptions } from '@dxos/echo-pipeline';
11
11
  import { writeMessages } from '@dxos/feed-store';
12
12
  import { log } from '@dxos/log';
13
13
  import { ModelFactory } from '@dxos/model-factory';
14
- import { test, describe } from '@dxos/test';
14
+ import { test, describe, afterTest } from '@dxos/test';
15
15
 
16
16
  import { createSigningContext, HostTestBuilder, syncItemsLocal } from '../testing';
17
17
  import { DataSpaceManager } from './data-space-manager';
@@ -32,6 +32,8 @@ describe('DataSpaceManager', () => {
32
32
  peer.feedStore,
33
33
  peer.snapshotStore
34
34
  );
35
+ await dataSpaceManager.open();
36
+ afterTest(() => dataSpaceManager.close());
35
37
  const space = await dataSpaceManager.createSpace();
36
38
 
37
39
  // Process all written mutations.
@@ -58,6 +60,8 @@ describe('DataSpaceManager', () => {
58
60
  peer1.feedStore,
59
61
  peer1.snapshotStore
60
62
  );
63
+ await dataSpaceManager1.open();
64
+ afterTest(() => dataSpaceManager1.close());
61
65
 
62
66
  const peer2 = builder.createPeer();
63
67
  const identity2 = await createSigningContext(peer2.keyring);
@@ -71,6 +75,8 @@ describe('DataSpaceManager', () => {
71
75
  peer2.feedStore,
72
76
  peer1.snapshotStore
73
77
  );
78
+ await dataSpaceManager2.open();
79
+ afterTest(() => dataSpaceManager2.close());
74
80
 
75
81
  const space1 = await dataSpaceManager1.createSpace();
76
82
  await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
@@ -91,6 +97,7 @@ describe('DataSpaceManager', () => {
91
97
  spaceKey: space1.key,
92
98
  genesisFeedKey: space1.inner.genesisFeedKey
93
99
  });
100
+ await dataSpaceManager2.waitUntilSpaceReady(space2.key);
94
101
 
95
102
  log('', {
96
103
  peer1: {
@@ -122,7 +129,7 @@ describe('DataSpaceManager', () => {
122
129
  });
123
130
  log.break();
124
131
 
125
- await syncItemsLocal(space1.dataPipelineController, space2.dataPipelineController);
132
+ await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
126
133
 
127
134
  expect(space1.inner.protocol.sessions.get(identity2.deviceKey)).to.exist;
128
135
  expect(space1.inner.protocol.sessions.get(identity2.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
@@ -145,6 +152,8 @@ describe('DataSpaceManager', () => {
145
152
  peer1.feedStore,
146
153
  peer1.snapshotStore
147
154
  );
155
+ await dataSpaceManager1.open();
156
+ afterTest(() => dataSpaceManager1.close());
148
157
 
149
158
  const peer2 = builder.createPeer();
150
159
  const identity2 = await createSigningContext(peer2.keyring);
@@ -158,6 +167,8 @@ describe('DataSpaceManager', () => {
158
167
  peer2.feedStore,
159
168
  peer1.snapshotStore
160
169
  );
170
+ await dataSpaceManager2.open();
171
+ afterTest(() => dataSpaceManager2.close());
161
172
 
162
173
  const space1 = await dataSpaceManager1.createSpace();
163
174
  await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
@@ -179,6 +190,10 @@ describe('DataSpaceManager', () => {
179
190
  genesisFeedKey: space1.inner.genesisFeedKey
180
191
  });
181
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
+
182
197
  const [receivedMessage, inc] = latch({ count: 1 });
183
198
  space2.listen('test', (message) => {
184
199
  expect(message.channelId).to.equal('test');
@@ -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';
@@ -25,16 +26,27 @@ 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
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,26 +136,43 @@ 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 });
153
176
  const gossip = new Gossip({
154
177
  localPeerId: this._signingContext.deviceKey
155
178
  });
@@ -195,10 +218,33 @@ export class DataSpaceManager {
195
218
  keyring: this._keyring,
196
219
  feedStore: this._feedStore,
197
220
  signingContext: this._signingContext,
198
- 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
+ }
199
237
  });
200
238
 
201
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
+
202
248
  this._spaces.set(metadata.key, dataSpace);
203
249
  return dataSpace;
204
250
  }
@@ -2,21 +2,24 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { trackLeaks } from '@dxos/async';
5
+ import { Event, scheduleTask, trackLeaks } from '@dxos/async';
6
+ import { SpaceState } from '@dxos/client';
6
7
  import { Context } from '@dxos/context';
7
8
  import { CredentialConsumer } from '@dxos/credentials';
8
9
  import { timed } from '@dxos/debug';
9
10
  import {
10
- DataPipelineControllerImpl,
11
11
  MetadataStore,
12
12
  Space,
13
13
  SigningContext,
14
14
  createMappedFeedWriter,
15
- SnapshotManager
15
+ SnapshotManager,
16
+ DataPipeline
16
17
  } from '@dxos/echo-pipeline';
18
+ import { CancelledError, SystemError } from '@dxos/errors';
17
19
  import { FeedStore } from '@dxos/feed-store';
18
20
  import { Keyring } from '@dxos/keyring';
19
21
  import { PublicKey } from '@dxos/keys';
22
+ import { log } from '@dxos/log';
20
23
  import { ModelFactory } from '@dxos/model-factory';
21
24
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
25
  import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
@@ -29,6 +32,18 @@ import { NotarizationPlugin } from './notarization-plugin';
29
32
 
30
33
  const AUTH_TIMEOUT = 30000;
31
34
 
35
+ export type DataSpaceCallbacks = {
36
+ /**
37
+ * Called before transitioning to the ready state.
38
+ */
39
+ beforeReady?: () => Promise<void>;
40
+
41
+ /**
42
+ * Called after transitioning to the ready state.
43
+ */
44
+ afterReady?: () => Promise<void>;
45
+ };
46
+
32
47
  export type DataSpaceParams = {
33
48
  inner: Space;
34
49
  modelFactory: ModelFactory;
@@ -41,13 +56,13 @@ export type DataSpaceParams = {
41
56
  signingContext: SigningContext;
42
57
  memberKey: PublicKey;
43
58
  snapshotId?: string | undefined;
59
+ callbacks?: DataSpaceCallbacks;
44
60
  };
45
61
 
46
- const CONTROL_PIPELINE_READY_TIMEFRAME = 3000;
47
62
  @trackLeaks('open', 'close')
48
63
  export class DataSpace {
49
64
  private readonly _ctx = new Context();
50
- private readonly _dataPipelineController: DataPipelineControllerImpl;
65
+ private readonly _dataPipeline: DataPipeline;
51
66
  private readonly _inner: Space;
52
67
  private readonly _gossip: Gossip;
53
68
  private readonly _presence: Presence;
@@ -56,18 +71,25 @@ export class DataSpace {
56
71
  private readonly _metadataStore: MetadataStore;
57
72
  private readonly _signingContext: SigningContext;
58
73
  private readonly _notarizationPluginConsumer: CredentialConsumer<NotarizationPlugin>;
74
+ private readonly _callbacks: DataSpaceCallbacks;
75
+
76
+ private _state = SpaceState.CLOSED;
59
77
 
60
78
  public readonly authVerifier: TrustedKeySetAuthVerifier;
79
+ public readonly stateUpdate = new Event();
61
80
 
62
81
  constructor(params: DataSpaceParams) {
63
82
  this._inner = params.inner;
83
+ this._inner.stateUpdate.on(this._ctx, () => this.stateUpdate.emit());
84
+
64
85
  this._gossip = params.gossip;
65
86
  this._presence = params.presence;
66
87
  this._keyring = params.keyring;
67
88
  this._feedStore = params.feedStore;
68
89
  this._metadataStore = params.metadataStore;
69
90
  this._signingContext = params.signingContext;
70
- this._dataPipelineController = new DataPipelineControllerImpl({
91
+ this._callbacks = params.callbacks ?? {};
92
+ this._dataPipeline = new DataPipeline({
71
93
  modelFactory: params.modelFactory,
72
94
  metadataStore: params.metadataStore,
73
95
  snapshotManager: params.snapshotManager,
@@ -94,17 +116,17 @@ export class DataSpace {
94
116
  return this._inner.isOpen;
95
117
  }
96
118
 
119
+ get state(): SpaceState {
120
+ return this._state;
121
+ }
122
+
97
123
  // TODO(burdon): Can we mark this for debugging only?
98
124
  get inner() {
99
125
  return this._inner;
100
126
  }
101
127
 
102
- get dataPipelineController(): DataPipelineControllerImpl {
103
- return this._dataPipelineController;
104
- }
105
-
106
- get stateUpdate() {
107
- return this._inner.stateUpdate;
128
+ get dataPipeline(): DataPipeline {
129
+ return this._dataPipeline;
108
130
  }
109
131
 
110
132
  get presence() {
@@ -119,11 +141,14 @@ export class DataSpace {
119
141
  await this.notarizationPlugin.open();
120
142
  await this._notarizationPluginConsumer.open();
121
143
  await this._inner.open();
144
+ this._state = SpaceState.INACTIVE;
122
145
  }
123
146
 
124
147
  async close() {
148
+ this._state = SpaceState.CLOSED;
125
149
  await this._ctx.dispose();
126
150
 
151
+ await this._dataPipeline.close();
127
152
  await this.authVerifier.close();
128
153
 
129
154
  await this._inner.close();
@@ -141,12 +166,40 @@ export class DataSpace {
141
166
  return this._gossip.listen(channel, callback);
142
167
  }
143
168
 
169
+ /**
170
+ * Initialize the data pipeline in a separate task.
171
+ */
172
+ initializeDataPipelineAsync() {
173
+ scheduleTask(this._ctx, async () => {
174
+ try {
175
+ await this.initializeDataPipeline();
176
+ } catch (err) {
177
+ if (err instanceof CancelledError) {
178
+ log('Data pipeline initialization cancelled', err);
179
+ return;
180
+ }
181
+
182
+ log.error('Error initializing data pipeline', err);
183
+ }
184
+ });
185
+ }
186
+
144
187
  async initializeDataPipeline() {
188
+ if (this._state !== SpaceState.INACTIVE) {
189
+ throw new SystemError('Invalid operation');
190
+ }
191
+ this._state = SpaceState.INITIALIZING;
192
+
193
+ // TODO(dmaretskyi): Cancel with context.
145
194
  await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
146
- timeout: CONTROL_PIPELINE_READY_TIMEFRAME
195
+ ctx: this._ctx,
196
+ breakOnStall: false
147
197
  });
148
198
 
149
199
  await this._createWritableFeeds();
200
+ log('Writable feeds created');
201
+ this.stateUpdate.emit();
202
+
150
203
  this.notarizationPlugin.setWriter(
151
204
  createMappedFeedWriter<Credential, FeedMessage.Payload>(
152
205
  (credential) => ({
@@ -156,7 +209,28 @@ export class DataSpace {
156
209
  )
157
210
  );
158
211
 
159
- await this._inner.initDataPipeline(this._dataPipelineController);
212
+ await this._dataPipeline.open({
213
+ openPipeline: async (start) => {
214
+ const pipeline = await this._inner.createDataPipeline({ start });
215
+ await pipeline.start();
216
+ return pipeline;
217
+ }
218
+ });
219
+
220
+ log('waiting for data pipeline to reach target timeframe');
221
+ // Wait for the data pipeline to catch up to its desired timeframe.
222
+ await this._dataPipeline.pipelineState!.waitUntilReachedTargetTimeframe({
223
+ ctx: this._ctx,
224
+ breakOnStall: false
225
+ });
226
+
227
+ log('data pipeline ready');
228
+ await this._callbacks.beforeReady?.();
229
+
230
+ this._state = SpaceState.READY;
231
+ this.stateUpdate.emit();
232
+
233
+ await this._callbacks.afterReady?.();
160
234
  }
161
235
 
162
236
  @timed(10_000)
@@ -198,7 +272,8 @@ export class DataSpace {
198
272
  }
199
273
 
200
274
  if (credentials.length > 0) {
201
- await this.notarizationPlugin.notarize(credentials);
275
+ // Never times out
276
+ await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
202
277
  }
203
278
 
204
279
  // Set this after credentials are notarized so that on failure we will retry.
@@ -70,7 +70,9 @@ describe('NotarizationPlugin', () => {
70
70
  AdmittedFeed.Designation.CONTROL
71
71
  );
72
72
 
73
- const notarized = peer2.notarizationPlugin.notarize([credential]);
73
+ const notarized = peer2.notarizationPlugin.notarize({
74
+ credentials: [credential]
75
+ });
74
76
 
75
77
  await testBuilder.connect(peer1, peer2);
76
78
  await notarized;