@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
@@ -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
  });
@@ -2,8 +2,13 @@
2
2
  // Copyright 2021 DXOS.org
3
3
  //
4
4
 
5
- import { Client } from '@dxos/client';
6
- import { describe, test, afterTest } from '@dxos/test';
5
+ import { expect } from 'chai';
6
+
7
+ import { Trigger } from '@dxos/async';
8
+ import { Client, Invitation } from '@dxos/client';
9
+ import { raise } from '@dxos/debug';
10
+ import { log } from '@dxos/log';
11
+ import { afterTest, describe, test } from '@dxos/test';
7
12
 
8
13
  import { TestBuilder, testSpace } from '../testing';
9
14
 
@@ -11,13 +16,48 @@ describe('Spaces/invitations', () => {
11
16
  test('creates a space and invites a peer', async () => {
12
17
  const testBuilder = new TestBuilder();
13
18
 
14
- const client = new Client({ services: testBuilder.createLocal() });
15
- afterTest(() => client.destroy());
16
- await client.initialize();
17
- await client.halo.createIdentity({ displayName: 'test-user' });
19
+ const client1 = new Client({ services: testBuilder.createLocal() });
20
+ const client2 = new Client({ services: testBuilder.createLocal() });
21
+ await client1.initialize();
22
+ await client2.initialize();
23
+ await client1.halo.createIdentity({ displayName: 'Peer 1' });
24
+ await client2.halo.createIdentity({ displayName: 'Peer 2' });
25
+
26
+ log('initialized');
27
+
28
+ afterTest(() => Promise.all([client1.destroy()]));
29
+ afterTest(() => Promise.all([client2.destroy()]));
30
+
31
+ const success1 = new Trigger<Invitation>();
32
+ const success2 = new Trigger<Invitation>();
33
+
34
+ const space1 = await client1.createSpace();
35
+ log('createSpace', { key: space1.key });
36
+ const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
37
+
38
+ observable1.subscribe({
39
+ onConnecting: (invitation) => {
40
+ const observable2 = client2.acceptInvitation(invitation);
41
+ observable2.subscribe({
42
+ onSuccess: (invitation: Invitation) => {
43
+ success2.wake(invitation);
44
+ },
45
+ onError: (err: Error) => raise(err)
46
+ });
47
+ },
48
+ onSuccess: (invitation) => {
49
+ log('onSuccess');
50
+ success1.wake(invitation);
51
+ },
52
+ onError: (err) => raise(err)
53
+ });
54
+
55
+ const [invitation1, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
56
+ expect(invitation1.spaceKey).to.deep.eq(invitation2.spaceKey);
57
+ expect(invitation1.state).to.eq(Invitation.State.SUCCESS);
18
58
 
19
59
  {
20
- const space = await client.echo.createSpace();
60
+ const space = await client2.getSpace(invitation2.spaceKey!)!.waitUntilReady();
21
61
  await testSpace(space.internal.db);
22
62
  }
23
63
  });
@@ -4,8 +4,10 @@
4
4
 
5
5
  import { expect } from 'chai';
6
6
 
7
- import { Client } from '@dxos/client';
7
+ import { asyncTimeout, Trigger } from '@dxos/async';
8
+ import { Client, Invitation, Space } from '@dxos/client';
8
9
  import { Config } from '@dxos/config';
10
+ import { raise } from '@dxos/debug';
9
11
  import { log } from '@dxos/log';
10
12
  import { createStorage, StorageType } from '@dxos/random-access-storage';
11
13
  import { describe, test, afterTest } from '@dxos/test';
@@ -23,10 +25,10 @@ describe('Spaces', () => {
23
25
  await client.halo.createIdentity({ displayName: 'test-user' });
24
26
 
25
27
  // TODO(burdon): Extend basic queries.
26
- const space = await client.echo.createSpace();
28
+ const space = await client.createSpace();
27
29
  await testSpace(space.internal.db);
28
30
 
29
- expect(space.getMembers()).to.be.length(1);
31
+ expect(space.members.get()).to.be.length(1);
30
32
  });
31
33
 
32
34
  test('creates a space re-opens the client', async () => {
@@ -40,12 +42,12 @@ describe('Spaces', () => {
40
42
  let itemId: string;
41
43
  {
42
44
  // TODO(burdon): API (client.echo/client.halo).
43
- const space = await client.echo.createSpace();
45
+ const space = await client.createSpace();
44
46
  const {
45
- objectsCreated: [item]
47
+ objectsUpdated: [item]
46
48
  } = await testSpace(space.internal.db);
47
49
  itemId = item.id;
48
- expect(space.getMembers()).to.be.length(1);
50
+ expect(space.members.get()).to.be.length(1);
49
51
  }
50
52
 
51
53
  await client.destroy();
@@ -55,9 +57,18 @@ describe('Spaces', () => {
55
57
  await client.initialize();
56
58
 
57
59
  {
58
- const result = client.echo.getSpaces();
59
- expect(result).to.have.length(1);
60
- 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();
61
72
 
62
73
  const item = space.internal.db._itemManager.getItem(itemId)!;
63
74
  expect(item).to.exist;
@@ -65,4 +76,70 @@ describe('Spaces', () => {
65
76
 
66
77
  await client.destroy();
67
78
  });
79
+
80
+ test('post and listen to messages', async () => {
81
+ const testBuilder = new TestBuilder();
82
+
83
+ const client1 = new Client({ services: testBuilder.createLocal() });
84
+ const client2 = new Client({ services: testBuilder.createLocal() });
85
+ await client1.initialize();
86
+ await client2.initialize();
87
+ await client1.halo.createIdentity({ displayName: 'Peer 1' });
88
+ await client2.halo.createIdentity({ displayName: 'Peer 2' });
89
+
90
+ log('initialized');
91
+
92
+ afterTest(() => Promise.all([client1.destroy()]));
93
+ afterTest(() => Promise.all([client2.destroy()]));
94
+
95
+ const success1 = new Trigger<Invitation>();
96
+ const success2 = new Trigger<Invitation>();
97
+
98
+ const space1 = await client1.createSpace();
99
+ log('createSpace', { key: space1.key });
100
+ const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
101
+
102
+ observable1.subscribe({
103
+ onConnecting: (invitation) => {
104
+ const observable2 = client2.acceptInvitation(invitation);
105
+ observable2.subscribe({
106
+ onSuccess: (invitation: Invitation) => {
107
+ success2.wake(invitation);
108
+ },
109
+ onError: (err: Error) => raise(err)
110
+ });
111
+ },
112
+ onSuccess: (invitation) => {
113
+ log('onSuccess');
114
+ success1.wake(invitation);
115
+ },
116
+ onError: (err) => raise(err)
117
+ });
118
+
119
+ const [_, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
120
+
121
+ const space2 = await client2.getSpace(invitation2.spaceKey!)!.waitUntilReady();
122
+
123
+ const hello = new Trigger();
124
+ {
125
+ space2.listen('hello', (message) => {
126
+ expect(message.channelId).to.include('hello');
127
+ expect(message.payload).to.deep.contain({ data: 'Hello, world!' });
128
+ hello.wake();
129
+ });
130
+ await space1.postMessage('hello', { data: 'Hello, world!' });
131
+ }
132
+
133
+ const goodbye = new Trigger();
134
+ {
135
+ space2.listen('goodbye', (message) => {
136
+ expect(message.channelId).to.include('goodbye');
137
+ expect(message.payload).to.deep.contain({ data: 'Goodbye' });
138
+ goodbye.wake();
139
+ });
140
+ await space1.postMessage('goodbye', { data: 'Goodbye' });
141
+ }
142
+
143
+ await asyncTimeout(Promise.all([hello.wait(), goodbye.wait()]), 200);
144
+ });
68
145
  });
@@ -0,0 +1,206 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import * as fc from 'fast-check';
6
+ import { ModelRunSetup } from 'fast-check';
7
+ import waitForExpect from 'wait-for-expect';
8
+
9
+ import { Client, Expando, Text } from '@dxos/client';
10
+ import { PublicKey } from '@dxos/keys';
11
+ import { log } from '@dxos/log';
12
+ import { describe, test } from '@dxos/test';
13
+ import { Doc } from '@dxos/text-model';
14
+ import { ComplexMap, ComplexSet, range } from '@dxos/util';
15
+
16
+ import { joinCommonSpace, TestBuilder } from '../testing';
17
+
18
+ // log.config({ filter: 'text.test:debug,error' });
19
+
20
+ const testBuilder = new TestBuilder();
21
+ const initialContent = 'Hello, world!';
22
+
23
+ type Model = {
24
+ text: string;
25
+ peers: ComplexSet<PublicKey>;
26
+ };
27
+
28
+ type Real = {
29
+ spaceKey: PublicKey;
30
+ peers: ComplexMap<PublicKey, Client>;
31
+ };
32
+
33
+ const assertState = async (model: Model, real: Real) => {
34
+ // Wait for replication.
35
+ await waitForExpect(() => {
36
+ for (const [peerId, peer] of real.peers.entries()) {
37
+ const space = peer.getSpace(real.spaceKey);
38
+ if (space) {
39
+ if (!model.peers.has(peerId)) {
40
+ throw new Error(`Expected peer to not be in space: ${peerId.truncate()}`);
41
+ }
42
+
43
+ const [document] = space.db.query((obj) => !!obj.content).objects;
44
+ const text = (document.content.doc as Doc).getText('utf8');
45
+ if (text.toString() !== model.text) {
46
+ throw new Error(
47
+ `Text mismatch for peer ${peerId.truncate()}: ${JSON.stringify({ expected: model.text, actual: text })}`
48
+ );
49
+ }
50
+ } else if (model.peers.has(peerId)) {
51
+ throw new Error(`Expected peer to be in space: ${peerId.truncate()}`);
52
+ }
53
+ }
54
+ }, 1000);
55
+ };
56
+
57
+ class CreatePeerCommand implements fc.AsyncCommand<Model, Real> {
58
+ constructor(readonly peerId: PublicKey) {}
59
+
60
+ check(model: Model) {
61
+ return model.peers.size === 0 || !model.peers.has(this.peerId);
62
+ }
63
+
64
+ async run(model: Model, real: Real) {
65
+ log.debug('run', { command: this.toString() });
66
+ model.peers.add(this.peerId);
67
+
68
+ // TODO(wittjosiah): Too many steps to creat client.
69
+ const services = testBuilder.createClientServicesHost();
70
+ await services.open();
71
+ const [client, server] = testBuilder.createClientServer(services);
72
+ void server.open();
73
+ await client.initialize();
74
+ await client.halo.createIdentity();
75
+ if (real.peers.size === 0) {
76
+ const space = await client.createSpace();
77
+ const content = new Text();
78
+ content.doc?.getText('utf8').insert(0, initialContent);
79
+ await space.db.add(new Expando({ content }));
80
+ real.spaceKey = space.key;
81
+ } else {
82
+ const host = Array.from(real.peers.values())[0];
83
+ await joinCommonSpace([host!, client], real.spaceKey);
84
+ }
85
+ real.peers.set(this.peerId, client);
86
+
87
+ await assertState(model, real);
88
+ }
89
+
90
+ toString() {
91
+ return `CreatePeer(peer=${this.peerId.truncate()})`;
92
+ }
93
+ }
94
+
95
+ // TODO(wittjosiah)
96
+ // class OfflineMemberCommand implements fc.AsyncCommand<Model, Real> {
97
+ // constructor(readonly peer: Client) {}
98
+ // }
99
+
100
+ // TODO(wittjosiah)
101
+ // class OnlineMemberCommand implements fc.AsyncCommand<Model, Real> {
102
+ // constructor(readonly peer: Client) {}
103
+ // }
104
+
105
+ class InsertTextCommand implements fc.AsyncCommand<Model, Real> {
106
+ constructor(readonly peerId: PublicKey, readonly index: number, readonly text: string) {}
107
+
108
+ check(model: Model) {
109
+ return model.peers.has(this.peerId) && model.text.length >= this.index;
110
+ }
111
+
112
+ async run(model: Model, real: Real) {
113
+ log.debug('run', { command: this.toString() });
114
+ model.text = model.text.slice(0, this.index) + this.text + model.text.slice(this.index);
115
+
116
+ const peer = real.peers.get(this.peerId);
117
+ const space = peer!.getSpace(real.spaceKey)!;
118
+ const [document] = space.db.query((obj) => !!obj.content).objects;
119
+ const text = (document.content.doc as Doc).getText('utf8');
120
+ text.insert(this.index, this.text);
121
+
122
+ await assertState(model, real);
123
+ }
124
+
125
+ toString() {
126
+ const text = this.text.length > 10 ? `${this.text.slice(0, 10)}...` : this.text;
127
+ return `InsertText(peer=${this.peerId.truncate()}, index=${this.index}, text=${text})`;
128
+ }
129
+ }
130
+
131
+ class RemoveTextCommand implements fc.AsyncCommand<Model, Real> {
132
+ constructor(readonly peerId: PublicKey, readonly index: number, readonly length: number) {}
133
+
134
+ check(model: Model) {
135
+ return model.peers.has(this.peerId) && model.text.length > this.index + this.length;
136
+ }
137
+
138
+ async run(model: Model, real: Real) {
139
+ log.debug('run', { command: this.toString() });
140
+ model.text = model.text.slice(0, this.index) + model.text.slice(this.index + this.length);
141
+
142
+ const peer = real.peers.get(this.peerId);
143
+ const space = peer!.getSpace(real.spaceKey)!;
144
+ const [document] = space.db.query((obj) => !!obj.content).objects;
145
+ const text = (document.content.doc as Doc).getText('utf8');
146
+ text.delete(this.index, this.length);
147
+
148
+ await assertState(model, real);
149
+ }
150
+
151
+ toString() {
152
+ return `RemoveText(peer=${this.peerId.truncate()}, index=${this.index}, length=${this.length})`;
153
+ }
154
+ }
155
+
156
+ describe('Client text replication', () => {
157
+ test('property-based tests', async () => {
158
+ // TODO(wittjosiah): Increasing to 5 causes failures.
159
+ const peerIds = range(3).map(() => PublicKey.random());
160
+ const peerId = fc.constantFrom(...peerIds);
161
+
162
+ const allCommands = [
163
+ peerId.map((peerId) => new CreatePeerCommand(peerId)),
164
+ fc
165
+ .tuple(peerId, fc.integer({ min: 0, max: 100 }), fc.unicodeString())
166
+ .map(([peerId, index, text]) => new InsertTextCommand(peerId, index, text)),
167
+ fc
168
+ .tuple(peerId, fc.integer({ min: 0, max: 100 }), fc.integer({ min: 1, max: 10 }))
169
+ .map(([peerId, index, length]) => new RemoveTextCommand(peerId, index, length))
170
+ ];
171
+ const commands = fc.commands(allCommands, { size: 'medium' });
172
+
173
+ const model = fc.asyncProperty(commands, async (commands) => {
174
+ const peers = new ComplexMap<PublicKey, Client>(PublicKey.hash);
175
+ const setup: ModelRunSetup<Model, Real> = () => ({
176
+ model: {
177
+ text: initialContent,
178
+ peers: new ComplexSet<PublicKey>(PublicKey.hash)
179
+ },
180
+ real: {
181
+ spaceKey: PublicKey.random(),
182
+ peers
183
+ }
184
+ });
185
+
186
+ await fc.asyncModelRun(setup, commands);
187
+
188
+ await Promise.all(Array.from(peers.values()).map((peer) => peer.destroy()));
189
+ });
190
+
191
+ const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
192
+ [
193
+ [
194
+ new CreatePeerCommand(peerIds[0]),
195
+ new CreatePeerCommand(peerIds[1]),
196
+ new RemoveTextCommand(peerIds[0], 7, 5),
197
+ new InsertTextCommand(peerIds[1], 7, 'DXOS')
198
+ ]
199
+ ]
200
+ ];
201
+
202
+ await fc.assert(model, { examples });
203
+ })
204
+ .onlyEnvironments('node')
205
+ .timeout(300_000);
206
+ });
@@ -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