@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
@@ -5,7 +5,7 @@
5
5
  import assert from 'node:assert';
6
6
 
7
7
  import { DeferredTask, Event, scheduleTask, sleep, TimeoutError, Trigger } from '@dxos/async';
8
- import { Context } from '@dxos/context';
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,14 +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
- // Timeout for retrying notarization.
20
- const RETRY_TIMEOUT = 1_000;
19
+ const DEFAULT_RETRY_TIMEOUT = 1_000;
21
20
 
22
- // Minimum wait time after a peer confirms successful notarization before attempting with a new peer.
23
- const SUCCESS_DELAY = 1_000;
21
+ const DEFAULT_SUCCESS_DELAY = 1_000;
24
22
 
25
- // Timeout for the whole notarization process.
26
- const NOTARIZE_TIMEOUT = 10_000;
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
+ };
27
56
 
28
57
  /**
29
58
  * See NotarizationService proto.
@@ -46,7 +75,13 @@ export class NotarizationPlugin implements CredentialProcessor {
46
75
  /**
47
76
  * Request credentials to be notarized.
48
77
  */
49
- 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) {
50
85
  log('notarize', { credentials });
51
86
  assert(
52
87
  credentials.every((credential) => credential.id),
@@ -61,15 +96,23 @@ export class NotarizationPlugin implements CredentialProcessor {
61
96
  errors.throw(err);
62
97
  }
63
98
  });
64
- scheduleTask(
65
- ctx,
66
- () => {
67
- log.warn('Notarization timeout');
68
- void ctx.dispose();
69
- errors.throw(new TimeoutError(NOTARIZE_TIMEOUT, 'Notarization timed out'));
70
- },
71
- NOTARIZE_TIMEOUT
72
- );
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
+ }
73
116
 
74
117
  const allNotarized = Promise.all(credentials.map((credential) => this._waitUntilProcessed(credential.id!)));
75
118
 
