@dxos/client-services 0.1.31-next.ea8f0bc → 0.1.31

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 (31) hide show
  1. package/dist/lib/browser/{chunk-ZIQUYEJS.mjs → chunk-DFO3FW2W.mjs} +53 -20
  2. package/dist/lib/browser/{chunk-ZIQUYEJS.mjs.map → chunk-DFO3FW2W.mjs.map} +3 -3
  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 +52 -19
  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 +114 -22
  11. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  12. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  13. package/dist/types/src/packlets/spaces/data-space.d.ts +8 -1
  14. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  15. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  16. package/dist/types/src/packlets/spaces/spaces-service.d.ts +4 -1
  17. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  18. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -0
  19. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  20. package/dist/types/src/packlets/tests/text.test.d.ts +2 -0
  21. package/dist/types/src/packlets/tests/text.test.d.ts.map +1 -0
  22. package/package.json +31 -30
  23. package/src/packlets/spaces/data-space-manager.test.ts +60 -0
  24. package/src/packlets/spaces/data-space-manager.ts +11 -7
  25. package/src/packlets/spaces/data-space.ts +13 -1
  26. package/src/packlets/spaces/notarization-plugin.ts +9 -2
  27. package/src/packlets/spaces/spaces-service.ts +25 -0
  28. package/src/packlets/testing/test-builder.ts +45 -5
  29. package/src/packlets/tests/spaces-invitations.test.ts +46 -6
  30. package/src/packlets/tests/spaces.test.ts +70 -2
  31. package/src/packlets/tests/text.test.ts +206 -0
@@ -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 } 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';
@@ -42,7 +44,7 @@ describe('Spaces', () => {
42
44
  // TODO(burdon): API (client.echo/client.halo).
43
45
  const space = await client.echo.createSpace();
44
46
  const {
45
- objectsCreated: [item]
47
+ objectsUpdated: [item]
46
48
  } = await testSpace(space.internal.db);
47
49
  itemId = item.id;
48
50
  expect(space.getMembers()).to.be.length(1);
@@ -65,4 +67,70 @@ describe('Spaces', () => {
65
67
 
66
68
  await client.destroy();
67
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
+ });
68
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
+ });