@dxos/echo-pipeline 0.6.5 → 0.6.6-staging.23d123d
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-2MII6KJX.mjs → chunk-P6XSIJKM.mjs} +2184 -2132
- package/dist/lib/browser/chunk-P6XSIJKM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +8 -8
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-6MWU4MHX.cjs → chunk-IYTGTZ7D.cjs} +2177 -2125
- package/dist/lib/node/chunk-IYTGTZ7D.cjs.map +7 -0
- package/dist/lib/node/index.cjs +35 -35
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +18 -18
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/automerge/automerge-host.d.ts +1 -0
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +2 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +10 -1
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts +3 -28
- package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +3 -1
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +10 -3
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts +2 -1
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-host.ts +13 -1
- package/src/automerge/automerge-repo.test.ts +380 -366
- package/src/automerge/echo-network-adapter.test.ts +1 -0
- package/src/automerge/echo-network-adapter.ts +8 -0
- package/src/automerge/echo-replicator.ts +11 -1
- package/src/automerge/mesh-echo-replicator-connection.ts +18 -0
- package/src/automerge/mesh-echo-replicator.ts +10 -2
- package/src/automerge/network-protocol.ts +8 -34
- package/src/space/space-manager.ts +15 -2
- package/src/space/space-protocol.ts +42 -5
- package/src/space/space.ts +4 -4
- package/src/testing/test-network-adapter.ts +5 -3
- package/dist/lib/browser/chunk-2MII6KJX.mjs.map +0 -7
- package/dist/lib/node/chunk-6MWU4MHX.cjs.map +0 -7
|
@@ -7,37 +7,42 @@ import waitForExpect from 'wait-for-expect';
|
|
|
7
7
|
|
|
8
8
|
import { asyncTimeout, sleep } from '@dxos/async';
|
|
9
9
|
import {
|
|
10
|
-
next as A,
|
|
11
|
-
type Heads,
|
|
12
10
|
change,
|
|
13
11
|
clone,
|
|
14
12
|
equals,
|
|
15
13
|
from,
|
|
16
14
|
getBackend,
|
|
17
15
|
getHeads,
|
|
16
|
+
type Heads,
|
|
17
|
+
next as A,
|
|
18
18
|
save,
|
|
19
19
|
saveSince,
|
|
20
20
|
} from '@dxos/automerge/automerge';
|
|
21
21
|
import {
|
|
22
|
-
type
|
|
23
|
-
|
|
24
|
-
type PeerId,
|
|
22
|
+
type AutomergeUrl,
|
|
23
|
+
type DocHandle,
|
|
25
24
|
type DocumentId,
|
|
25
|
+
generateAutomergeUrl,
|
|
26
26
|
type HandleState,
|
|
27
|
-
type AutomergeUrl,
|
|
28
27
|
parseAutomergeUrl,
|
|
29
|
-
|
|
28
|
+
type StorageAdapterInterface,
|
|
29
|
+
type PeerId,
|
|
30
|
+
Repo,
|
|
31
|
+
type SharePolicy,
|
|
30
32
|
} from '@dxos/automerge/automerge-repo';
|
|
31
33
|
import { randomBytes } from '@dxos/crypto';
|
|
32
34
|
import { PublicKey } from '@dxos/keys';
|
|
33
35
|
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
34
36
|
import { TestBuilder as TeleportBuilder, TestPeer as TeleportPeer } from '@dxos/teleport/testing';
|
|
35
37
|
import { afterTest, describe, openAndClose, test } from '@dxos/test';
|
|
38
|
+
import { nonNullable, range } from '@dxos/util';
|
|
36
39
|
|
|
37
40
|
import { EchoNetworkAdapter } from './echo-network-adapter';
|
|
38
41
|
import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
|
|
39
42
|
import { MeshEchoReplicator } from './mesh-echo-replicator';
|
|
40
|
-
import { TestAdapter } from '../testing';
|
|
43
|
+
import { TestAdapter, type TestConnectionStateProvider } from '../testing';
|
|
44
|
+
|
|
45
|
+
const HOST_AND_CLIENT: [string, string] = ['host', 'client'];
|
|
41
46
|
|
|
42
47
|
describe('AutomergeRepo', () => {
|
|
43
48
|
test('change events', () => {
|
|
@@ -58,9 +63,7 @@ describe('AutomergeRepo', () => {
|
|
|
58
63
|
});
|
|
59
64
|
|
|
60
65
|
test('flush', async () => {
|
|
61
|
-
const
|
|
62
|
-
const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
63
|
-
await openAndClose(level, storage);
|
|
66
|
+
const storage = await createLevelAdapter();
|
|
64
67
|
|
|
65
68
|
const repo = new Repo({
|
|
66
69
|
network: [],
|
|
@@ -78,10 +81,6 @@ describe('AutomergeRepo', () => {
|
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
test('getChangeByHash', async () => {
|
|
81
|
-
const level = createTestLevel();
|
|
82
|
-
const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
83
|
-
await openAndClose(level, storage);
|
|
84
|
-
|
|
85
84
|
const doc = from({ foo: 'bar' });
|
|
86
85
|
const copy = clone(doc);
|
|
87
86
|
const newDoc = change(copy, 'change', (doc: any) => {
|
|
@@ -113,59 +112,27 @@ describe('AutomergeRepo', () => {
|
|
|
113
112
|
});
|
|
114
113
|
|
|
115
114
|
test('documents missing from local storage go to requesting state', async () => {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
clientAdapter.receive(message);
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
const clientAdapter: TestAdapter = new TestAdapter({
|
|
123
|
-
send: (message: Message) => {
|
|
124
|
-
if (message.type !== 'doc-unavailable' && message.type !== 'sync') {
|
|
125
|
-
hostAdapter.receive(message);
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const host = new Repo({
|
|
131
|
-
network: [hostAdapter],
|
|
132
|
-
});
|
|
133
|
-
const _client = new Repo({
|
|
134
|
-
network: [clientAdapter],
|
|
135
|
-
});
|
|
136
|
-
hostAdapter.ready();
|
|
137
|
-
clientAdapter.ready();
|
|
138
|
-
await hostAdapter.onConnect.wait();
|
|
139
|
-
await clientAdapter.onConnect.wait();
|
|
140
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
141
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
142
|
-
|
|
115
|
+
const { repos, adapters } = await createHostClientRepoTopology();
|
|
116
|
+
const [host] = repos;
|
|
117
|
+
await connectAdapters(adapters);
|
|
143
118
|
const url = 'automerge:3JN8F3Z4dUWEEKKFN7WE9gEGvVUT';
|
|
144
119
|
const handle = host.find(url as AutomergeUrl);
|
|
145
120
|
await handle.whenReady(['requesting']);
|
|
146
121
|
});
|
|
147
122
|
|
|
148
123
|
test('documents on disk go to ready state', async () => {
|
|
149
|
-
const
|
|
150
|
-
const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
151
|
-
await openAndClose(level, storage);
|
|
124
|
+
const storage = await createLevelAdapter();
|
|
152
125
|
|
|
153
126
|
let url: AutomergeUrl | undefined;
|
|
154
127
|
{
|
|
155
|
-
const repo = new Repo({
|
|
156
|
-
network: [],
|
|
157
|
-
storage,
|
|
158
|
-
});
|
|
128
|
+
const repo = new Repo({ network: [], storage });
|
|
159
129
|
const handle = repo.create<{ field?: string }>();
|
|
160
130
|
url = handle.url;
|
|
161
131
|
await repo.flush();
|
|
162
132
|
}
|
|
163
133
|
|
|
164
134
|
{
|
|
165
|
-
const repo = new Repo({
|
|
166
|
-
network: [],
|
|
167
|
-
storage,
|
|
168
|
-
});
|
|
135
|
+
const repo = new Repo({ network: [], storage });
|
|
169
136
|
const handle = repo.find(url as AutomergeUrl);
|
|
170
137
|
|
|
171
138
|
let requestingState = false;
|
|
@@ -181,25 +148,9 @@ describe('AutomergeRepo', () => {
|
|
|
181
148
|
|
|
182
149
|
describe('network', () => {
|
|
183
150
|
test('basic networking', async () => {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const clientAdapter: TestAdapter = new TestAdapter({
|
|
188
|
-
send: (message: Message) => hostAdapter.receive(message),
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const host = new Repo({
|
|
192
|
-
network: [hostAdapter],
|
|
193
|
-
});
|
|
194
|
-
const client = new Repo({
|
|
195
|
-
network: [clientAdapter],
|
|
196
|
-
});
|
|
197
|
-
hostAdapter.ready();
|
|
198
|
-
clientAdapter.ready();
|
|
199
|
-
await hostAdapter.onConnect.wait();
|
|
200
|
-
await clientAdapter.onConnect.wait();
|
|
201
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
202
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
151
|
+
const { repos, adapters } = await createHostClientRepoTopology();
|
|
152
|
+
const [host, client] = repos;
|
|
153
|
+
await connectAdapters(adapters);
|
|
203
154
|
|
|
204
155
|
const handle = host.create();
|
|
205
156
|
const text = 'Hello world';
|
|
@@ -212,25 +163,13 @@ describe('AutomergeRepo', () => {
|
|
|
212
163
|
});
|
|
213
164
|
|
|
214
165
|
test('share policy gets enabled afterwards', async () => {
|
|
215
|
-
const [hostAdapter, clientAdapter] = TestAdapter.createPair();
|
|
216
166
|
let sharePolicy = false;
|
|
217
167
|
|
|
218
|
-
const
|
|
219
|
-
network: [hostAdapter],
|
|
220
|
-
peerId: 'host' as PeerId,
|
|
221
|
-
sharePolicy: async () => sharePolicy,
|
|
222
|
-
});
|
|
223
|
-
const client = new Repo({
|
|
224
|
-
network: [clientAdapter],
|
|
225
|
-
peerId: 'client' as PeerId,
|
|
168
|
+
const { repos, adapters } = await createHostClientRepoTopology({
|
|
226
169
|
sharePolicy: async () => sharePolicy,
|
|
227
170
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
await hostAdapter.onConnect.wait();
|
|
231
|
-
await clientAdapter.onConnect.wait();
|
|
232
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
233
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
171
|
+
const [host, client] = repos;
|
|
172
|
+
await connectAdapters(adapters);
|
|
234
173
|
|
|
235
174
|
const handle = host.create();
|
|
236
175
|
const text = 'Hello world';
|
|
@@ -253,35 +192,25 @@ describe('AutomergeRepo', () => {
|
|
|
253
192
|
});
|
|
254
193
|
|
|
255
194
|
test('two documents and share policy switching', async () => {
|
|
256
|
-
const [hostAdapter, clientAdapter] = TestAdapter.createPair();
|
|
257
195
|
const allowedDocs: DocumentId[] = [];
|
|
258
196
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
peerId: 'client' as PeerId,
|
|
268
|
-
sharePolicy: async (_, docId) => (docId ? allowedDocs.includes(docId) && !!client.handles[docId] : false),
|
|
197
|
+
const { repos, adapters } = await createHostClientRepoTopology({
|
|
198
|
+
sharePolicy: async (peerId, docId): Promise<boolean> => {
|
|
199
|
+
if (peerId === 'host') {
|
|
200
|
+
return docId ? allowedDocs.includes(docId) && !!client.handles[docId] : false;
|
|
201
|
+
} else {
|
|
202
|
+
return docId ? allowedDocs.includes(docId) && !!host.handles[docId] : false;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
269
205
|
});
|
|
206
|
+
const [host, client] = repos;
|
|
270
207
|
|
|
271
208
|
const firstHandle = host.create();
|
|
272
209
|
firstHandle.change((doc: any) => (doc.text = 'Hello world'));
|
|
273
210
|
await host.find(firstHandle.url).whenReady();
|
|
274
211
|
allowedDocs.push(firstHandle.documentId);
|
|
275
212
|
|
|
276
|
-
|
|
277
|
-
// Initiate connection.
|
|
278
|
-
hostAdapter.ready();
|
|
279
|
-
clientAdapter.ready();
|
|
280
|
-
await hostAdapter.onConnect.wait();
|
|
281
|
-
await clientAdapter.onConnect.wait();
|
|
282
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
283
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
284
|
-
}
|
|
213
|
+
await connectAdapters(adapters);
|
|
285
214
|
|
|
286
215
|
{
|
|
287
216
|
const firstDocOnClient = client.find(firstHandle.url);
|
|
@@ -304,27 +233,11 @@ describe('AutomergeRepo', () => {
|
|
|
304
233
|
test('recovering from a lost connection', async () => {
|
|
305
234
|
let connectionState: 'on' | 'off' = 'on';
|
|
306
235
|
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
});
|
|
310
|
-
const clientAdapter: TestAdapter = new TestAdapter({
|
|
311
|
-
send: (message: Message) => connectionState === 'on' && sleep(10).then(() => hostAdapter.receive(message)),
|
|
236
|
+
const { repos, adapters } = await createHostClientRepoTopology({
|
|
237
|
+
connectionStateProvider: () => connectionState,
|
|
312
238
|
});
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
network: [hostAdapter],
|
|
316
|
-
});
|
|
317
|
-
const client = new Repo({
|
|
318
|
-
network: [clientAdapter],
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Establish connection.
|
|
322
|
-
hostAdapter.ready();
|
|
323
|
-
clientAdapter.ready();
|
|
324
|
-
await hostAdapter.onConnect.wait();
|
|
325
|
-
await clientAdapter.onConnect.wait();
|
|
326
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
327
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
239
|
+
const [host, client] = repos;
|
|
240
|
+
await connectAdapters(adapters);
|
|
328
241
|
|
|
329
242
|
const handle = host.create();
|
|
330
243
|
const docOnClient = client.find(handle.url);
|
|
@@ -354,10 +267,7 @@ describe('AutomergeRepo', () => {
|
|
|
354
267
|
const onlineText = 'This has been written after the connection was re-established';
|
|
355
268
|
{
|
|
356
269
|
connectionState = 'on';
|
|
357
|
-
|
|
358
|
-
clientAdapter.peerDisconnected(hostAdapter.peerId!);
|
|
359
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
360
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
270
|
+
await reconnectAdapters(adapters);
|
|
361
271
|
|
|
362
272
|
handle.change((doc: any) => {
|
|
363
273
|
doc.onlineText = onlineText;
|
|
@@ -368,126 +278,17 @@ describe('AutomergeRepo', () => {
|
|
|
368
278
|
}
|
|
369
279
|
});
|
|
370
280
|
|
|
371
|
-
test('integration test with teleport', async () => {
|
|
372
|
-
const [spaceKey] = PublicKey.randomSequence();
|
|
373
|
-
|
|
374
|
-
const createAutomergeRepo = async () => {
|
|
375
|
-
const meshAdapter = new MeshEchoReplicator();
|
|
376
|
-
const echoAdapter = new EchoNetworkAdapter({
|
|
377
|
-
getContainingSpaceForDocument: async () => spaceKey,
|
|
378
|
-
onCollectionStateQueried: () => {},
|
|
379
|
-
onCollectionStateReceived: () => {},
|
|
380
|
-
});
|
|
381
|
-
const repo = new Repo({
|
|
382
|
-
network: [echoAdapter],
|
|
383
|
-
});
|
|
384
|
-
await echoAdapter.open();
|
|
385
|
-
await echoAdapter.whenConnected();
|
|
386
|
-
await echoAdapter.addReplicator(meshAdapter);
|
|
387
|
-
return { repo, meshAdapter };
|
|
388
|
-
};
|
|
389
|
-
const peer1 = await createAutomergeRepo();
|
|
390
|
-
const peer2 = await createAutomergeRepo();
|
|
391
|
-
|
|
392
|
-
const handle = peer1.repo.create();
|
|
393
|
-
|
|
394
|
-
const teleportBuilder = new TeleportBuilder();
|
|
395
|
-
afterTest(() => teleportBuilder.destroy());
|
|
396
|
-
|
|
397
|
-
const [teleportPeer1, teleportPeer2] = teleportBuilder.createPeers({ factory: () => new TeleportPeer() });
|
|
398
|
-
{
|
|
399
|
-
// Initiate connection.
|
|
400
|
-
const [connection1, connection2] = await teleportBuilder.connect(teleportPeer1, teleportPeer2);
|
|
401
|
-
connection1.teleport.addExtension('automerge', peer1.meshAdapter.createExtension());
|
|
402
|
-
connection2.teleport.addExtension('automerge', peer2.meshAdapter.createExtension());
|
|
403
|
-
|
|
404
|
-
// Test connection.
|
|
405
|
-
const text = 'Hello world';
|
|
406
|
-
handle.change((doc: any) => {
|
|
407
|
-
doc.text = text;
|
|
408
|
-
});
|
|
409
|
-
await waitForExpect(async () => {
|
|
410
|
-
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
411
|
-
const doc = await asyncTimeout(docOnPeer2.doc(), 1000);
|
|
412
|
-
expect(doc.text).to.eq(text);
|
|
413
|
-
}, 1000);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const offlineText = 'This has been written while the connection was off';
|
|
417
|
-
{
|
|
418
|
-
// Disconnect peers.
|
|
419
|
-
await teleportBuilder.disconnect(teleportPeer1, teleportPeer2);
|
|
420
|
-
|
|
421
|
-
// Make offline changes.
|
|
422
|
-
const offlineText = 'This has been written while the connection was off';
|
|
423
|
-
handle.change((doc: any) => {
|
|
424
|
-
doc.offlineText = offlineText;
|
|
425
|
-
});
|
|
426
|
-
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
427
|
-
await sleep(100);
|
|
428
|
-
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).to.be.undefined;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
{
|
|
432
|
-
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
433
|
-
const receivedUpdate = new Promise((resolve, reject) => docOnPeer2.once('heads-changed', resolve));
|
|
434
|
-
|
|
435
|
-
// Reconnect peers.
|
|
436
|
-
const [connection1, connection2] = await teleportBuilder.connect(teleportPeer1, teleportPeer2);
|
|
437
|
-
connection1.teleport.addExtension('automerge', peer1.meshAdapter.createExtension());
|
|
438
|
-
connection2.teleport.addExtension('automerge', peer2.meshAdapter.createExtension());
|
|
439
|
-
|
|
440
|
-
// Wait for offline changes to be synced.
|
|
441
|
-
await receivedUpdate;
|
|
442
|
-
await docOnPeer2.whenReady();
|
|
443
|
-
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).to.eq(offlineText);
|
|
444
|
-
|
|
445
|
-
// Test connection.
|
|
446
|
-
const onlineText = 'This has been written after the connection was re-established';
|
|
447
|
-
const receivedOnlineUpdate = new Promise((resolve, reject) => docOnPeer2.once('heads-changed', resolve));
|
|
448
|
-
handle.change((doc: any) => {
|
|
449
|
-
doc.onlineText = onlineText;
|
|
450
|
-
});
|
|
451
|
-
await receivedOnlineUpdate;
|
|
452
|
-
await docOnPeer2.whenReady();
|
|
453
|
-
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).onlineText).to.eq(onlineText);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
281
|
test('replication though a 4 peer chain', async () => {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
sharePolicy: async () => true,
|
|
466
|
-
});
|
|
467
|
-
const _repoB = new Repo({
|
|
468
|
-
peerId: 'B' as any,
|
|
469
|
-
network: [pairAB[1], pairBC[0]],
|
|
470
|
-
sharePolicy: async () => true,
|
|
282
|
+
const { repos, adapters } = await createRepoTopology({
|
|
283
|
+
peers: ['A', 'B', 'C', 'D'],
|
|
284
|
+
connections: [
|
|
285
|
+
['A', 'B'],
|
|
286
|
+
['B', 'C'],
|
|
287
|
+
['C', 'D'],
|
|
288
|
+
],
|
|
471
289
|
});
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
network: [pairBC[1], pairCD[0]],
|
|
475
|
-
sharePolicy: async () => true,
|
|
476
|
-
});
|
|
477
|
-
const repoD = new Repo({
|
|
478
|
-
peerId: 'D' as any,
|
|
479
|
-
network: [pairCD[1]],
|
|
480
|
-
sharePolicy: async () => true,
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
for (const pair of [pairAB, pairBC, pairCD]) {
|
|
484
|
-
pair[0].ready();
|
|
485
|
-
pair[1].ready();
|
|
486
|
-
await pair[0].onConnect.wait();
|
|
487
|
-
await pair[1].onConnect.wait();
|
|
488
|
-
pair[0].peerCandidate(pair[1].peerId!);
|
|
489
|
-
pair[1].peerCandidate(pair[0].peerId!);
|
|
490
|
-
}
|
|
290
|
+
const [repoA, _, __, repoD] = repos;
|
|
291
|
+
await connectAdapters(adapters);
|
|
491
292
|
|
|
492
293
|
const docA = repoA.create();
|
|
493
294
|
// NOTE: Doesn't work if the doc is empty.
|
|
@@ -503,33 +304,15 @@ describe('AutomergeRepo', () => {
|
|
|
503
304
|
});
|
|
504
305
|
|
|
505
306
|
test('replication though a 3 peer chain', async () => {
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
sharePolicy: async () => true,
|
|
513
|
-
});
|
|
514
|
-
const repoB = new Repo({
|
|
515
|
-
peerId: 'B' as any,
|
|
516
|
-
network: [pairAB[1], pairBC[0]],
|
|
517
|
-
sharePolicy: async () => true,
|
|
518
|
-
});
|
|
519
|
-
const repoC = new Repo({
|
|
520
|
-
peerId: 'C' as any,
|
|
521
|
-
network: [pairBC[1]],
|
|
522
|
-
sharePolicy: async () => true,
|
|
307
|
+
const { repos, adapters } = await createRepoTopology({
|
|
308
|
+
peers: ['A', 'B', 'C'],
|
|
309
|
+
connections: [
|
|
310
|
+
['A', 'B'],
|
|
311
|
+
['B', 'C'],
|
|
312
|
+
],
|
|
523
313
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
pair[0].ready();
|
|
527
|
-
pair[1].ready();
|
|
528
|
-
await pair[0].onConnect.wait();
|
|
529
|
-
await pair[1].onConnect.wait();
|
|
530
|
-
pair[0].peerCandidate(pair[1].peerId!);
|
|
531
|
-
pair[1].peerCandidate(pair[0].peerId!);
|
|
532
|
-
}
|
|
314
|
+
const [repoA, repoB, repoC] = repos;
|
|
315
|
+
await connectAdapters(adapters);
|
|
533
316
|
|
|
534
317
|
const docA = repoA.create();
|
|
535
318
|
// NOTE: Doesn't work if the doc is empty.
|
|
@@ -537,77 +320,44 @@ describe('AutomergeRepo', () => {
|
|
|
537
320
|
doc.text = 'Hello world';
|
|
538
321
|
});
|
|
539
322
|
|
|
540
|
-
const
|
|
323
|
+
const _ = repoB.find(docA.url);
|
|
541
324
|
const docC = repoC.find(docA.url);
|
|
542
|
-
|
|
543
325
|
await docC.whenReady();
|
|
544
326
|
});
|
|
545
327
|
|
|
546
328
|
test('replicate document after request', async () => {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
network: [adapter1],
|
|
551
|
-
sharePolicy: async () => true,
|
|
552
|
-
});
|
|
553
|
-
const repoB = new Repo({
|
|
554
|
-
peerId: 'B' as any,
|
|
555
|
-
network: [adapter2],
|
|
556
|
-
sharePolicy: async () => true,
|
|
557
|
-
});
|
|
329
|
+
const { repos, adapters } = await createHostClientRepoTopology();
|
|
330
|
+
const [host, client] = repos;
|
|
331
|
+
const [[adapter1, adapter2]] = adapters;
|
|
558
332
|
|
|
559
333
|
const unavailable: HandleState = 'unavailable';
|
|
334
|
+
await connectAdapters(adapters, { noEmitPeerCandidate: true });
|
|
560
335
|
|
|
561
|
-
|
|
562
|
-
// Connect repos.
|
|
563
|
-
adapter1.ready();
|
|
564
|
-
adapter2.ready();
|
|
565
|
-
await adapter1.onConnect.wait();
|
|
566
|
-
await adapter2.onConnect.wait();
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const docA = repoA.create();
|
|
336
|
+
const docA = host.create();
|
|
570
337
|
// NOTE: Doesn't work if the doc is empty.
|
|
571
338
|
docA.change((doc: any) => {
|
|
572
339
|
doc.text = 'Hello world';
|
|
573
340
|
});
|
|
574
341
|
|
|
575
|
-
const docB =
|
|
576
|
-
{
|
|
577
|
-
// Request document from repoB.
|
|
578
|
-
await asyncTimeout(docB.whenReady([unavailable]), 1_000);
|
|
579
|
-
}
|
|
342
|
+
const docB = client.find(docA.url);
|
|
580
343
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// await (docB.whenReady([unavailable]), 1_000);
|
|
584
|
-
}
|
|
344
|
+
// Request document from repoB.
|
|
345
|
+
await asyncTimeout(docB.whenReady([unavailable]), 1_000);
|
|
585
346
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
adapter2.peerCandidate(adapter1.peerId!);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
{
|
|
593
|
-
await asyncTimeout(docB.whenReady([unavailable]), 1_000);
|
|
594
|
-
}
|
|
347
|
+
adapter1.peerCandidate(adapter2.peerId!);
|
|
348
|
+
await sleep(100);
|
|
349
|
+
adapter2.peerCandidate(adapter1.peerId!);
|
|
595
350
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
adapter1.peerDisconnected(adapter2.peerId!);
|
|
599
|
-
adapter2.peerDisconnected(adapter1.peerId!);
|
|
600
|
-
adapter1.peerCandidate(adapter2.peerId!);
|
|
601
|
-
adapter2.peerCandidate(adapter1.peerId!);
|
|
351
|
+
// Wait becomes unavailable.
|
|
352
|
+
await asyncTimeout(docB.whenReady([unavailable]), 1_000);
|
|
602
353
|
|
|
603
|
-
|
|
604
|
-
|
|
354
|
+
// Wait gets loaded after reconnect.
|
|
355
|
+
await reconnectAdapters(adapters);
|
|
356
|
+
await asyncTimeout(docB.whenReady(), 1_000);
|
|
605
357
|
});
|
|
606
358
|
|
|
607
359
|
test('documents loaded from disk get replicated', async () => {
|
|
608
|
-
const
|
|
609
|
-
const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
610
|
-
await openAndClose(level, storage);
|
|
360
|
+
const storage = await createLevelAdapter();
|
|
611
361
|
|
|
612
362
|
let url: AutomergeUrl | undefined;
|
|
613
363
|
{
|
|
@@ -619,29 +369,17 @@ describe('AutomergeRepo', () => {
|
|
|
619
369
|
url = handle.url;
|
|
620
370
|
}
|
|
621
371
|
|
|
622
|
-
const
|
|
623
|
-
const peer1 =
|
|
624
|
-
|
|
625
|
-
storage,
|
|
626
|
-
sharePolicy: async () => true,
|
|
627
|
-
});
|
|
628
|
-
const peer2 = new Repo({
|
|
629
|
-
network: [adapter2],
|
|
630
|
-
sharePolicy: async () => true,
|
|
631
|
-
});
|
|
632
|
-
adapter1.ready();
|
|
633
|
-
adapter2.ready();
|
|
634
|
-
await adapter1.onConnect.wait();
|
|
635
|
-
await adapter2.onConnect.wait();
|
|
636
|
-
adapter1.peerCandidate(adapter2.peerId!);
|
|
637
|
-
adapter2.peerCandidate(adapter1.peerId!);
|
|
372
|
+
const { repos, adapters } = await createHostClientRepoTopology({ storages: [storage] });
|
|
373
|
+
const [peer1, peer2] = repos;
|
|
374
|
+
await connectAdapters(adapters);
|
|
638
375
|
|
|
639
376
|
// Load doc on peer1
|
|
640
377
|
const hostHandle = peer1.find(url as AutomergeUrl);
|
|
641
378
|
|
|
642
379
|
// Doc should be pushed to peer2
|
|
643
380
|
await waitForExpect(() => {
|
|
644
|
-
expect(
|
|
381
|
+
expect(hostHandle.docSync().text).not.to.be.undefined;
|
|
382
|
+
expect(peer2.handles[hostHandle.documentId].docSync()).to.deep.eq(hostHandle.docSync());
|
|
645
383
|
});
|
|
646
384
|
});
|
|
647
385
|
|
|
@@ -716,37 +454,18 @@ describe('AutomergeRepo', () => {
|
|
|
716
454
|
});
|
|
717
455
|
|
|
718
456
|
test('two repo sync docs on `update` call', async () => {
|
|
719
|
-
const
|
|
720
|
-
const repoA =
|
|
721
|
-
|
|
722
|
-
network: [adapter1],
|
|
723
|
-
sharePolicy: async () => true,
|
|
724
|
-
});
|
|
725
|
-
const repoB = new Repo({
|
|
726
|
-
peerId: 'B' as any,
|
|
727
|
-
network: [adapter2],
|
|
728
|
-
sharePolicy: async () => true,
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
{
|
|
732
|
-
// Connect repos.
|
|
733
|
-
adapter1.ready();
|
|
734
|
-
adapter2.ready();
|
|
735
|
-
await adapter1.onConnect.wait();
|
|
736
|
-
await adapter2.onConnect.wait();
|
|
737
|
-
adapter1.peerCandidate(adapter2.peerId!);
|
|
738
|
-
adapter2.peerCandidate(adapter1.peerId!);
|
|
739
|
-
}
|
|
457
|
+
const { repos, adapters } = await createHostClientRepoTopology();
|
|
458
|
+
const [repoA, repoB] = repos;
|
|
459
|
+
await connectAdapters(adapters);
|
|
740
460
|
|
|
741
461
|
const handleA = repoA.create();
|
|
742
462
|
const handleB = repoB.find(handleA.url);
|
|
743
463
|
|
|
744
464
|
const text = 'Hello world';
|
|
745
465
|
handleA.update((doc: any) => {
|
|
746
|
-
|
|
466
|
+
return A.change(doc, (doc: any) => {
|
|
747
467
|
doc.text = text;
|
|
748
468
|
});
|
|
749
|
-
return newDoc;
|
|
750
469
|
});
|
|
751
470
|
|
|
752
471
|
expect(handleA.docSync().text).to.equal(text);
|
|
@@ -755,4 +474,299 @@ describe('AutomergeRepo', () => {
|
|
|
755
474
|
expect(handleB.docSync().text).to.equal(text);
|
|
756
475
|
});
|
|
757
476
|
});
|
|
477
|
+
|
|
478
|
+
describe('teleport', () => {
|
|
479
|
+
test('integration test with teleport', async () => {
|
|
480
|
+
const [spaceKey] = PublicKey.randomSequence();
|
|
481
|
+
|
|
482
|
+
const teleportBuilder = new TeleportBuilder();
|
|
483
|
+
afterTest(() => teleportBuilder.destroy());
|
|
484
|
+
|
|
485
|
+
const peer1 = await createTeleportTestPeer(teleportBuilder, spaceKey);
|
|
486
|
+
const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey);
|
|
487
|
+
|
|
488
|
+
const handle = peer1.repo.create();
|
|
489
|
+
{
|
|
490
|
+
// Initiate connection and test the connection.
|
|
491
|
+
await connectPeers(spaceKey, teleportBuilder, peer1, peer2);
|
|
492
|
+
const text = 'Hello world';
|
|
493
|
+
handle.change((doc: any) => {
|
|
494
|
+
doc.text = text;
|
|
495
|
+
});
|
|
496
|
+
await waitForExpect(async () => {
|
|
497
|
+
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
498
|
+
const doc = await asyncTimeout(docOnPeer2.doc(), 1000);
|
|
499
|
+
expect(doc.text).to.eq(text);
|
|
500
|
+
}, 1000);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const offlineText = 'This has been written while the connection was off';
|
|
504
|
+
{
|
|
505
|
+
// Disconnect peers and make offline changes.
|
|
506
|
+
await teleportBuilder.disconnect(peer1.teleport, peer2.teleport);
|
|
507
|
+
const offlineText = 'This has been written while the connection was off';
|
|
508
|
+
handle.change((doc: any) => {
|
|
509
|
+
doc.offlineText = offlineText;
|
|
510
|
+
});
|
|
511
|
+
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
512
|
+
await sleep(100);
|
|
513
|
+
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).to.be.undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
{
|
|
517
|
+
// Wait for offline changes to be synced after reconnect.
|
|
518
|
+
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
519
|
+
await docChangeAfterAction(docOnPeer2, () => connectPeers(spaceKey, teleportBuilder, peer1, peer2));
|
|
520
|
+
await docOnPeer2.whenReady();
|
|
521
|
+
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).to.eq(offlineText);
|
|
522
|
+
|
|
523
|
+
// Test connection.
|
|
524
|
+
const onlineText = 'This has been written after the connection was re-established';
|
|
525
|
+
await docChangeAfterAction(docOnPeer2, async () => {
|
|
526
|
+
handle.change((doc: any) => {
|
|
527
|
+
doc.onlineText = onlineText;
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
await docOnPeer2.whenReady();
|
|
531
|
+
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).onlineText).to.eq(onlineText);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('pulls a document if it is in a remote collection', async () => {
|
|
536
|
+
const [spaceKey] = PublicKey.randomSequence();
|
|
537
|
+
|
|
538
|
+
const teleportBuilder = new TeleportBuilder();
|
|
539
|
+
afterTest(() => teleportBuilder.destroy());
|
|
540
|
+
|
|
541
|
+
const peerWithDocs = await createTeleportPeerWithStoredDocs(teleportBuilder, spaceKey, async (repo) => {
|
|
542
|
+
return range(2, (idx) => {
|
|
543
|
+
const document = repo.create();
|
|
544
|
+
document.change((doc: any) => (doc.text = 'hello ' + idx));
|
|
545
|
+
return document;
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
const [docInRemoteCollection, docNotInRemoteCollection] = peerWithDocs.documents;
|
|
549
|
+
|
|
550
|
+
// Create a peer that shares one of the collections.
|
|
551
|
+
const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey, {
|
|
552
|
+
localDocuments: [],
|
|
553
|
+
remoteCollections: { [peerWithDocs.peerId]: [docInRemoteCollection.documentId] },
|
|
554
|
+
});
|
|
555
|
+
await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
|
|
556
|
+
|
|
557
|
+
const shouldNotFindDoc = peer2.repo.find(docNotInRemoteCollection.url);
|
|
558
|
+
const shouldFindDoc = peer2.repo.find(docInRemoteCollection.url);
|
|
559
|
+
await shouldFindDoc.whenReady();
|
|
560
|
+
expect(shouldFindDoc.docSync()).to.deep.eq(docInRemoteCollection.docSync());
|
|
561
|
+
await shouldNotFindDoc.whenReady(['unavailable']);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* This can happen if there's no local document and sharePolicy returns true to every connected
|
|
566
|
+
* peer to try getting a document from it. Remote collection check has to be performed.
|
|
567
|
+
*/
|
|
568
|
+
test('document is not shared with peer who did not request it', async () => {
|
|
569
|
+
const [spaceKey, anotherSpaceKey] = PublicKey.randomSequence();
|
|
570
|
+
|
|
571
|
+
const teleportBuilder = new TeleportBuilder();
|
|
572
|
+
afterTest(() => teleportBuilder.destroy());
|
|
573
|
+
|
|
574
|
+
const peerWithDocs = await createTeleportPeerWithStoredDocs(teleportBuilder, spaceKey, async (repo) => {
|
|
575
|
+
const document = repo.create();
|
|
576
|
+
document.change((doc: any) => (doc.text = 'hello'));
|
|
577
|
+
return [document];
|
|
578
|
+
});
|
|
579
|
+
const [document] = peerWithDocs.documents;
|
|
580
|
+
|
|
581
|
+
const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey, {
|
|
582
|
+
localDocuments: [],
|
|
583
|
+
remoteCollections: { [peerWithDocs.peerId]: [document.documentId] },
|
|
584
|
+
});
|
|
585
|
+
const peerFromAnotherSpace = await createTeleportTestPeer(teleportBuilder, anotherSpaceKey, {
|
|
586
|
+
localDocuments: [],
|
|
587
|
+
});
|
|
588
|
+
await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
|
|
589
|
+
await connectPeers(anotherSpaceKey, teleportBuilder, peer2, peerFromAnotherSpace);
|
|
590
|
+
|
|
591
|
+
const loadedDocument = peer2.repo.find(document.url);
|
|
592
|
+
await loadedDocument.whenReady();
|
|
593
|
+
|
|
594
|
+
await sleep(200);
|
|
595
|
+
const doc = peerFromAnotherSpace.repo.find(document.url);
|
|
596
|
+
await doc.whenReady(['unavailable']);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('collection-sync with chain of peers', async () => {
|
|
600
|
+
const [spaceKey] = PublicKey.randomSequence();
|
|
601
|
+
|
|
602
|
+
const teleportBuilder = new TeleportBuilder();
|
|
603
|
+
afterTest(() => teleportBuilder.destroy());
|
|
604
|
+
|
|
605
|
+
const peerWithDocs = await createTeleportPeerWithStoredDocs(teleportBuilder, spaceKey, async (repo) => {
|
|
606
|
+
const document = repo.create();
|
|
607
|
+
document.change((doc: any) => (doc.text = 'hello'));
|
|
608
|
+
return [document];
|
|
609
|
+
});
|
|
610
|
+
const [document] = peerWithDocs.documents;
|
|
611
|
+
|
|
612
|
+
const peer2Id = 'peer2';
|
|
613
|
+
const peer2 = await createTeleportTestPeer(teleportBuilder, spaceKey, {
|
|
614
|
+
peerId: peer2Id,
|
|
615
|
+
localDocuments: [],
|
|
616
|
+
remoteCollections: { [peerWithDocs.peerId]: [document.documentId] },
|
|
617
|
+
});
|
|
618
|
+
const peer3 = await createTeleportTestPeer(teleportBuilder, spaceKey, {
|
|
619
|
+
localDocuments: [],
|
|
620
|
+
remoteCollections: { [peer2Id]: [document.documentId] },
|
|
621
|
+
});
|
|
622
|
+
await connectPeers(spaceKey, teleportBuilder, peerWithDocs.peer, peer2);
|
|
623
|
+
await connectPeers(spaceKey, teleportBuilder, peer2, peer3);
|
|
624
|
+
|
|
625
|
+
const loadedDocument = peer2.repo.find(document.url);
|
|
626
|
+
await loadedDocument.whenReady();
|
|
627
|
+
|
|
628
|
+
const doc = peer3.repo.find(document.url);
|
|
629
|
+
await doc.whenReady();
|
|
630
|
+
expect(doc.docSync()).to.deep.eq(document.docSync());
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const createLevelAdapter = async () => {
|
|
635
|
+
const level = createTestLevel();
|
|
636
|
+
const storage = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
637
|
+
await openAndClose(level, storage);
|
|
638
|
+
return storage;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const createTeleportPeerWithStoredDocs = async (
|
|
642
|
+
teleportBuilder: TeleportBuilder,
|
|
643
|
+
spaceKey: PublicKey,
|
|
644
|
+
createDocCallback: (repo: Repo) => Promise<A.Doc<any>[]>,
|
|
645
|
+
) => {
|
|
646
|
+
const peerId = 'A';
|
|
647
|
+
const storage = await createLevelAdapter();
|
|
648
|
+
const peer = await createTeleportTestPeer(teleportBuilder, spaceKey, { storage, peerId });
|
|
649
|
+
const documents = await createDocCallback(peer.repo);
|
|
650
|
+
await peer.repo.flush();
|
|
651
|
+
// Reload the first peer so that documents are NOT pushed to the new connection.
|
|
652
|
+
const reloadedPeer = await createTeleportTestPeer(teleportBuilder, spaceKey, { storage, peerId });
|
|
653
|
+
return { peer: reloadedPeer, peerId, documents };
|
|
654
|
+
};
|
|
758
655
|
});
|
|
656
|
+
|
|
657
|
+
type ConnectedRepoOptions = {
|
|
658
|
+
storages?: StorageAdapterInterface[];
|
|
659
|
+
connectionStateProvider?: TestConnectionStateProvider;
|
|
660
|
+
sharePolicy?: SharePolicy;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const createRepoTopology = async <Peers extends string[], Peer extends string = Peers[number]>(args: {
|
|
664
|
+
peers: Peers;
|
|
665
|
+
connections: [Peer, Peer][];
|
|
666
|
+
options?: ConnectedRepoOptions;
|
|
667
|
+
}) => {
|
|
668
|
+
const adapters = args.connections.map(
|
|
669
|
+
() => TestAdapter.createPair(args.options?.connectionStateProvider) as [TestAdapter, TestAdapter],
|
|
670
|
+
);
|
|
671
|
+
const repos = args.peers.map((peerId, peerIndex) => {
|
|
672
|
+
const network = adapters
|
|
673
|
+
.map((pair, idx) => {
|
|
674
|
+
return args.connections[idx].includes(peerId as Peer)
|
|
675
|
+
? peerId === args.connections[idx][0]
|
|
676
|
+
? pair[0]
|
|
677
|
+
: pair[1]
|
|
678
|
+
: null;
|
|
679
|
+
})
|
|
680
|
+
.filter(nonNullable);
|
|
681
|
+
return new Repo({
|
|
682
|
+
peerId: peerId as PeerId,
|
|
683
|
+
storage: args.options?.storages?.[peerIndex],
|
|
684
|
+
network,
|
|
685
|
+
sharePolicy: args.options?.sharePolicy ?? (async () => true),
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
return { repos, adapters };
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const connectAdapters = async (pairs: [TestAdapter, TestAdapter][], options?: { noEmitPeerCandidate?: boolean }) => {
|
|
692
|
+
for (const pair of pairs) {
|
|
693
|
+
pair[0].ready();
|
|
694
|
+
pair[1].ready();
|
|
695
|
+
await pair[0].onConnect.wait();
|
|
696
|
+
await pair[1].onConnect.wait();
|
|
697
|
+
if (!options?.noEmitPeerCandidate) {
|
|
698
|
+
pair[0].peerCandidate(pair[1].peerId!);
|
|
699
|
+
pair[1].peerCandidate(pair[0].peerId!);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const createHostClientRepoTopology = (options?: ConnectedRepoOptions) =>
|
|
705
|
+
createRepoTopology({ peers: HOST_AND_CLIENT, connections: [HOST_AND_CLIENT], options });
|
|
706
|
+
|
|
707
|
+
const reconnectAdapters = async (pairs: [TestAdapter, TestAdapter][]) => {
|
|
708
|
+
for (const pair of pairs) {
|
|
709
|
+
// Note: Retry hack.
|
|
710
|
+
pair[0].peerDisconnected(pair[1].peerId!);
|
|
711
|
+
pair[1].peerDisconnected(pair[0].peerId!);
|
|
712
|
+
pair[0].peerCandidate(pair[1].peerId!);
|
|
713
|
+
pair[1].peerCandidate(pair[0].peerId!);
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const docChangeAfterAction = async (handle: DocHandle<any>, action: () => Promise<void>) => {
|
|
718
|
+
const receivedUpdate = new Promise((resolve) => handle.once('heads-changed', resolve));
|
|
719
|
+
await action();
|
|
720
|
+
return receivedUpdate;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const createTeleportTestPeer = async (
|
|
724
|
+
teleportBuilder: TeleportBuilder,
|
|
725
|
+
spaceKey: PublicKey,
|
|
726
|
+
options?: {
|
|
727
|
+
peerId?: string;
|
|
728
|
+
storage?: StorageAdapterInterface;
|
|
729
|
+
localDocuments?: string[];
|
|
730
|
+
remoteCollections?: { [peerId: string]: string[] };
|
|
731
|
+
},
|
|
732
|
+
): Promise<TeleportTestPeer> => {
|
|
733
|
+
const meshAdapter = new MeshEchoReplicator();
|
|
734
|
+
const echoAdapter = new EchoNetworkAdapter({
|
|
735
|
+
// If a document is in the remote collection we don't have it locally, so can't get spaceKey from it.
|
|
736
|
+
getContainingSpaceForDocument: async (documentId) => {
|
|
737
|
+
return options?.localDocuments ? (options.localDocuments.includes(documentId) ? spaceKey : null) : spaceKey;
|
|
738
|
+
},
|
|
739
|
+
isDocumentInRemoteCollection: async (params) => {
|
|
740
|
+
return options?.remoteCollections?.[params.peerId]?.includes(params.documentId) ?? false;
|
|
741
|
+
},
|
|
742
|
+
onCollectionStateQueried: () => {},
|
|
743
|
+
onCollectionStateReceived: () => {},
|
|
744
|
+
});
|
|
745
|
+
const repo = new Repo({
|
|
746
|
+
peerId: options?.peerId as PeerId,
|
|
747
|
+
network: [echoAdapter],
|
|
748
|
+
storage: options?.storage,
|
|
749
|
+
sharePolicy: async (peerId, documentId) =>
|
|
750
|
+
documentId ? echoAdapter.shouldAdvertise(peerId, { documentId }) : false,
|
|
751
|
+
});
|
|
752
|
+
await echoAdapter.open();
|
|
753
|
+
await echoAdapter.whenConnected();
|
|
754
|
+
await echoAdapter.addReplicator(meshAdapter);
|
|
755
|
+
const [teleport] = teleportBuilder.createPeers({ factory: () => new TeleportPeer() });
|
|
756
|
+
return { repo, meshAdapter, teleport };
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const connectPeers = async (
|
|
760
|
+
spaceKey: PublicKey,
|
|
761
|
+
builder: TeleportBuilder,
|
|
762
|
+
peer1: TeleportTestPeer,
|
|
763
|
+
peer2: TeleportTestPeer,
|
|
764
|
+
) => {
|
|
765
|
+
const [connection1, connection2] = await builder.connect(peer1.teleport, peer2.teleport);
|
|
766
|
+
await peer1.meshAdapter.authorizeDevice(spaceKey, peer2.teleport.peerId);
|
|
767
|
+
connection1.teleport.addExtension('automerge', peer1.meshAdapter.createExtension());
|
|
768
|
+
await peer2.meshAdapter.authorizeDevice(spaceKey, peer1.teleport.peerId);
|
|
769
|
+
connection2.teleport.addExtension('automerge', peer2.meshAdapter.createExtension());
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
type TeleportTestPeer = { repo: Repo; meshAdapter: MeshEchoReplicator; teleport: TeleportPeer };
|