@@ -79,16 +122,15 @@ export class NotarizationPlugin implements CredentialProcessor {
79
122
  const notarizeTask = new DeferredTask(ctx, async () => {
80
123
  try {
81
124
  if (this._extensions.size === 0) {
82
- log.warn('No peers to notarize with');
83
125
  return; // No peers to try.
84
126
  }
85
127
 
86
128
  // Pick a peer that we haven't tried yet.
87
129
  const peer = [...this._extensions].find((peer) => !peersTried.has(peer));
88
130
  if (!peer) {
89
- log.warn('Exhausted all peers to notarize with', { retryIn: RETRY_TIMEOUT });
131
+ log.warn('Exhausted all peers to notarize with', { retryIn: retryTimeout });
90
132
  peersTried.clear();
91
- scheduleTask(ctx, () => notarizeTask.schedule(), RETRY_TIMEOUT); // retry with all peers again
133
+ scheduleTask(ctx, () => notarizeTask.schedule(), retryTimeout); // retry with all peers again
92
134
  return;
93
135
  }
94
136
 
@@ -98,7 +140,7 @@ export class NotarizationPlugin implements CredentialProcessor {
98
140
  credentials: credentials.filter((credential) => !this._processedCredentials.has(credential.id!))
99
141
  });
100
142
  log('success');
101
- await sleep(SUCCESS_DELAY); // wait before trying with a new peer
143
+ await sleep(successDelay); // wait before trying with a new peer
102
144
  } catch (err) {
103
145
  log.warn('error notarizing (recoverable)', err);
104
146
  notarizeTask.schedule(); // retry immediately with next peer
@@ -109,8 +151,7 @@ export class NotarizationPlugin implements CredentialProcessor {
109
151
  this._extensionOpened.on(ctx, () => notarizeTask.schedule());
110
152
 
111
153
  try {
112
- // TODO(dmaretskyi): Abort (context) & timeout.
113
- await Promise.race([allNotarized, errors.wait()]);
154
+ await Promise.race([rejectOnDispose(ctx), allNotarized, errors.wait()]);
114
155
  log('done');
115
156
  } finally {
116
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 () => {
@@ -14,7 +14,6 @@ import {
14
14
  Space,
15
15
  SpaceMember,
16
16
  SpacesService,
17
- SpaceStatus,
18
17
  SubscribeMessagesRequest,
19
18
  UpdateSpaceRequest,
20
19
  WriteCredentialsRequest
@@ -27,6 +26,8 @@ import { IdentityManager } from '../identity';
27
26
  import { DataSpace } from './data-space';
28
27
  import { DataSpaceManager } from './data-space-manager';
29
28
 
29
+ const TIMEFRAME_UPDATE_DEBOUNCE_TIME = 500;
30
+
30
31
  /**
31
32
  *
32
33
  */
@@ -55,29 +56,36 @@ export class SpacesServiceImpl implements SpacesService {
55
56
  return new Stream<QuerySpacesResponse>(({ next, ctx }) => {
56
57
  const onUpdate = async () => {
57
58
  const dataSpaceManager = await this._getDataSpaceManager();
58
- const spaces = Array.from(dataSpaceManager.spaces.values())
59
- // Skip spaces without data service available.
60
- .filter((space) => this._dataServiceSubscriptions.getDataService(space.key))
61
- .map((space) => this._transformSpace(space));
59
+ const spaces = Array.from(dataSpaceManager.spaces.values()).map((space) => this._transformSpace(space));
62
60
  log('update', { spaces });
63
61
  next({ spaces });
64
62
  };
65
63
 
66
- setTimeout(async () => {
64
+ scheduleTask(ctx, async () => {
67
65
  const dataSpaceManager = await this._getDataSpaceManager();
66
+
68
67
  const subscriptions = new EventSubscriptions();
68
+ ctx.onDispose(() => subscriptions.clear());
69
+
69
70
  // TODO(dmaretskyi): Create a pattern for subscribing to a set of objects.
70
71
  const subscribeSpaces = () => {
71
72
  subscriptions.clear();
72
73
 
73
74
  for (const space of dataSpaceManager.spaces.values()) {
74
- if (!this._dataServiceSubscriptions.getDataService(space.key)) {
75
- // Skip spaces without data service available.
76
- continue;
77
- }
78
-
79
75
  subscriptions.add(space.stateUpdate.on(ctx, onUpdate));
80
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
+ }
81
89
  }
82
90
  };
83
91
 
@@ -85,12 +93,9 @@ export class SpacesServiceImpl implements SpacesService {
85
93
  subscribeSpaces();
86
94
  void onUpdate();
87
95
  });
96
+ subscribeSpaces();
88
97
 
89
- ctx.onDispose(() => subscriptions.clear());
90
- scheduleTask(ctx, () => {
91
- subscribeSpaces();
92
- void onUpdate();
93
- });
98
+ void onUpdate();
94
99
  });
95
100
 
96
101
  if (!this._identityManager.identity) {
@@ -142,7 +147,13 @@ export class SpacesServiceImpl implements SpacesService {
142
147
  private _transformSpace(space: DataSpace): Space {
143
148
  return {
144
149
  spaceKey: space.key,
145
- 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
+ },
146
157
  members: Array.from(space.inner.spaceState.members.values()).map((member) => ({
147
158
  identity: {
148
159
  identityKey: member.key,
@@ -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
  };
@@ -142,7 +142,7 @@ export const syncItems = async (db1: DatabaseBackendProxy, db2: DatabaseBackendP
142
142
  };
143
143
 
144
144
  export const joinCommonSpace = async ([initialPeer, ...peers]: Client[], spaceKey?: PublicKey): Promise<PublicKey> => {
145
- const rootSpace = spaceKey ? initialPeer.echo.getSpace(spaceKey) : await initialPeer.echo.createSpace();
145
+ const rootSpace = spaceKey ? initialPeer.getSpace(spaceKey) : await initialPeer.createSpace();
146
146
  assert(rootSpace, 'Space not found.');
147
147
 
148
148
  await Promise.all(
@@ -154,7 +154,7 @@ export const joinCommonSpace = async ([initialPeer, ...peers]: Client[], spaceKe
154
154
  log('invitation created');
155
155
  hostObservabli.subscribe({
156
156
  onConnecting: (invitation) => {
157
- const guestObservable = peer.echo.acceptInvitation(invitation);
157
+ const guestObservable = peer.acceptInvitation(invitation);
158
158
  log('invitation accepted');
159
159
 
160
160
  guestObservable.subscribe({
@@ -7,7 +7,7 @@ import assert from 'node:assert';
7
7
  import waitForExpect from 'wait-for-expect';
8
8
 
9
9
  import { Trigger } from '@dxos/async';
10
- import { Space } from '@dxos/client';
10
+ import { Client, Space } from '@dxos/client';
11
11
  import { raise } from '@dxos/debug';
12
12
  import { log } from '@dxos/log';
13
13
  import { Invitation, SpaceMember } from '@dxos/protocols/proto/dxos/client/services';
@@ -19,6 +19,19 @@ import { syncItems, TestBuilder } from '../testing';
19
19
  // TODO(burdon): Timeouts and progress callback/events.
20
20
 
21
21
  describe('Client services', () => {
22
+ test('creates client with local host', async () => {
23
+ const testBuilder = new TestBuilder();
24
+
25
+ const servicesProvider = testBuilder.createLocal();
26
+ await servicesProvider.open();
27
+ afterTest(() => servicesProvider.close());
28
+
29
+ const client = new Client({ services: servicesProvider });
30
+ await client.initialize();
31
+ afterTest(() => client.destroy());
32
+ expect(client.initialized).to.be.true;
33
+ });
34
+
22
35
  test('creates client with remote server', async () => {
23
36
  const testBuilder = new TestBuilder();
24
37
 
@@ -141,16 +154,16 @@ describe('Client services', () => {
141
154
  // Check same identity.
142
155
  const [invitation1, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
143
156
  expect(invitation1.identityKey).not.to.exist;
144
- expect(invitation2.identityKey).to.deep.eq(client1.halo.identity!.identityKey);
145
- expect(invitation2.identityKey).to.deep.eq(client2.halo.identity!.identityKey);
157
+ expect(invitation2.identityKey).to.deep.eq(client1.halo.identity.get()!.identityKey);
158
+ expect(invitation2.identityKey).to.deep.eq(client2.halo.identity.get()!.identityKey);
146
159
  expect(invitation1.state).to.eq(Invitation.State.SUCCESS);
147
160
  expect(invitation2.state).to.eq(Invitation.State.SUCCESS);
148
161
 
149
162
  // Check devices.
150
163
  // TODO(burdon): Incorrect number of devices.
151
164
  await waitForExpect(async () => {
152
- expect(client1.halo.getDevices()).to.have.lengthOf(2);
153
- expect(client2.halo.getDevices()).to.have.lengthOf(2);
165
+ expect(client1.halo.devices.get()).to.have.lengthOf(2);
166
+ expect(client2.halo.devices.get()).to.have.lengthOf(2);
154
167
  });
155
168
  });
156
169
 
@@ -184,13 +197,13 @@ describe('Client services', () => {
184
197
  const success1 = new Trigger<Invitation>();
185
198
  const success2 = new Trigger<Invitation>();
186
199
 
187
- const space1 = await client1.echo.createSpace();
200
+ const space1 = await client1.createSpace();
188
201
  log('createSpace', { key: space1.key });
189
202
  const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
190
203
 
191
204
  observable1.subscribe({
192
205
  onConnecting: (invitation) => {
193
- const observable2 = client2.echo.acceptInvitation(invitation);
206
+ const observable2 = client2.acceptInvitation(invitation);
194
207
  observable2.subscribe({
195
208
  onSuccess: (invitation: Invitation) => {
196
209
  success2.wake(invitation);
@@ -214,7 +227,7 @@ describe('Client services', () => {
214
227
  // TODO(burdon): Space should now be available?
215
228
  const trigger = new Trigger<Space>();
216
229
  await waitForExpect(() => {
217
- const space2 = client2.echo.getSpace(invitation2.spaceKey!);
230
+ const space2 = client2.getSpace(invitation2.spaceKey!);
218
231
  assert(space2);
219
232
  expect(space2).to.exist;
220
233
  trigger.wake(space2);
@@ -224,10 +237,10 @@ describe('Client services', () => {
224
237
 
225
238
  for (const space of [space1, space2]) {
226
239
  await waitForExpect(() => {
227
- expect(space.getMembers()).to.deep.equal([
240
+ expect(space.members.get()).to.deep.equal([
228
241
  {
229
242
  identity: {
230
- identityKey: client1.halo.identity!.identityKey,
243
+ identityKey: client1.halo.identity.get()!.identityKey,
231
244
  profile: {
232
245
  displayName: 'Peer 1'
233
246
  }
@@ -236,7 +249,7 @@ describe('Client services', () => {
236
249
  },
237
250
  {
238
251
  identity: {
239
- identityKey: client2.halo.identity!.identityKey,
252
+ identityKey: client2.halo.identity.get()!.identityKey,
240
253
  profile: {
241
254
  displayName: 'Peer 2'
242
255
  }
@@ -61,9 +61,7 @@ describe('Client', () => {
61
61
  const client = new Client({ services: testBuilder.createLocal() });
62
62
  await client.initialize();
63
63
  afterTest(() => client.destroy());
64
- await expect(client.echo.createSpace()).to.eventually.be.rejectedWith(
65
- 'This device has no HALO identity available.'
66
- );
64
+ await expect(client.createSpace()).to.eventually.be.rejectedWith('This device has no HALO identity available.');
67
65
  }).timeout(1_000);
68
66
 
69
67
  // TODO(burdon): Memory store is reset on close (feed store is closed).
@@ -109,13 +107,12 @@ const getNodeConfig = async (reset = false) => {
109
107
  const runTest = async (testBuilder: TestBuilder) => {
110
108
  const client = new Client({ services: testBuilder.createLocal() });
111
109
  const displayName = 'test-user';
112
-
113
110
  {
114
111
  // Create identity.
115
112
  await client.initialize();
116
- expect(client.halo.identity).not.to.exist;
113
+ expect(client.halo.identity.get()).not.to.exist;
117
114
  const identity = await client.halo.createIdentity({ displayName });
118
- expect(client.halo.identity).to.deep.eq(identity);
115
+ expect(client.halo.identity.get()).to.deep.eq(identity);
119
116
  await client.destroy();
120
117
  }
121
118
 
@@ -22,8 +22,8 @@ describe('Halo', () => {
22
22
  await client.halo.createIdentity({ displayName: 'test-user' });
23
23
  expect(client.halo.identity).exist;
24
24
 
25
- expect(await client.halo.getDevices()).to.have.lengthOf(1);
26
- expect(client.halo.identity!.profile?.displayName).to.equal('test-user');
25
+ expect(await client.halo.devices.get()).to.have.lengthOf(1);
26
+ expect(client.halo.identity.get()!.profile?.displayName).to.equal('test-user');
27
27
  });
28
28
 
29
29
  test('device invitations', async () => {
@@ -36,7 +36,7 @@ describe('Halo', () => {
36
36
  await client1.halo.createIdentity({ displayName: 'test-user' });
37
37
  expect(client1.halo.identity).exist;
38
38
 
39
- expect(await client1.halo.getDevices()).to.have.lengthOf(1);
39
+ expect(await client1.halo.devices.get()).to.have.lengthOf(1);
40
40
 
41
41
  const client2 = new Client({ services: testBuilder.createLocal() });
42
42
  afterTest(() => client2.destroy());
@@ -68,7 +68,7 @@ describe('Halo', () => {
68
68
  await done1.wait();
69
69
  await done2.wait();
70
70
 
71
- expect(await client1.halo.getDevices()).to.have.lengthOf(2);
72
- expect(await client2.halo.getDevices()).to.have.lengthOf(2);
71
+ expect(await client1.halo.devices.get()).to.have.lengthOf(2);
72
+ expect(await client2.halo.devices.get()).to.have.lengthOf(2);
73
73
  });
74
74
  });
@@ -8,7 +8,7 @@ import { Trigger } from '@dxos/async';
8
8
  import { Client, Invitation } from '@dxos/client';
9
9
  import { raise } from '@dxos/debug';
10
10
  import { log } from '@dxos/log';
11
- import { describe, test, afterTest } from '@dxos/test';
11
+ import { afterTest, describe, test } from '@dxos/test';
12
12
 
13
13
  import { TestBuilder, testSpace } from '../testing';
14
14
 
@@ -31,13 +31,13 @@ describe('Spaces/invitations', () => {
31
31
  const success1 = new Trigger<Invitation>();
32
32
  const success2 = new Trigger<Invitation>();
33
33
 
34
- const space1 = await client1.echo.createSpace();
34
+ const space1 = await client1.createSpace();
35
35
  log('createSpace', { key: space1.key });
36
36
  const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
37
37
 
38
38
  observable1.subscribe({
39
39
  onConnecting: (invitation) => {
40
- const observable2 = client2.echo.acceptInvitation(invitation);
40
+ const observable2 = client2.acceptInvitation(invitation);
41
41
  observable2.subscribe({
42
42
  onSuccess: (invitation: Invitation) => {
43
43
  success2.wake(invitation);
@@ -57,7 +57,7 @@ describe('Spaces/invitations', () => {
57
57
  expect(invitation1.state).to.eq(Invitation.State.SUCCESS);
58
58
 
59
59
  {
60
- const space = client2.echo.getSpace(invitation2.spaceKey!)!;
60
+ const space = await client2.getSpace(invitation2.spaceKey!)!.waitUntilReady();
61
61
  await testSpace(space.internal.db);
62
62
  }
63
63
  });
@@ -5,7 +5,7 @@
5
5
  import { expect } from 'chai';
6
6
 
7
7
  import { asyncTimeout, Trigger } from '@dxos/async';
8
- import { Client, Invitation } from '@dxos/client';
8
+ import { Client, Invitation, Space } from '@dxos/client';
9
9
  import { Config } from '@dxos/config';
10
10
  import { raise } from '@dxos/debug';
11
11
  import { log } from '@dxos/log';
@@ -25,10 +25,10 @@ describe('Spaces', () => {
25
25
  await client.halo.createIdentity({ displayName: 'test-user' });
26
26
 
27
27
  // TODO(burdon): Extend basic queries.
28
- const space = await client.echo.createSpace();
28
+ const space = await client.createSpace();
29
29
  await testSpace(space.internal.db);
30
30
 
31
- expect(space.getMembers()).to.be.length(1);
31
+ expect(space.members.get()).to.be.length(1);
32
32
  });
33
33
 
34
34
  test('creates a space re-opens the client', async () => {
@@ -42,12 +42,12 @@ describe('Spaces', () => {
42
42
  let itemId: string;
43
43
  {
44
44
  // TODO(burdon): API (client.echo/client.halo).
45
- const space = await client.echo.createSpace();
45
+ const space = await client.createSpace();
46
46
  const {
47
47
  objectsUpdated: [item]
48
48
  } = await testSpace(space.internal.db);
49
49
  itemId = item.id;
50
- expect(space.getMembers()).to.be.length(1);
50
+ expect(space.members.get()).to.be.length(1);
51
51
  }
52
52
 
53
53
  await client.destroy();
@@ -57,9 +57,18 @@ describe('Spaces', () => {
57
57
  await client.initialize();
58
58
 
59
59
  {
60
- const result = client.echo.getSpaces();
61
- expect(result).to.have.length(1);
62
- const space = result[0];
60
+ // TODO(dmaretskyi): Replace with helper?.
61
+ const spaceTrigger = new Trigger<Space>();
62
+ if (client.spaces.get()[0]) {
63
+ spaceTrigger.wake(client.spaces.get()[0]);
64
+ }
65
+ client.spaces.subscribe(() => {
66
+ if (client.spaces.get()[0]) {
67
+ spaceTrigger.wake(client.spaces.get()[0]);
68
+ }
69
+ });
70
+ const space = await spaceTrigger.wait({ timeout: 500 });
71
+ await space.waitUntilReady();
63
72
 
64
73
  const item = space.internal.db._itemManager.getItem(itemId)!;
65
74
  expect(item).to.exist;
@@ -86,13 +95,13 @@ describe('Spaces', () => {
86
95
  const success1 = new Trigger<Invitation>();
87
96
  const success2 = new Trigger<Invitation>();
88
97
 
89
- const space1 = await client1.echo.createSpace();
98
+ const space1 = await client1.createSpace();
90
99
  log('createSpace', { key: space1.key });
91
100
  const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
92
101
 
93
102
  observable1.subscribe({
94
103
  onConnecting: (invitation) => {
95
- const observable2 = client2.echo.acceptInvitation(invitation);
104
+ const observable2 = client2.acceptInvitation(invitation);
96
105
  observable2.subscribe({
97
106
  onSuccess: (invitation: Invitation) => {
98
107
  success2.wake(invitation);
@@ -109,7 +118,7 @@ describe('Spaces', () => {
109
118
 
110
119
  const [_, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
111
120
 
112
- const space2 = client2.echo.getSpace(invitation2.spaceKey!)!;
121
+ const space2 = await client2.getSpace(invitation2.spaceKey!)!.waitUntilReady();
113
122
 
114
123
  const hello = new Trigger();
115
124
  {
@@ -6,7 +6,7 @@ import * as fc from 'fast-check';
6
6
  import { ModelRunSetup } from 'fast-check';
7
7
  import waitForExpect from 'wait-for-expect';
8
8
 
9
- import { Client, Document, Text } from '@dxos/client';
9
+ import { Client, Expando, Text } from '@dxos/client';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { log } from '@dxos/log';
12
12
  import { describe, test } from '@dxos/test';
@@ -34,7 +34,7 @@ const assertState = async (model: Model, real: Real) => {
34
34
  // Wait for replication.
35
35
  await waitForExpect(() => {
36
36
  for (const [peerId, peer] of real.peers.entries()) {
37
- const space = peer.echo.getSpace(real.spaceKey);
37
+ const space = peer.getSpace(real.spaceKey);
38
38
  if (space) {
39
39
  if (!model.peers.has(peerId)) {
40
40
  throw new Error(`Expected peer to not be in space: ${peerId.truncate()}`);
@@ -73,10 +73,10 @@ class CreatePeerCommand implements fc.AsyncCommand<Model, Real> {
73
73
  await client.initialize();
74
74
  await client.halo.createIdentity();
75
75
  if (real.peers.size === 0) {
76
- const space = await client.echo.createSpace();
76
+ const space = await client.createSpace();
77
77
  const content = new Text();
78
78
  content.doc?.getText('utf8').insert(0, initialContent);
79
- await space.db.add(new Document({ content }));
79
+ await space.db.add(new Expando({ content }));
80
80
  real.spaceKey = space.key;
81
81
  } else {
82
82
  const host = Array.from(real.peers.values())[0];
@@ -114,7 +114,7 @@ class InsertTextCommand implements fc.AsyncCommand<Model, Real> {
114
114
  model.text = model.text.slice(0, this.index) + this.text + model.text.slice(this.index);
115
115
 
116
116
  const peer = real.peers.get(this.peerId);
117
- const space = peer!.echo.getSpace(real.spaceKey)!;
117
+ const space = peer!.getSpace(real.spaceKey)!;
118
118
  const [document] = space.db.query((obj) => !!obj.content).objects;
119
119
  const text = (document.content.doc as Doc).getText('utf8');
120
120
  text.insert(this.index, this.text);
@@ -140,7 +140,7 @@ class RemoveTextCommand implements fc.AsyncCommand<Model, Real> {
140
140
  model.text = model.text.slice(0, this.index) + model.text.slice(this.index + this.length);
141
141
 
142
142
  const peer = real.peers.get(this.peerId);
143
- const space = peer!.echo.getSpace(real.spaceKey)!;
143
+ const space = peer!.getSpace(real.spaceKey)!;
144
144
  const [document] = space.db.query((obj) => !!obj.content).objects;
145
145
  const text = (document.content.doc as Doc).getText('utf8');
146
146
  text.delete(this.index, this.length);
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { asyncTimeout, Trigger } from '@dxos/async';
6
- import { log } from '@dxos/log';
6
+ import { log, logInfo } from '@dxos/log';
7
7
  import { MaybePromise } from '@dxos/util';
8
8
 
9
9
  enum Message {
@@ -30,15 +30,24 @@ export class VaultResourceLock {
30
30
  this._broadcastChannel.onmessage = this._onMessage.bind(this);
31
31
  }
32
32
 
33
+ @logInfo
34
+ get lockKey() {
35
+ return this._lockKey;
36
+ }
37
+
33
38
  async acquire() {
34
39
  this._broadcastChannel.postMessage({
35
40
  message: Message.ACQUIRING
36
41
  });
37
42
 
38
43
  try {
44
+ log('aquiring lock...');
39
45
  await asyncTimeout(this._requestLock(), 3_000);
46
+ log('acquired lock');
40
47
  } catch {
48
+ log('stealing lock...');
41
49
  await this._requestLock(true);
50
+ log('stolen lock');
42
51
  }
43
52
  }
44
53