@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
@@ -2,25 +2,29 @@
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';
23
- import { Presence } from '@dxos/teleport-extension-presence';
26
+ import { GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';
27
+ import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
24
28
  import { ComplexSet } from '@dxos/util';
25
29
 
26
30
  import { TrustedKeySetAuthVerifier } from '../identity';
@@ -28,42 +32,64 @@ import { NotarizationPlugin } from './notarization-plugin';
28
32
 
29
33
  const AUTH_TIMEOUT = 30000;
30
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
+
31
47
  export type DataSpaceParams = {
32
48
  inner: Space;
33
49
  modelFactory: ModelFactory;
34
50
  metadataStore: MetadataStore;
35
51
  snapshotManager: SnapshotManager;
52
+ gossip: Gossip;
36
53
  presence: Presence;
37
54
  keyring: Keyring;
38
55
  feedStore: FeedStore<FeedMessage>;
39
56
  signingContext: SigningContext;
40
57
  memberKey: PublicKey;
41
58
  snapshotId?: string | undefined;
59
+ callbacks?: DataSpaceCallbacks;
42
60
  };
43
61
 
44
- const CONTROL_PIPELINE_READY_TIMEFRAME = 3000;
45
62
  @trackLeaks('open', 'close')
46
63
  export class DataSpace {
47
64
  private readonly _ctx = new Context();
48
- private readonly _dataPipelineController: DataPipelineControllerImpl;
65
+ private readonly _dataPipeline: DataPipeline;
49
66
  private readonly _inner: Space;
67
+ private readonly _gossip: Gossip;
50
68
  private readonly _presence: Presence;
51
69
  private readonly _keyring: Keyring;
52
70
  private readonly _feedStore: FeedStore<FeedMessage>;
53
71
  private readonly _metadataStore: MetadataStore;
54
72
  private readonly _signingContext: SigningContext;
55
73
  private readonly _notarizationPluginConsumer: CredentialConsumer<NotarizationPlugin>;
74
+ private readonly _callbacks: DataSpaceCallbacks;
75
+
76
+ private _state = SpaceState.CLOSED;
56
77
 
57
78
  public readonly authVerifier: TrustedKeySetAuthVerifier;
79
+ public readonly stateUpdate = new Event();
58
80
 
59
81
  constructor(params: DataSpaceParams) {
60
82
  this._inner = params.inner;
83
+ this._inner.stateUpdate.on(this._ctx, () => this.stateUpdate.emit());
84
+
85
+ this._gossip = params.gossip;
61
86
  this._presence = params.presence;
62
87
  this._keyring = params.keyring;
63
88
  this._feedStore = params.feedStore;
64
89
  this._metadataStore = params.metadataStore;
65
90
  this._signingContext = params.signingContext;
66
- this._dataPipelineController = new DataPipelineControllerImpl({
91
+ this._callbacks = params.callbacks ?? {};
92
+ this._dataPipeline = new DataPipeline({
67
93
  modelFactory: params.modelFactory,
68
94
  metadataStore: params.metadataStore,
69
95
  snapshotManager: params.snapshotManager,
@@ -90,17 +116,17 @@ export class DataSpace {
90
116
  return this._inner.isOpen;
91
117
  }
92
118
 
119
+ get state(): SpaceState {
120
+ return this._state;
121
+ }
122
+
93
123
  // TODO(burdon): Can we mark this for debugging only?
94
124
  get inner() {
95
125
  return this._inner;
96
126
  }
97
127
 
98
- get dataPipelineController(): DataPipelineControllerImpl {
99
- return this._dataPipelineController;
100
- }
101
-
102
- get stateUpdate() {
103
- return this._inner.stateUpdate;
128
+ get dataPipeline(): DataPipeline {
129
+ return this._dataPipeline;
104
130
  }
105
131
 
106
132
  get presence() {
@@ -115,11 +141,14 @@ export class DataSpace {
115
141
  await this.notarizationPlugin.open();
116
142
  await this._notarizationPluginConsumer.open();
117
143
  await this._inner.open();
144
+ this._state = SpaceState.INACTIVE;
118
145
  }
119
146
 
120
147
  async close() {
148
+ this._state = SpaceState.CLOSED;
121
149
  await this._ctx.dispose();
122
150
 
151
+ await this._dataPipeline.close();
123
152
  await this.authVerifier.close();
124
153
 
125
154
  await this._inner.close();
@@ -129,12 +158,48 @@ export class DataSpace {
129
158
  await this._presence.destroy();
130
159
  }
131
160
 
161
+ async postMessage(channel: string, message: any) {
162
+ return this._gossip.postMessage(channel, message);
163
+ }
164
+
165
+ listen(channel: string, callback: (message: GossipMessage) => void) {
166
+ return this._gossip.listen(channel, callback);
167
+ }
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
+
132
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.
133
194
  await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
134
- timeout: CONTROL_PIPELINE_READY_TIMEFRAME
195
+ ctx: this._ctx,
196
+ breakOnStall: false
135
197
  });
136
198
 
137
199
  await this._createWritableFeeds();
200
+ log('Writable feeds created');
201
+ this.stateUpdate.emit();
202
+
138
203
  this.notarizationPlugin.setWriter(
139
204
  createMappedFeedWriter<Credential, FeedMessage.Payload>(
140
205
  (credential) => ({
@@ -144,7 +209,28 @@ export class DataSpace {
144
209
  )
145
210
  );
146
211
 
147
- 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?.();
148
234
  }
149
235
 
150
236
  @timed(10_000)
@@ -186,7 +272,8 @@ export class DataSpace {
186
272
  }
187
273
 
188
274
  if (credentials.length > 0) {
189
- await this.notarizationPlugin.notarize(credentials);
275
+ // Never times out
276
+ await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
190
277
  }
191
278
 
192
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;
@@ -4,8 +4,8 @@
4
4
 
5
5
  import assert from 'node:assert';
6
6
 
7
- import { DeferredTask, Event, scheduleTask, TimeoutError, Trigger } from '@dxos/async';
8
- import { Context } from '@dxos/context';
7
+ import { DeferredTask, Event, scheduleTask, sleep, TimeoutError, Trigger } from '@dxos/async';
8
+ import { Context, rejectOnDispose } from '@dxos/context';
9
9
  import { CredentialProcessor } from '@dxos/credentials';
10
10
  import { FeedWriter } from '@dxos/feed-store';
11
11
  import { PublicKey } from '@dxos/keys';
@@ -16,8 +16,43 @@ import { NotarizationService, NotarizeRequest } from '@dxos/protocols/proto/dxos
16
16
  import { ExtensionContext, RpcExtension } from '@dxos/teleport';
17
17
  import { ComplexMap, ComplexSet, entry } from '@dxos/util';
18
18
 
19
- const RETRY_TIMEOUT = 1000;
20
- const NOTARIZE_TIMEOUT = 10_000;
19
+ const DEFAULT_RETRY_TIMEOUT = 1_000;
20
+
21
+ const DEFAULT_SUCCESS_DELAY = 1_000;
22
+
23
+ const DEFAULT_NOTARIZE_TIMEOUT = 10_000;
24
+
25
+ export type NotarizeParams = {
26
+ /**
27
+ * For cancellation.
28
+ */
29
+ ctx?: Context;
30
+
31
+ /**
32
+ * Credentials to notarize.
33
+ */
34
+ credentials: Credential[];
35
+
36
+ /**
37
+ * Timeout for the whole notarization process.
38
+ * Set to 0 to disable.
39
+ * @default {@link DEFAULT_NOTARIZE_TIMEOUT}
40
+ */
41
+ timeout?: number;
42
+
43
+ /**
44
+ * Retry timeout.
45
+ * @default {@link DEFAULT_RETRY_TIMEOUT}
46
+ */
47
+ retryTimeout?: number;
48
+
49
+ /**
50
+ * Minimum wait time after a peer confirms successful notarization before attempting with a new peer.
51
+ * This is to avoid spamming peers with notarization requests.
52
+ * @default {@link DEFAULT_SUCCESS_DELAY}
53
+ */
54
+ successDelay?: number;
55
+ };
21
56
 
22
57
  /**
23
58
  * See NotarizationService proto.
@@ -40,7 +75,13 @@ export class NotarizationPlugin implements CredentialProcessor {
40
75
  /**
41
76
  * Request credentials to be notarized.
42
77
  */
43
- async notarize(credentials: Credential[]) {
78
+ async notarize({
79
+ ctx: opCtx,
80
+ credentials,
81
+ timeout = DEFAULT_NOTARIZE_TIMEOUT,
82
+ retryTimeout = DEFAULT_RETRY_TIMEOUT,
83
+ successDelay = DEFAULT_SUCCESS_DELAY
84
+ }: NotarizeParams) {
44
85
  log('notarize', { credentials });
45
86
  assert(
46
87
  credentials.every((credential) => credential.id),
@@ -55,15 +96,23 @@ export class NotarizationPlugin implements CredentialProcessor {
55
96
  errors.throw(err);
56
97
  }
57
98
  });
58
- scheduleTask(
59
- ctx,
60
- () => {
61
- log.warn('Notarization timeout');
62
- void ctx.dispose();
63
- errors.throw(new TimeoutError(NOTARIZE_TIMEOUT, 'Notarization timed out'));
64
- },
65
- NOTARIZE_TIMEOUT
66
- );
99
+ opCtx?.onDispose(() => ctx.dispose());
100
+
101
+ // Timeout/
102
+ if (timeout !== 0) {
103
+ scheduleTask(
104
+ ctx,
105
+ () => {
106
+ log.warn('Notarization timeout', {
107
+ timeout,
108
+ peers: Array.from(this._extensions).map((extension) => extension.remotePeerId)
109
+ });
110
+ void ctx.dispose();
111
+ errors.throw(new TimeoutError(timeout, 'Notarization timed out'));
112
+ },
113
+ timeout
114
+ );
115
+ }
67
116
 
68
117
  const allNotarized = Promise.all(credentials.map((credential) => this._waitUntilProcessed(credential.id!)));
69
118
 
@@ -73,16 +122,15 @@ export class NotarizationPlugin implements CredentialProcessor {
73
122
  const notarizeTask = new DeferredTask(ctx, async () => {
74
123
  try {
75
124
  if (this._extensions.size === 0) {
76
- log.warn('No peers to notarize with');
77
125
  return; // No peers to try.
78
126
  }
79
127
 
80
128
  // Pick a peer that we haven't tried yet.
81
129
  const peer = [...this._extensions].find((peer) => !peersTried.has(peer));
82
130
  if (!peer) {
83
- log.warn('Exhausted all peers to notarize with', { retryIn: RETRY_TIMEOUT });
131
+ log.warn('Exhausted all peers to notarize with', { retryIn: retryTimeout });
84
132
  peersTried.clear();
85
- scheduleTask(ctx, () => notarizeTask.schedule(), RETRY_TIMEOUT); // retry with all peers again
133
+ scheduleTask(ctx, () => notarizeTask.schedule(), retryTimeout); // retry with all peers again
86
134
  return;
87
135
  }
88
136
 
@@ -92,6 +140,7 @@ export class NotarizationPlugin implements CredentialProcessor {
92
140
  credentials: credentials.filter((credential) => !this._processedCredentials.has(credential.id!))
93
141
  });
94
142
  log('success');
143
+ await sleep(successDelay); // wait before trying with a new peer
95
144
  } catch (err) {
96
145
  log.warn('error notarizing (recoverable)', err);
97
146
  notarizeTask.schedule(); // retry immediately with next peer
@@ -102,8 +151,7 @@ export class NotarizationPlugin implements CredentialProcessor {
102
151
  this._extensionOpened.on(ctx, () => notarizeTask.schedule());
103
152
 
104
153
  try {
105
- // TODO(dmaretskyi): Abort (context) & timeout.
106
- await Promise.race([allNotarized, errors.wait()]);
154
+ await Promise.race([rejectOnDispose(ctx), allNotarized, errors.wait()]);
107
155
  log('done');
108
156
  } finally {
109
157
  await ctx.dispose();
@@ -81,7 +81,7 @@ describe('SpacesService', () => {
81
81
 
82
82
  const spaces = await result.wait();
83
83
  expect(spaces).to.be.length(3);
84
- expect(spaces).to.deep.equal(existingSpaces);
84
+ expect(spaces?.map((s) => s.spaceKey)).to.deep.equal(existingSpaces?.map((s) => s.spaceKey));
85
85
  });
86
86
 
87
87
  test('updates when new space is added', async () => {
@@ -8,22 +8,26 @@ import { raise, todo } from '@dxos/debug';
8
8
  import { DataServiceSubscriptions, SpaceManager, SpaceNotFoundError } from '@dxos/echo-pipeline';
9
9
  import { log } from '@dxos/log';
10
10
  import {
11
+ PostMessageRequest,
11
12
  QueryCredentialsRequest,
12
13
  QuerySpacesResponse,
13
14
  Space,
14
15
  SpaceMember,
15
16
  SpacesService,
16
- SpaceStatus,
17
+ SubscribeMessagesRequest,
17
18
  UpdateSpaceRequest,
18
19
  WriteCredentialsRequest
19
20
  } from '@dxos/protocols/proto/dxos/client/services';
20
21
  import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
22
+ import { GossipMessage } from '@dxos/protocols/proto/dxos/mesh/teleport/gossip';
21
23
  import { humanize, Provider } from '@dxos/util';
22
24
 
23
25
  import { IdentityManager } from '../identity';
24
26
  import { DataSpace } from './data-space';
25
27
  import { DataSpaceManager } from './data-space-manager';
26
28
 
29
+ const TIMEFRAME_UPDATE_DEBOUNCE_TIME = 500;
30
+
27
31
  /**
28
32
  *
29
33
  */
@@ -52,29 +56,36 @@ export class SpacesServiceImpl implements SpacesService {
52
56
  return new Stream<QuerySpacesResponse>(({ next, ctx }) => {
53
57
  const onUpdate = async () => {
54
58
  const dataSpaceManager = await this._getDataSpaceManager();
55
- const spaces = Array.from(dataSpaceManager.spaces.values())
56
- // Skip spaces without data service available.
57
- .filter((space) => this._dataServiceSubscriptions.getDataService(space.key))
58
- .map((space) => this._transformSpace(space));
59
+ const spaces = Array.from(dataSpaceManager.spaces.values()).map((space) => this._transformSpace(space));
59
60
  log('update', { spaces });
60
61
  next({ spaces });
61
62
  };
62
63
 
63
- setTimeout(async () => {
64
+ scheduleTask(ctx, async () => {
64
65
  const dataSpaceManager = await this._getDataSpaceManager();
66
+
65
67
  const subscriptions = new EventSubscriptions();
68
+ ctx.onDispose(() => subscriptions.clear());
69
+
66
70
  // TODO(dmaretskyi): Create a pattern for subscribing to a set of objects.
67
71
  const subscribeSpaces = () => {
68
72
  subscriptions.clear();
69
73
 
70
74
  for (const space of dataSpaceManager.spaces.values()) {
71
- if (!this._dataServiceSubscriptions.getDataService(space.key)) {
72
- // Skip spaces without data service available.
73
- continue;
74
- }
75
-
76
75
  subscriptions.add(space.stateUpdate.on(ctx, onUpdate));
77
76
  subscriptions.add(space.presence.updated.on(ctx, onUpdate));
77
+
78
+ // Pipeline progress.
79
+ space.inner.controlPipeline.state.timeframeUpdate
80
+ .debounce(TIMEFRAME_UPDATE_DEBOUNCE_TIME)
81
+ .on(ctx, onUpdate);
82
+ if (space.dataPipeline.pipelineState) {
83
+ subscriptions.add(
84
+ space.dataPipeline.pipelineState.timeframeUpdate
85
+ .debounce(TIMEFRAME_UPDATE_DEBOUNCE_TIME)
86
+ .on(ctx, onUpdate)
87
+ );
88
+ }
78
89
  }
79
90
  };
80
91
 
@@ -82,12 +93,9 @@ export class SpacesServiceImpl implements SpacesService {
82
93
  subscribeSpaces();
83
94
  void onUpdate();
84
95
  });
96
+ subscribeSpaces();
85
97
 
86
- ctx.onDispose(() => subscriptions.clear());
87
- scheduleTask(ctx, () => {
88
- subscribeSpaces();
89
- void onUpdate();
90
- });
98
+ void onUpdate();
91
99
  });
92
100
 
93
101
  if (!this._identityManager.identity) {
@@ -96,6 +104,25 @@ export class SpacesServiceImpl implements SpacesService {
96
104
  });
97
105
  }
98
106
 
107
+ async postMessage({ spaceKey, channel, message }: PostMessageRequest) {
108
+ const dataSpaceManager = await this._getDataSpaceManager();
109
+ const space = dataSpaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
110
+ await space.postMessage(getChannelId(channel), message);
111
+ }
112
+
113
+ subscribeMessages({ spaceKey, channel }: SubscribeMessagesRequest) {
114
+ return new Stream<GossipMessage>(({ ctx, next }) => {
115
+ scheduleTask(ctx, async () => {
116
+ const dataSpaceManager = await this._getDataSpaceManager();
117
+ const space = dataSpaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
118
+ const handle = space.listen(getChannelId(channel), (message) => {
119
+ next(message);
120
+ });
121
+ ctx.onDispose(() => handle.unsubscribe());
122
+ });
123
+ });
124
+ }
125
+
99
126
  queryCredentials({ spaceKey }: QueryCredentialsRequest): Stream<Credential> {
100
127
  return new Stream(({ ctx, next }) => {
101
128
  const space = this._spaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
@@ -120,7 +147,13 @@ export class SpacesServiceImpl implements SpacesService {
120
147
  private _transformSpace(space: DataSpace): Space {
121
148
  return {
122
149
  spaceKey: space.key,
123
- status: space.isOpen ? SpaceStatus.ACTIVE : SpaceStatus.INACTIVE,
150
+ state: space.state,
151
+ pipeline: {
152
+ targetControlTimeframe: space.inner.controlPipeline.state.targetTimeframe,
153
+ currentControlTimeframe: space.inner.controlPipeline.state.timeframe,
154
+ currentDataTimeframe: space.dataPipeline.pipelineState?.timeframe,
155
+ targetDataTimeframe: space.dataPipeline.pipelineState?.targetTimeframe
156
+ },
124
157
  members: Array.from(space.inner.spaceState.members.values()).map((member) => ({
125
158
  identity: {
126
159
  identityKey: member.key,
@@ -137,3 +170,6 @@ export class SpacesServiceImpl implements SpacesService {
137
170
  };
138
171
  }
139
172
  }
173
+
174
+ // Add `user-channel` prefix to the channel name, so that it doesn't collide with the internal channels.
175
+ const getChannelId = (channel: string): string => `user-channel/${channel}`;
@@ -7,7 +7,7 @@ import { Config } from '@dxos/config';
7
7
  import { createCredentialSignerWithChain, CredentialGenerator } from '@dxos/credentials';
8
8
  import {
9
9
  SnapshotStore,
10
- DataPipelineControllerImpl,
10
+ DataPipeline,
11
11
  MetadataStore,
12
12
  SigningContext,
13
13
  SpaceManager,
@@ -73,7 +73,7 @@ export const createIdentity = async (peer: ServiceContext) => {
73
73
 
74
74
  // TODO(burdon): Remove @dxos/client-testing.
75
75
  // TODO(burdon): Create builder and make configurable.
76
- export const syncItemsLocal = async (db1: DataPipelineControllerImpl, db2: DataPipelineControllerImpl) => {
76
+ export const syncItemsLocal = async (db1: DataPipeline, db2: DataPipeline) => {
77
77
  await testLocalDatabase(db1, db2);
78
78
  await testLocalDatabase(db2, db1);
79
79
  };
@@ -4,12 +4,14 @@
4
4
 
5
5
  import assert from 'node:assert';
6
6
 
7
- import { asyncTimeout } from '@dxos/async';
8
- import { Client, ClientServices, ClientServicesProxy, createDefaultModelFactory } from '@dxos/client';
7
+ import { asyncTimeout, Trigger } from '@dxos/async';
8
+ import { Client, ClientServices, ClientServicesProxy, createDefaultModelFactory, Invitation } from '@dxos/client';
9
9
  import { Config } from '@dxos/config';
10
+ import { raise } from '@dxos/debug';
10
11
  import { DocumentModel } from '@dxos/document-model';
11
12
  import { DatabaseBackendProxy, genesisMutation } from '@dxos/echo-db';
12
13
  import { PublicKey } from '@dxos/keys';
14
+ import { log } from '@dxos/log';
13
15
  import { MemorySignalManager, MemorySignalManagerContext, WebsocketSignalManager } from '@dxos/messaging';
14
16
  import { createWebRTCTransportFactory, MemoryTransportFactory, NetworkManager } from '@dxos/network-manager';
15
17
  import { Storage } from '@dxos/random-access-storage';
@@ -119,12 +121,12 @@ export const testSpace = async (create: DatabaseBackendProxy, check: DatabaseBac
119
121
 
120
122
  const result = create.mutate(genesisMutation(objectId, DocumentModel.meta.type));
121
123
 
122
- await result.getReceipt();
124
+ await result.batch.getReceipt();
123
125
  // TODO(dmaretskiy): await result.waitToBeProcessed()
124
- assert(create._itemManager.entities.has(result.objectsCreated[0].id));
126
+ assert(create._itemManager.entities.has(result.objectsUpdated[0].id));
125
127
 
126
128
  await asyncTimeout(
127
- check._itemManager.update.waitForCondition(() => check._itemManager.entities.has(objectId)),
129
+ check.itemUpdate.waitForCondition(() => check._itemManager.entities.has(objectId)),
128
130
  1000
129
131
  );
130
132
 
@@ -138,3 +140,41 @@ export const syncItems = async (db1: DatabaseBackendProxy, db2: DatabaseBackendP
138
140
  // Check item replicated from 2 => 1.
139
141
  await testSpace(db2, db1);
140
142
  };
143
+
144
+ export const joinCommonSpace = async ([initialPeer, ...peers]: Client[], spaceKey?: PublicKey): Promise<PublicKey> => {
145
+ const rootSpace = spaceKey ? initialPeer.getSpace(spaceKey) : await initialPeer.createSpace();
146
+ assert(rootSpace, 'Space not found.');
147
+
148
+ await Promise.all(
149
+ peers.map(async (peer) => {
150
+ const hostDone = new Trigger<Invitation>();
151
+ const guestDone = new Trigger<Invitation>();
152
+
153
+ const hostObservabli = rootSpace.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
154
+ log('invitation created');
155
+ hostObservabli.subscribe({
156
+ onConnecting: (invitation) => {
157
+ const guestObservable = peer.acceptInvitation(invitation);
158
+ log('invitation accepted');
159
+
160
+ guestObservable.subscribe({
161
+ onSuccess: (invitation: Invitation) => {
162
+ guestDone.wake(invitation);
163
+ log('invitation guestDone');
164
+ },
165
+ onError: (err: Error) => raise(err)
166
+ });
167
+ },
168
+ onSuccess: (invitation) => {
169
+ hostDone.wake(invitation);
170
+ log('invitation hostDone');
171
+ },
172
+ onError: (err) => raise(err)
173
+ });
174
+
175
+ await Promise.all([hostDone.wait(), guestDone.wait()]);
176
+ })
177
+ );
178
+
179
+ return rootSpace.key;
180
+ };