@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.
- package/dist/lib/browser/{chunk-ZIQUYEJS.mjs → chunk-E3FD43YN.mjs} +460 -213
- package/dist/lib/browser/chunk-E3FD43YN.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +64 -6
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +462 -215
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +524 -218
- package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
- package/dist/types/src/packlets/identity/identity-manager.d.ts +6 -0
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +1 -0
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +19 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +32 -5
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +29 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +4 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/host-test-builder.d.ts +2 -2
- package/dist/types/src/packlets/testing/host-test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +2 -0
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/tests/text.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/text.test.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +1 -0
- package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -1
- package/package.json +31 -30
- package/src/packlets/identity/identity-manager.ts +32 -17
- package/src/packlets/invitations/device-invitations-handler.ts +5 -3
- package/src/packlets/invitations/space-invitations-handler.test.ts +20 -3
- package/src/packlets/invitations/space-invitations-handler.ts +15 -8
- package/src/packlets/services/service-context.test.ts +6 -5
- package/src/packlets/services/service-host.ts +7 -2
- package/src/packlets/spaces/data-space-manager.test.ts +77 -2
- package/src/packlets/spaces/data-space-manager.ts +87 -37
- package/src/packlets/spaces/data-space.ts +103 -16
- package/src/packlets/spaces/notarization-plugin.test.ts +3 -1
- package/src/packlets/spaces/notarization-plugin.ts +67 -19
- package/src/packlets/spaces/spaces-service.test.ts +1 -1
- package/src/packlets/spaces/spaces-service.ts +53 -17
- package/src/packlets/testing/host-test-builder.ts +2 -2
- package/src/packlets/testing/test-builder.ts +45 -5
- package/src/packlets/tests/client-services.test.ts +24 -11
- package/src/packlets/tests/client.test.ts +3 -6
- package/src/packlets/tests/halo-profile.test.ts +5 -5
- package/src/packlets/tests/spaces-invitations.test.ts +47 -7
- package/src/packlets/tests/spaces.test.ts +86 -9
- package/src/packlets/tests/text.test.ts +206 -0
- package/src/packlets/vault/vault-resource-lock.ts +10 -1
- 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.
|
|
153
|
-
expect(client2.halo.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
72
|
-
expect(await client2.halo.
|
|
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 {
|
|
6
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
await
|
|
17
|
-
await
|
|
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
|
|
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 {
|
|
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.
|
|
28
|
+
const space = await client.createSpace();
|
|
27
29
|
await testSpace(space.internal.db);
|
|
28
30
|
|
|
29
|
-
expect(space.
|
|
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.
|
|
45
|
+
const space = await client.createSpace();
|
|
44
46
|
const {
|
|
45
|
-
|
|
47
|
+
objectsUpdated: [item]
|
|
46
48
|
} = await testSpace(space.internal.db);
|
|
47
49
|
itemId = item.id;
|
|
48
|
-
expect(space.
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|