@dxos/client-services 0.1.30-next.7c60cd3 → 0.1.30
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-JLLUQQXD.mjs → chunk-DFO3FW2W.mjs} +949 -1150
- package/dist/lib/browser/chunk-DFO3FW2W.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +5 -41
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +221 -50
- package/dist/lib/browser/packlets/testing/index.mjs.map +4 -4
- package/dist/lib/node/index.cjs +837 -1062
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +2937 -2719
- package/dist/lib/node/packlets/testing/index.cjs.map +4 -4
- package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts +1 -2
- package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitations-service.d.ts +1 -1
- package/dist/types/src/packlets/invitations/device-invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/index.d.ts +0 -6
- package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-service.d.ts +2 -3
- package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts +2 -4
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitations-service.d.ts +2 -3
- package/dist/types/src/packlets/invitations/space-invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/services/client-rpc-server.d.ts +22 -0
- package/dist/types/src/packlets/services/client-rpc-server.d.ts.map +1 -0
- package/dist/types/src/packlets/services/index.d.ts +2 -3
- package/dist/types/src/packlets/services/index.d.ts.map +1 -1
- package/dist/types/src/packlets/services/local-client-services.d.ts +3 -2
- package/dist/types/src/packlets/services/local-client-services.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +22 -8
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-registry.d.ts +3 -1
- package/dist/types/src/packlets/services/service-registry.d.ts.map +1 -1
- package/dist/types/src/packlets/services/utils.d.ts +7 -0
- package/dist/types/src/packlets/services/utils.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +8 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/index.d.ts +2 -0
- package/dist/types/src/packlets/spaces/index.d.ts.map +1 -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/system/system-service.d.ts +2 -2
- package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/host-test-builder.d.ts +43 -0
- package/dist/types/src/packlets/testing/host-test-builder.d.ts.map +1 -0
- package/dist/types/src/packlets/testing/index.d.ts +1 -0
- package/dist/types/src/packlets/testing/index.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +32 -34
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/tests/client-services.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/client-services.test.d.ts.map +1 -0
- package/dist/types/src/packlets/tests/client.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/client.test.d.ts.map +1 -0
- package/dist/types/src/packlets/tests/halo-credentials.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/halo-credentials.test.d.ts.map +1 -0
- package/dist/types/src/packlets/tests/halo-persistent.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/halo-persistent.test.d.ts.map +1 -0
- package/dist/types/src/packlets/tests/halo-profile.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/halo-profile.test.d.ts.map +1 -0
- package/dist/types/src/packlets/tests/shared-worker.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/shared-worker.test.d.ts.map +1 -0
- package/dist/types/src/packlets/tests/spaces-invitations.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/spaces-invitations.test.d.ts.map +1 -0
- package/dist/types/src/packlets/tests/spaces.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/spaces.test.d.ts.map +1 -0
- 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/iframe-host-runtime.d.ts +0 -1
- package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/index.d.ts +0 -2
- package/dist/types/src/packlets/vault/index.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/shell-runtime.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/worker-session.d.ts +4 -4
- package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
- package/package.json +31 -30
- package/src/packlets/invitations/device-invitations-handler.ts +10 -9
- package/src/packlets/invitations/device-invitations-service.test.ts +1 -1
- package/src/packlets/invitations/device-invitations-service.ts +1 -1
- package/src/packlets/invitations/index.ts +0 -6
- package/src/packlets/invitations/invitations-service.ts +7 -2
- package/src/packlets/invitations/space-invitations-handler.test.ts +3 -3
- package/src/packlets/invitations/space-invitations-handler.ts +11 -11
- package/src/packlets/invitations/space-invitations-service.test.ts +1 -1
- package/src/packlets/invitations/space-invitations-service.ts +2 -3
- package/src/packlets/services/client-rpc-server.ts +91 -0
- package/src/packlets/services/index.ts +2 -3
- package/src/packlets/services/local-client-services.ts +5 -1
- package/src/packlets/services/service-context.test.ts +4 -4
- package/src/packlets/services/service-context.ts +1 -1
- package/src/packlets/services/service-host.ts +52 -19
- package/src/packlets/services/service-registry.test.ts +2 -2
- package/src/packlets/services/service-registry.ts +9 -1
- package/src/packlets/services/utils.ts +53 -0
- package/src/packlets/spaces/data-space-manager.test.ts +64 -4
- package/src/packlets/spaces/data-space-manager.ts +11 -7
- package/src/packlets/spaces/data-space.ts +13 -1
- package/src/packlets/spaces/index.ts +2 -0
- package/src/packlets/spaces/notarization-plugin.ts +9 -2
- package/src/packlets/spaces/spaces-service.ts +25 -0
- package/src/packlets/system/system-service.ts +3 -3
- package/src/packlets/testing/host-test-builder.ts +163 -0
- package/src/packlets/testing/index.ts +1 -0
- package/src/packlets/testing/invitation-utils.ts +1 -2
- package/src/packlets/testing/test-builder.ts +156 -138
- package/src/packlets/tests/client-services.test.ts +252 -0
- package/src/packlets/tests/client.test.ts +143 -0
- package/src/packlets/tests/halo-credentials.test.ts +94 -0
- package/src/packlets/tests/halo-persistent.test.ts +53 -0
- package/src/packlets/tests/halo-profile.test.ts +74 -0
- package/src/packlets/tests/shared-worker.test.ts +93 -0
- package/src/packlets/tests/spaces-invitations.test.ts +64 -0
- package/src/packlets/tests/spaces.test.ts +136 -0
- package/src/packlets/tests/text.test.ts +206 -0
- package/src/packlets/vault/iframe-host-runtime.ts +26 -34
- package/src/packlets/vault/iframe-proxy-runtime.ts +1 -1
- package/src/packlets/vault/index.ts +0 -2
- package/src/packlets/vault/shell-runtime.ts +1 -2
- package/src/packlets/vault/worker-runtime.ts +5 -15
- package/src/packlets/vault/worker-session.ts +39 -23
- package/dist/lib/browser/chunk-JLLUQQXD.mjs.map +0 -7
- package/dist/types/src/packlets/invitations/device-invitations-proxy.d.ts +0 -8
- package/dist/types/src/packlets/invitations/device-invitations-proxy.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/encoder.d.ts +0 -9
- package/dist/types/src/packlets/invitations/encoder.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/encoder.test.d.ts +0 -2
- package/dist/types/src/packlets/invitations/encoder.test.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +0 -56
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/invitations-proxy.d.ts +0 -18
- package/dist/types/src/packlets/invitations/invitations-proxy.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/invitations.d.ts +0 -55
- package/dist/types/src/packlets/invitations/invitations.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/space-invitations-proxy.d.ts +0 -11
- package/dist/types/src/packlets/invitations/space-invitations-proxy.d.ts.map +0 -1
- package/dist/types/src/packlets/services/service-definitions.d.ts +0 -30
- package/dist/types/src/packlets/services/service-definitions.d.ts.map +0 -1
- package/dist/types/src/packlets/services/service-proxy.d.ts +0 -17
- package/dist/types/src/packlets/services/service-proxy.d.ts.map +0 -1
- package/dist/types/src/packlets/vault/services.d.ts +0 -21
- package/dist/types/src/packlets/vault/services.d.ts.map +0 -1
- package/dist/types/src/packlets/vault/shell-controller.d.ts +0 -18
- package/dist/types/src/packlets/vault/shell-controller.d.ts.map +0 -1
- package/src/packlets/invitations/device-invitations-proxy.ts +0 -14
- package/src/packlets/invitations/encoder.test.ts +0 -23
- package/src/packlets/invitations/encoder.ts +0 -26
- package/src/packlets/invitations/invitations-handler.ts +0 -68
- package/src/packlets/invitations/invitations-proxy.ts +0 -159
- package/src/packlets/invitations/invitations.ts +0 -113
- package/src/packlets/invitations/space-invitations-proxy.ts +0 -16
- package/src/packlets/services/service-definitions.ts +0 -68
- package/src/packlets/services/service-proxy.ts +0 -54
- package/src/packlets/vault/services.ts +0 -42
- package/src/packlets/vault/shell-controller.ts +0 -56
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import chai, { expect } from 'chai';
|
|
6
|
+
import chaiAsPromised from 'chai-as-promised';
|
|
7
|
+
|
|
8
|
+
import { sleep } from '@dxos/async';
|
|
9
|
+
import { Client, ClientServicesProxy, fromIFrame } from '@dxos/client';
|
|
10
|
+
import { Config } from '@dxos/config';
|
|
11
|
+
import { createLinkedPorts } from '@dxos/rpc';
|
|
12
|
+
import { describe, test, afterTest } from '@dxos/test';
|
|
13
|
+
import { MaybePromise } from '@dxos/util';
|
|
14
|
+
|
|
15
|
+
import { TestBuilder } from '../testing';
|
|
16
|
+
import { IFrameProxyRuntime, WorkerRuntime } from '../vault';
|
|
17
|
+
|
|
18
|
+
chai.use(chaiAsPromised);
|
|
19
|
+
|
|
20
|
+
const setup = (getConfig: () => MaybePromise<Config>) => {
|
|
21
|
+
const workerRuntime = new WorkerRuntime(getConfig);
|
|
22
|
+
|
|
23
|
+
const systemPorts = createLinkedPorts();
|
|
24
|
+
const workerProxyPorts = createLinkedPorts();
|
|
25
|
+
const proxyWindowPorts = createLinkedPorts();
|
|
26
|
+
const shellPorts = createLinkedPorts();
|
|
27
|
+
void workerRuntime.createSession({
|
|
28
|
+
systemPort: systemPorts[1],
|
|
29
|
+
appPort: workerProxyPorts[1],
|
|
30
|
+
shellPort: shellPorts[1]
|
|
31
|
+
});
|
|
32
|
+
const clientProxy = new IFrameProxyRuntime({
|
|
33
|
+
systemPort: systemPorts[0],
|
|
34
|
+
windowAppPort: proxyWindowPorts[0],
|
|
35
|
+
workerAppPort: workerProxyPorts[0],
|
|
36
|
+
shellPort: shellPorts[0]
|
|
37
|
+
});
|
|
38
|
+
const client = new Client({
|
|
39
|
+
services: new ClientServicesProxy(proxyWindowPorts[1])
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return { workerRuntime, clientProxy, client };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('Shared worker', () => {
|
|
46
|
+
test('client connects to the worker', async () => {
|
|
47
|
+
const { workerRuntime, clientProxy, client } = setup(() => new Config({}));
|
|
48
|
+
|
|
49
|
+
await Promise.all([workerRuntime.start(), clientProxy.open('*'), client.initialize()]);
|
|
50
|
+
|
|
51
|
+
await client.halo.createIdentity();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('initialization errors get propagated to the client', async () => {
|
|
55
|
+
const { workerRuntime, clientProxy, client } = setup(async () => {
|
|
56
|
+
await sleep(1);
|
|
57
|
+
throw new Error('Test error');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const promise = Promise.all([
|
|
61
|
+
workerRuntime.start().catch(() => {}), // This error should be propagated to client.initialize() call.
|
|
62
|
+
clientProxy.open('*')
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
await expect(client.initialize()).to.be.rejectedWith('Test error');
|
|
66
|
+
|
|
67
|
+
await promise;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('host can be initialized after client', async () => {
|
|
71
|
+
const { workerRuntime, clientProxy, client } = setup(async () => {
|
|
72
|
+
await sleep(5);
|
|
73
|
+
return new Config({});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await Promise.all([workerRuntime.start(), clientProxy.open('*'), client.initialize()]);
|
|
77
|
+
|
|
78
|
+
await client.halo.createIdentity();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// TODO(burdon): Browser-only.
|
|
82
|
+
test.skip('creates client with remote iframe', async () => {
|
|
83
|
+
const testBuilder = new TestBuilder();
|
|
84
|
+
|
|
85
|
+
const client = new Client({
|
|
86
|
+
services: fromIFrame(testBuilder.config)
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await client.initialize();
|
|
90
|
+
afterTest(() => client.destroy());
|
|
91
|
+
expect(client.initialized).to.be.true;
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
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 { describe, test, afterTest } from '@dxos/test';
|
|
12
|
+
|
|
13
|
+
import { TestBuilder, testSpace } from '../testing';
|
|
14
|
+
|
|
15
|
+
describe('Spaces/invitations', () => {
|
|
16
|
+
test('creates a space and invites a peer', async () => {
|
|
17
|
+
const testBuilder = new TestBuilder();
|
|
18
|
+
|
|
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.echo.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.echo.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);
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
const space = client2.echo.getSpace(invitation2.spaceKey!)!;
|
|
61
|
+
await testSpace(space.internal.db);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { asyncTimeout, Trigger } from '@dxos/async';
|
|
8
|
+
import { Client, Invitation } from '@dxos/client';
|
|
9
|
+
import { Config } from '@dxos/config';
|
|
10
|
+
import { raise } from '@dxos/debug';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { createStorage, StorageType } from '@dxos/random-access-storage';
|
|
13
|
+
import { describe, test, afterTest } from '@dxos/test';
|
|
14
|
+
|
|
15
|
+
import { TestBuilder, testSpace } from '../testing';
|
|
16
|
+
|
|
17
|
+
describe('Spaces', () => {
|
|
18
|
+
test('creates a space', async () => {
|
|
19
|
+
const testBuilder = new TestBuilder();
|
|
20
|
+
|
|
21
|
+
const client = new Client({ services: testBuilder.createLocal() });
|
|
22
|
+
afterTest(() => client.destroy());
|
|
23
|
+
|
|
24
|
+
await client.initialize();
|
|
25
|
+
await client.halo.createIdentity({ displayName: 'test-user' });
|
|
26
|
+
|
|
27
|
+
// TODO(burdon): Extend basic queries.
|
|
28
|
+
const space = await client.echo.createSpace();
|
|
29
|
+
await testSpace(space.internal.db);
|
|
30
|
+
|
|
31
|
+
expect(space.getMembers()).to.be.length(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('creates a space re-opens the client', async () => {
|
|
35
|
+
const testBuilder = new TestBuilder(new Config({ version: 1 }));
|
|
36
|
+
testBuilder.storage = createStorage({ type: StorageType.RAM });
|
|
37
|
+
|
|
38
|
+
const client = new Client({ services: testBuilder.createLocal() });
|
|
39
|
+
await client.initialize();
|
|
40
|
+
await client.halo.createIdentity({ displayName: 'test-user' });
|
|
41
|
+
|
|
42
|
+
let itemId: string;
|
|
43
|
+
{
|
|
44
|
+
// TODO(burdon): API (client.echo/client.halo).
|
|
45
|
+
const space = await client.echo.createSpace();
|
|
46
|
+
const {
|
|
47
|
+
objectsUpdated: [item]
|
|
48
|
+
} = await testSpace(space.internal.db);
|
|
49
|
+
itemId = item.id;
|
|
50
|
+
expect(space.getMembers()).to.be.length(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await client.destroy();
|
|
54
|
+
|
|
55
|
+
log.break();
|
|
56
|
+
|
|
57
|
+
await client.initialize();
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
const result = client.echo.getSpaces();
|
|
61
|
+
expect(result).to.have.length(1);
|
|
62
|
+
const space = result[0];
|
|
63
|
+
|
|
64
|
+
const item = space.internal.db._itemManager.getItem(itemId)!;
|
|
65
|
+
expect(item).to.exist;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await client.destroy();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('post and listen to messages', async () => {
|
|
72
|
+
const testBuilder = new TestBuilder();
|
|
73
|
+
|
|
74
|
+
const client1 = new Client({ services: testBuilder.createLocal() });
|
|
75
|
+
const client2 = new Client({ services: testBuilder.createLocal() });
|
|
76
|
+
await client1.initialize();
|
|
77
|
+
await client2.initialize();
|
|
78
|
+
await client1.halo.createIdentity({ displayName: 'Peer 1' });
|
|
79
|
+
await client2.halo.createIdentity({ displayName: 'Peer 2' });
|
|
80
|
+
|
|
81
|
+
log('initialized');
|
|
82
|
+
|
|
83
|
+
afterTest(() => Promise.all([client1.destroy()]));
|
|
84
|
+
afterTest(() => Promise.all([client2.destroy()]));
|
|
85
|
+
|
|
86
|
+
const success1 = new Trigger<Invitation>();
|
|
87
|
+
const success2 = new Trigger<Invitation>();
|
|
88
|
+
|
|
89
|
+
const space1 = await client1.echo.createSpace();
|
|
90
|
+
log('createSpace', { key: space1.key });
|
|
91
|
+
const observable1 = space1.createInvitation({ type: Invitation.Type.INTERACTIVE_TESTING });
|
|
92
|
+
|
|
93
|
+
observable1.subscribe({
|
|
94
|
+
onConnecting: (invitation) => {
|
|
95
|
+
const observable2 = client2.echo.acceptInvitation(invitation);
|
|
96
|
+
observable2.subscribe({
|
|
97
|
+
onSuccess: (invitation: Invitation) => {
|
|
98
|
+
success2.wake(invitation);
|
|
99
|
+
},
|
|
100
|
+
onError: (err: Error) => raise(err)
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
onSuccess: (invitation) => {
|
|
104
|
+
log('onSuccess');
|
|
105
|
+
success1.wake(invitation);
|
|
106
|
+
},
|
|
107
|
+
onError: (err) => raise(err)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const [_, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
|
|
111
|
+
|
|
112
|
+
const space2 = client2.echo.getSpace(invitation2.spaceKey!)!;
|
|
113
|
+
|
|
114
|
+
const hello = new Trigger();
|
|
115
|
+
{
|
|
116
|
+
space2.listen('hello', (message) => {
|
|
117
|
+
expect(message.channelId).to.include('hello');
|
|
118
|
+
expect(message.payload).to.deep.contain({ data: 'Hello, world!' });
|
|
119
|
+
hello.wake();
|
|
120
|
+
});
|
|
121
|
+
await space1.postMessage('hello', { data: 'Hello, world!' });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const goodbye = new Trigger();
|
|
125
|
+
{
|
|
126
|
+
space2.listen('goodbye', (message) => {
|
|
127
|
+
expect(message.channelId).to.include('goodbye');
|
|
128
|
+
expect(message.payload).to.deep.contain({ data: 'Goodbye' });
|
|
129
|
+
goodbye.wake();
|
|
130
|
+
});
|
|
131
|
+
await space1.postMessage('goodbye', { data: 'Goodbye' });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await asyncTimeout(Promise.all([hello.wait(), goodbye.wait()]), 200);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -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, Document, 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.echo.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.echo.createSpace();
|
|
77
|
+
const content = new Text();
|
|
78
|
+
content.doc?.getText('utf8').insert(0, initialContent);
|
|
79
|
+
await space.db.add(new Document({ 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!.echo.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!.echo.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
|
+
});
|
|
@@ -7,10 +7,11 @@ import { Config } from '@dxos/config';
|
|
|
7
7
|
import { log, logInfo } from '@dxos/log';
|
|
8
8
|
import { MemorySignalManager, MemorySignalManagerContext, WebsocketSignalManager } from '@dxos/messaging';
|
|
9
9
|
import { createWebRTCTransportFactory, NetworkManager } from '@dxos/network-manager';
|
|
10
|
-
import {
|
|
10
|
+
import { RpcPort } from '@dxos/rpc';
|
|
11
11
|
import { getAsyncValue, Provider } from '@dxos/util';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { LocalClientServices } from '../services';
|
|
14
|
+
import { ClientRpcServer, ClientRpcServerParams } from '../services/client-rpc-server';
|
|
14
15
|
import { ShellRuntime, ShellRuntimeImpl } from './shell-runtime';
|
|
15
16
|
|
|
16
17
|
const LOCK_KEY = 'DXOS_RESOURCE_LOCK';
|
|
@@ -31,9 +32,6 @@ export class IFrameHostRuntime {
|
|
|
31
32
|
private readonly _configProvider: Config | Provider<Promise<Config>>;
|
|
32
33
|
private readonly _transportFactory = createWebRTCTransportFactory();
|
|
33
34
|
private readonly _ready = new Trigger<Error | undefined>();
|
|
34
|
-
private readonly _getService: <Service>(
|
|
35
|
-
find: (services: Partial<ClientServices>) => Service | undefined
|
|
36
|
-
) => Promise<Service>;
|
|
37
35
|
|
|
38
36
|
private readonly _appPort: RpcPort;
|
|
39
37
|
private readonly _shellPort?: RpcPort;
|
|
@@ -41,7 +39,7 @@ export class IFrameHostRuntime {
|
|
|
41
39
|
|
|
42
40
|
// TODO(dmaretskyi): Replace with host and figure out how to return services provider here.
|
|
43
41
|
private _clientServices!: LocalClientServices;
|
|
44
|
-
private _clientRpc!:
|
|
42
|
+
private _clientRpc!: ClientRpcServer;
|
|
45
43
|
private _shellRuntime?: ShellRuntimeImpl;
|
|
46
44
|
|
|
47
45
|
@logInfo
|
|
@@ -52,20 +50,6 @@ export class IFrameHostRuntime {
|
|
|
52
50
|
this._appPort = appPort;
|
|
53
51
|
this._shellPort = shellPort;
|
|
54
52
|
|
|
55
|
-
this._getService = async (find) => {
|
|
56
|
-
const error = await this._ready.wait();
|
|
57
|
-
if (error) {
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const service = await find(this._clientServices.services);
|
|
62
|
-
if (!service) {
|
|
63
|
-
throw new Error('Service not found');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return service;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
53
|
if (this._shellPort) {
|
|
70
54
|
this._shellRuntime = new ShellRuntimeImpl(this._shellPort);
|
|
71
55
|
}
|
|
@@ -96,21 +80,29 @@ export class IFrameHostRuntime {
|
|
|
96
80
|
})
|
|
97
81
|
});
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
IdentityService: async () => await this._getService((services) => services.IdentityService),
|
|
108
|
-
SpaceInvitationsService: async () => await this._getService((services) => services.SpaceInvitationsService),
|
|
109
|
-
SpacesService: async () => await this._getService((services) => services.SpacesService),
|
|
110
|
-
SystemService: async () => await this._getService((services) => services.SystemService),
|
|
111
|
-
TracingService: async () => await this._getService((services) => services.TracingService)
|
|
83
|
+
const middleware: Pick<ClientRpcServerParams, 'handleCall' | 'handleStream'> = {
|
|
84
|
+
handleCall: async (method, params, handler) => {
|
|
85
|
+
const error = await this._ready.wait({ timeout: 3_000 });
|
|
86
|
+
if (error) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return handler(method, params);
|
|
112
91
|
},
|
|
113
|
-
|
|
92
|
+
handleStream: async (method, params, handler) => {
|
|
93
|
+
const error = await this._ready.wait({ timeout: 3_000 });
|
|
94
|
+
if (error) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return handler(method, params);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this._clientRpc = new ClientRpcServer({
|
|
103
|
+
serviceRegistry: this._clientServices.host.serviceRegistry,
|
|
104
|
+
port: this._appPort,
|
|
105
|
+
...middleware
|
|
114
106
|
});
|
|
115
107
|
|
|
116
108
|
await Promise.all([this._clientServices.open(), this._clientRpc.open(), this._shellRuntime?.open()]);
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { iframeServiceBundle, workerServiceBundle, WorkerServiceBundle } from '@dxos/client';
|
|
5
6
|
import { WebRTCTransportService } from '@dxos/network-manager';
|
|
6
7
|
import { BridgeService } from '@dxos/protocols/proto/dxos/mesh/bridge';
|
|
7
8
|
import { createProtoRpcPeer, ProtoRpcPeer, RpcPort } from '@dxos/rpc';
|
|
8
9
|
|
|
9
|
-
import { iframeServiceBundle, WorkerServiceBundle, workerServiceBundle } from './services';
|
|
10
10
|
import { ShellRuntime, ShellRuntimeImpl } from './shell-runtime';
|
|
11
11
|
|
|
12
12
|
// NOTE: Keep as RpcPorts to avoid dependency on @dxos/rpc-tunnel so we don't depend on browser-specific apis.
|
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
import assert from 'node:assert';
|
|
6
6
|
|
|
7
7
|
import { Event } from '@dxos/async';
|
|
8
|
+
import { appServiceBundle, AppServiceBundle, shellServiceBundle } from '@dxos/client';
|
|
8
9
|
import { PublicKey } from '@dxos/keys';
|
|
9
10
|
import { AppContextRequest, LayoutRequest, ShellLayout } from '@dxos/protocols/proto/dxos/iframe';
|
|
10
11
|
import { createProtoRpcPeer, ProtoRpcPeer, RpcPort } from '@dxos/rpc';
|
|
11
12
|
|
|
12
|
-
import { AppServiceBundle, appServiceBundle, shellServiceBundle } from './services';
|
|
13
|
-
|
|
14
13
|
export interface ShellRuntime {
|
|
15
14
|
layoutUpdate: Event<LayoutRequest>;
|
|
16
15
|
layout: ShellLayout;
|
|
@@ -36,14 +36,16 @@ export class WorkerRuntime {
|
|
|
36
36
|
// prettier-ignore
|
|
37
37
|
constructor(
|
|
38
38
|
private readonly _configProvider: () => MaybePromise<Config>
|
|
39
|
-
) {
|
|
39
|
+
) {
|
|
40
|
+
this._clientServices = new ClientServicesHost();
|
|
41
|
+
}
|
|
40
42
|
|
|
41
43
|
async start() {
|
|
42
44
|
log('starting...');
|
|
43
45
|
try {
|
|
44
46
|
this._config = await this._configProvider();
|
|
45
47
|
const signalServer = this._config.get('runtime.services.signal.server');
|
|
46
|
-
this._clientServices
|
|
48
|
+
this._clientServices.initialize({
|
|
47
49
|
config: this._config,
|
|
48
50
|
networkManager: new NetworkManager({
|
|
49
51
|
log: true,
|
|
@@ -73,19 +75,7 @@ export class WorkerRuntime {
|
|
|
73
75
|
*/
|
|
74
76
|
async createSession({ appPort, systemPort, shellPort }: CreateSessionParams) {
|
|
75
77
|
const session = new WorkerSession({
|
|
76
|
-
|
|
77
|
-
const error = await this._ready.wait();
|
|
78
|
-
if (error) {
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const service = find(this._clientServices.services);
|
|
83
|
-
if (!service) {
|
|
84
|
-
throw new Error('Service not found');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return service;
|
|
88
|
-
},
|
|
78
|
+
serviceHost: this._clientServices,
|
|
89
79
|
appPort,
|
|
90
80
|
systemPort,
|
|
91
81
|
shellPort,
|