@dxos/echo-pipeline 0.5.7 → 0.5.8-main.1e35f75
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-GANAND63.mjs → chunk-BE5QQHWH.mjs} +43 -15
- package/dist/lib/browser/{chunk-GANAND63.mjs.map → chunk-BE5QQHWH.mjs.map} +3 -3
- package/dist/lib/browser/index.mjs +13 -9
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +99 -4
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/{chunk-M475BGBI.cjs → chunk-ZELCNJ3D.cjs} +54 -25
- package/dist/lib/node/chunk-ZELCNJ3D.cjs.map +7 -0
- package/dist/lib/node/index.cjs +34 -30
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +107 -13
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/migrations.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +2 -2
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +2 -2
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +9 -1
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts +18 -0
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -0
- package/package.json +33 -33
- package/src/automerge/automerge-doc-loader.test.ts +2 -1
- package/src/automerge/automerge-doc-loader.ts +1 -1
- package/src/automerge/automerge-host.test.ts +1 -553
- package/src/automerge/automerge-host.ts +12 -5
- package/src/automerge/automerge-repo.test.ts +450 -2
- package/src/automerge/migrations.ts +2 -1
- package/src/automerge/storage-adapter.test.ts +81 -15
- package/src/space/space-manager.ts +6 -4
- package/src/space/space-protocol.test.ts +3 -3
- package/src/space/space-protocol.ts +3 -3
- package/src/space/space.ts +30 -1
- package/src/testing/index.ts +1 -0
- package/src/testing/test-agent-builder.ts +4 -4
- package/src/testing/test-network-adapter.ts +62 -0
- package/dist/lib/node/chunk-M475BGBI.cjs.map +0 -7
|
@@ -2,32 +2,13 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { randomBytes } from 'crypto';
|
|
6
5
|
import expect from 'expect';
|
|
7
|
-
import waitForExpect from 'wait-for-expect';
|
|
8
6
|
|
|
9
|
-
import { Trigger, asyncTimeout, sleep } from '@dxos/async';
|
|
10
|
-
import {
|
|
11
|
-
type Message,
|
|
12
|
-
NetworkAdapter,
|
|
13
|
-
type PeerId,
|
|
14
|
-
Repo,
|
|
15
|
-
type HandleState,
|
|
16
|
-
type DocumentId,
|
|
17
|
-
} from '@dxos/automerge/automerge-repo';
|
|
18
7
|
import { IndexMetadataStore } from '@dxos/indexing';
|
|
19
|
-
import { invariant } from '@dxos/invariant';
|
|
20
|
-
import { PublicKey } from '@dxos/keys';
|
|
21
8
|
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
22
|
-
import {
|
|
23
|
-
import { TestBuilder as TeleportBuilder, TestPeer as TeleportPeer } from '@dxos/teleport/testing';
|
|
24
|
-
import { afterTest, describe, openAndClose, test } from '@dxos/test';
|
|
25
|
-
import { arrayToBuffer, bufferToArray } from '@dxos/util';
|
|
9
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
26
10
|
|
|
27
11
|
import { AutomergeHost } from './automerge-host';
|
|
28
|
-
import { EchoNetworkAdapter } from './echo-network-adapter';
|
|
29
|
-
import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
|
|
30
|
-
import { MeshEchoReplicator } from './mesh-echo-replicator';
|
|
31
12
|
|
|
32
13
|
describe('AutomergeHost', () => {
|
|
33
14
|
test('can create documents', async () => {
|
|
@@ -79,537 +60,4 @@ describe('AutomergeHost', () => {
|
|
|
79
60
|
expect(handle2.docSync().text).toEqual('Hello world');
|
|
80
61
|
await host2.repo.flush();
|
|
81
62
|
});
|
|
82
|
-
|
|
83
|
-
test('basic networking', async () => {
|
|
84
|
-
const hostAdapter: TestAdapter = new TestAdapter({
|
|
85
|
-
send: (message: Message) => clientAdapter.receive(message),
|
|
86
|
-
});
|
|
87
|
-
const clientAdapter: TestAdapter = new TestAdapter({
|
|
88
|
-
send: (message: Message) => hostAdapter.receive(message),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const host = new Repo({
|
|
92
|
-
network: [hostAdapter],
|
|
93
|
-
});
|
|
94
|
-
const client = new Repo({
|
|
95
|
-
network: [clientAdapter],
|
|
96
|
-
});
|
|
97
|
-
hostAdapter.ready();
|
|
98
|
-
clientAdapter.ready();
|
|
99
|
-
await hostAdapter.onConnect.wait();
|
|
100
|
-
await clientAdapter.onConnect.wait();
|
|
101
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
102
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
103
|
-
|
|
104
|
-
const handle = host.create();
|
|
105
|
-
const text = 'Hello world';
|
|
106
|
-
handle.change((doc: any) => {
|
|
107
|
-
doc.text = text;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const docOnClient = client.find(handle.url);
|
|
111
|
-
expect((await asyncTimeout(docOnClient.doc(), 1000)).text).toEqual(text);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('share policy gets enabled afterwards', async () => {
|
|
115
|
-
const [hostAdapter, clientAdapter] = TestAdapter.createPair();
|
|
116
|
-
let sharePolicy = false;
|
|
117
|
-
|
|
118
|
-
const host = new Repo({
|
|
119
|
-
network: [hostAdapter],
|
|
120
|
-
peerId: 'host' as PeerId,
|
|
121
|
-
sharePolicy: async () => sharePolicy,
|
|
122
|
-
});
|
|
123
|
-
const client = new Repo({
|
|
124
|
-
network: [clientAdapter],
|
|
125
|
-
peerId: 'client' as PeerId,
|
|
126
|
-
sharePolicy: async () => sharePolicy,
|
|
127
|
-
});
|
|
128
|
-
hostAdapter.ready();
|
|
129
|
-
clientAdapter.ready();
|
|
130
|
-
await hostAdapter.onConnect.wait();
|
|
131
|
-
await clientAdapter.onConnect.wait();
|
|
132
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
133
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
134
|
-
|
|
135
|
-
const handle = host.create();
|
|
136
|
-
const text = 'Hello world';
|
|
137
|
-
handle.change((doc: any) => {
|
|
138
|
-
doc.text = text;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
const docOnClient = client.find(handle.url);
|
|
143
|
-
await asyncTimeout(docOnClient.whenReady(['unavailable']), 1000);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
sharePolicy = true;
|
|
147
|
-
|
|
148
|
-
{
|
|
149
|
-
const docOnClient = client.find(handle.url);
|
|
150
|
-
// TODO(mykola): We expect the document to be available here, but it's not.
|
|
151
|
-
await asyncTimeout(docOnClient.whenReady(['unavailable']), 1000);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('two documents and share policy switching', async () => {
|
|
156
|
-
const [hostAdapter, clientAdapter] = TestAdapter.createPair();
|
|
157
|
-
const allowedDocs: DocumentId[] = [];
|
|
158
|
-
|
|
159
|
-
const host: Repo = new Repo({
|
|
160
|
-
network: [hostAdapter],
|
|
161
|
-
peerId: 'host' as PeerId,
|
|
162
|
-
sharePolicy: async (_, docId) => (docId ? allowedDocs.includes(docId) && !!host.handles[docId] : false),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const client: Repo = new Repo({
|
|
166
|
-
network: [clientAdapter],
|
|
167
|
-
peerId: 'client' as PeerId,
|
|
168
|
-
sharePolicy: async (_, docId) => (docId ? allowedDocs.includes(docId) && !!client.handles[docId] : false),
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const firstHandle = host.create();
|
|
172
|
-
firstHandle.change((doc: any) => (doc.text = 'Hello world'));
|
|
173
|
-
await host.find(firstHandle.url).whenReady();
|
|
174
|
-
allowedDocs.push(firstHandle.documentId);
|
|
175
|
-
|
|
176
|
-
{
|
|
177
|
-
// Initiate connection.
|
|
178
|
-
hostAdapter.ready();
|
|
179
|
-
clientAdapter.ready();
|
|
180
|
-
await hostAdapter.onConnect.wait();
|
|
181
|
-
await clientAdapter.onConnect.wait();
|
|
182
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
183
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
const firstDocOnClient = client.find(firstHandle.url);
|
|
188
|
-
await asyncTimeout(firstDocOnClient.whenReady(), 1000);
|
|
189
|
-
expect(firstDocOnClient.docSync().text).toEqual('Hello world');
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const secondHandle = host.create();
|
|
193
|
-
secondHandle.change((doc: any) => (doc.text = 'Hello world'));
|
|
194
|
-
await host.find(secondHandle.url).whenReady();
|
|
195
|
-
allowedDocs.push(secondHandle.documentId);
|
|
196
|
-
|
|
197
|
-
{
|
|
198
|
-
const secondDocOnClient = client.find(secondHandle.url);
|
|
199
|
-
await asyncTimeout(secondDocOnClient.whenReady(), 1000);
|
|
200
|
-
expect(secondDocOnClient.docSync().text).toEqual('Hello world');
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('recovering from a lost connection', async () => {
|
|
205
|
-
let connectionState: 'on' | 'off' = 'on';
|
|
206
|
-
|
|
207
|
-
const hostAdapter: TestAdapter = new TestAdapter({
|
|
208
|
-
send: (message: Message) => connectionState === 'on' && sleep(10).then(() => clientAdapter.receive(message)),
|
|
209
|
-
});
|
|
210
|
-
const clientAdapter: TestAdapter = new TestAdapter({
|
|
211
|
-
send: (message: Message) => connectionState === 'on' && sleep(10).then(() => hostAdapter.receive(message)),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const host = new Repo({
|
|
215
|
-
network: [hostAdapter],
|
|
216
|
-
});
|
|
217
|
-
const client = new Repo({
|
|
218
|
-
network: [clientAdapter],
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Establish connection.
|
|
222
|
-
hostAdapter.ready();
|
|
223
|
-
clientAdapter.ready();
|
|
224
|
-
await hostAdapter.onConnect.wait();
|
|
225
|
-
await clientAdapter.onConnect.wait();
|
|
226
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
227
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
228
|
-
|
|
229
|
-
const handle = host.create();
|
|
230
|
-
const docOnClient = client.find(handle.url);
|
|
231
|
-
{
|
|
232
|
-
const sanityText = 'Hello world';
|
|
233
|
-
handle.change((doc: any) => {
|
|
234
|
-
doc.sanityText = sanityText;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
expect((await asyncTimeout(docOnClient.doc(), 1000)).sanityText).toEqual(sanityText);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Disrupt connection.
|
|
241
|
-
const offlineText = 'This has been written while the connection was off';
|
|
242
|
-
{
|
|
243
|
-
connectionState = 'off';
|
|
244
|
-
|
|
245
|
-
handle.change((doc: any) => {
|
|
246
|
-
doc.offlineText = offlineText;
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await sleep(100);
|
|
250
|
-
expect((await asyncTimeout(docOnClient.doc(), 1000)).offlineText).toBeUndefined();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Re-establish connection.
|
|
254
|
-
const onlineText = 'This has been written after the connection was re-established';
|
|
255
|
-
{
|
|
256
|
-
connectionState = 'on';
|
|
257
|
-
hostAdapter.peerDisconnected(clientAdapter.peerId!);
|
|
258
|
-
clientAdapter.peerDisconnected(hostAdapter.peerId!);
|
|
259
|
-
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
260
|
-
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
261
|
-
|
|
262
|
-
handle.change((doc: any) => {
|
|
263
|
-
doc.onlineText = onlineText;
|
|
264
|
-
});
|
|
265
|
-
await sleep(100);
|
|
266
|
-
expect((await asyncTimeout(docOnClient.doc(), 1000)).onlineText).toEqual(onlineText);
|
|
267
|
-
expect((await asyncTimeout(docOnClient.doc(), 1000)).offlineText).toEqual(offlineText);
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test('integration test with teleport', async () => {
|
|
272
|
-
const [spaceKey] = PublicKey.randomSequence();
|
|
273
|
-
|
|
274
|
-
const createAutomergeRepo = async () => {
|
|
275
|
-
const meshAdapter = new MeshEchoReplicator();
|
|
276
|
-
const echoAdapter = new EchoNetworkAdapter({
|
|
277
|
-
getContainingSpaceForDocument: async () => spaceKey,
|
|
278
|
-
});
|
|
279
|
-
const repo = new Repo({
|
|
280
|
-
network: [echoAdapter],
|
|
281
|
-
});
|
|
282
|
-
await echoAdapter.open();
|
|
283
|
-
await echoAdapter.whenConnected();
|
|
284
|
-
await echoAdapter.addReplicator(meshAdapter);
|
|
285
|
-
return { repo, meshAdapter };
|
|
286
|
-
};
|
|
287
|
-
const peer1 = await createAutomergeRepo();
|
|
288
|
-
const peer2 = await createAutomergeRepo();
|
|
289
|
-
|
|
290
|
-
const handle = peer1.repo.create();
|
|
291
|
-
|
|
292
|
-
const teleportBuilder = new TeleportBuilder();
|
|
293
|
-
afterTest(() => teleportBuilder.destroy());
|
|
294
|
-
|
|
295
|
-
const [teleportPeer1, teleportPeer2] = teleportBuilder.createPeers({ factory: () => new TeleportPeer() });
|
|
296
|
-
{
|
|
297
|
-
// Initiate connection.
|
|
298
|
-
const [connection1, connection2] = await teleportBuilder.connect(teleportPeer1, teleportPeer2);
|
|
299
|
-
connection1.teleport.addExtension('automerge', peer1.meshAdapter.createExtension());
|
|
300
|
-
connection2.teleport.addExtension('automerge', peer2.meshAdapter.createExtension());
|
|
301
|
-
|
|
302
|
-
// Test connection.
|
|
303
|
-
const text = 'Hello world';
|
|
304
|
-
handle.change((doc: any) => {
|
|
305
|
-
doc.text = text;
|
|
306
|
-
});
|
|
307
|
-
await waitForExpect(async () => {
|
|
308
|
-
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
309
|
-
const doc = await asyncTimeout(docOnPeer2.doc(), 1000);
|
|
310
|
-
expect(doc.text).toEqual(text);
|
|
311
|
-
}, 1000);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const offlineText = 'This has been written while the connection was off';
|
|
315
|
-
{
|
|
316
|
-
// Disconnect peers.
|
|
317
|
-
await teleportBuilder.disconnect(teleportPeer1, teleportPeer2);
|
|
318
|
-
|
|
319
|
-
// Make offline changes.
|
|
320
|
-
const offlineText = 'This has been written while the connection was off';
|
|
321
|
-
handle.change((doc: any) => {
|
|
322
|
-
doc.offlineText = offlineText;
|
|
323
|
-
});
|
|
324
|
-
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
325
|
-
await sleep(100);
|
|
326
|
-
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).toBeUndefined();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
{
|
|
330
|
-
// Reconnect peers.
|
|
331
|
-
const [connection1, connection2] = await teleportBuilder.connect(teleportPeer1, teleportPeer2);
|
|
332
|
-
connection1.teleport.addExtension('automerge', peer1.meshAdapter.createExtension());
|
|
333
|
-
connection2.teleport.addExtension('automerge', peer2.meshAdapter.createExtension());
|
|
334
|
-
|
|
335
|
-
// Wait for offline changes to be synced.
|
|
336
|
-
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
337
|
-
await waitForExpect(
|
|
338
|
-
async () => expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).toEqual(offlineText),
|
|
339
|
-
1000,
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
// Test connection.
|
|
343
|
-
const onlineText = 'This has been written after the connection was re-established';
|
|
344
|
-
handle.change((doc: any) => {
|
|
345
|
-
doc.onlineText = onlineText;
|
|
346
|
-
});
|
|
347
|
-
await waitForExpect(
|
|
348
|
-
async () => expect((await asyncTimeout(docOnPeer2.doc(), 1000)).onlineText).toEqual(onlineText),
|
|
349
|
-
1000,
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
describe('storage', () => {
|
|
355
|
-
test('loadRange', async () => {
|
|
356
|
-
const root = `/tmp/${randomBytes(16).toString('hex')}`;
|
|
357
|
-
{
|
|
358
|
-
const level = createTestLevel(root);
|
|
359
|
-
const adapter = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
360
|
-
await level.open();
|
|
361
|
-
await adapter.open();
|
|
362
|
-
|
|
363
|
-
await adapter.save(['test', '1'], bufferToArray(Buffer.from('one')));
|
|
364
|
-
await adapter.save(['test', '2'], bufferToArray(Buffer.from('two')));
|
|
365
|
-
await adapter.save(['bar', '1'], bufferToArray(Buffer.from('bar')));
|
|
366
|
-
await adapter.close();
|
|
367
|
-
await level.close();
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
{
|
|
371
|
-
const level = createTestLevel(root);
|
|
372
|
-
const adapter = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
373
|
-
await openAndClose(level, adapter);
|
|
374
|
-
|
|
375
|
-
const range = await adapter.loadRange(['test']);
|
|
376
|
-
expect(range.map((chunk) => arrayToBuffer(chunk.data!).toString())).toEqual(['one', 'two']);
|
|
377
|
-
expect(range.map((chunk) => chunk.key)).toEqual([
|
|
378
|
-
['test', '1'],
|
|
379
|
-
['test', '2'],
|
|
380
|
-
]);
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test('removeRange', async () => {
|
|
385
|
-
const root = `/tmp/${randomBytes(16).toString('hex')}`;
|
|
386
|
-
{
|
|
387
|
-
const level = createTestLevel(root);
|
|
388
|
-
const adapter = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
389
|
-
await level.open();
|
|
390
|
-
await adapter.open();
|
|
391
|
-
await adapter.save(['test', '1'], bufferToArray(Buffer.from('one')));
|
|
392
|
-
await adapter.save(['test', '2'], bufferToArray(Buffer.from('two')));
|
|
393
|
-
await adapter.save(['bar', '1'], bufferToArray(Buffer.from('bar')));
|
|
394
|
-
await adapter.close();
|
|
395
|
-
await level.close();
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
{
|
|
399
|
-
const level = createTestLevel(root);
|
|
400
|
-
const adapter = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
|
|
401
|
-
await openAndClose(level, adapter);
|
|
402
|
-
await adapter.removeRange(['test']);
|
|
403
|
-
const range = await adapter.loadRange(['test']);
|
|
404
|
-
expect(range.map((chunk) => arrayToBuffer(chunk.data!).toString())).toEqual([]);
|
|
405
|
-
const range2 = await adapter.loadRange(['bar']);
|
|
406
|
-
expect(range2.map((chunk) => arrayToBuffer(chunk.data!).toString())).toEqual(['bar']);
|
|
407
|
-
expect(range2.map((chunk) => chunk.key)).toEqual([['bar', '1']]);
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test('replication though a 4 peer chain', async () => {
|
|
413
|
-
const pairAB = TestAdapter.createPair();
|
|
414
|
-
const pairBC = TestAdapter.createPair();
|
|
415
|
-
const pairCD = TestAdapter.createPair();
|
|
416
|
-
|
|
417
|
-
const repoA = new Repo({
|
|
418
|
-
peerId: 'A' as any,
|
|
419
|
-
network: [pairAB[0]],
|
|
420
|
-
sharePolicy: async () => true,
|
|
421
|
-
});
|
|
422
|
-
const _repoB = new Repo({
|
|
423
|
-
peerId: 'B' as any,
|
|
424
|
-
network: [pairAB[1], pairBC[0]],
|
|
425
|
-
sharePolicy: async () => true,
|
|
426
|
-
});
|
|
427
|
-
const _repoC = new Repo({
|
|
428
|
-
peerId: 'C' as any,
|
|
429
|
-
network: [pairBC[1], pairCD[0]],
|
|
430
|
-
sharePolicy: async () => true,
|
|
431
|
-
});
|
|
432
|
-
const repoD = new Repo({
|
|
433
|
-
peerId: 'D' as any,
|
|
434
|
-
network: [pairCD[1]],
|
|
435
|
-
sharePolicy: async () => true,
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
for (const pair of [pairAB, pairBC, pairCD]) {
|
|
439
|
-
pair[0].ready();
|
|
440
|
-
pair[1].ready();
|
|
441
|
-
await pair[0].onConnect.wait();
|
|
442
|
-
await pair[1].onConnect.wait();
|
|
443
|
-
pair[0].peerCandidate(pair[1].peerId!);
|
|
444
|
-
pair[1].peerCandidate(pair[0].peerId!);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const docA = repoA.create();
|
|
448
|
-
// NOTE: Doesn't work if the doc is empty.
|
|
449
|
-
docA.change((doc: any) => {
|
|
450
|
-
doc.text = 'Hello world';
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// If we wait here for replication to finish naturally, the test will pass.
|
|
454
|
-
|
|
455
|
-
const docD = repoD.find(docA.url);
|
|
456
|
-
|
|
457
|
-
await docD.whenReady();
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
test('replication though a 3 peer chain', async () => {
|
|
461
|
-
const pairAB = TestAdapter.createPair();
|
|
462
|
-
const pairBC = TestAdapter.createPair();
|
|
463
|
-
|
|
464
|
-
const repoA = new Repo({
|
|
465
|
-
peerId: 'A' as any,
|
|
466
|
-
network: [pairAB[0]],
|
|
467
|
-
sharePolicy: async () => true,
|
|
468
|
-
});
|
|
469
|
-
const repoB = new Repo({
|
|
470
|
-
peerId: 'B' as any,
|
|
471
|
-
network: [pairAB[1], pairBC[0]],
|
|
472
|
-
sharePolicy: async () => true,
|
|
473
|
-
});
|
|
474
|
-
const repoC = new Repo({
|
|
475
|
-
peerId: 'C' as any,
|
|
476
|
-
network: [pairBC[1]],
|
|
477
|
-
sharePolicy: async () => true,
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
for (const pair of [pairAB, pairBC]) {
|
|
481
|
-
pair[0].ready();
|
|
482
|
-
pair[1].ready();
|
|
483
|
-
await pair[0].onConnect.wait();
|
|
484
|
-
await pair[1].onConnect.wait();
|
|
485
|
-
pair[0].peerCandidate(pair[1].peerId!);
|
|
486
|
-
pair[1].peerCandidate(pair[0].peerId!);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const docA = repoA.create();
|
|
490
|
-
// NOTE: Doesn't work if the doc is empty.
|
|
491
|
-
docA.change((doc: any) => {
|
|
492
|
-
doc.text = 'Hello world';
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
const _docB = repoB.find(docA.url);
|
|
496
|
-
const docC = repoC.find(docA.url);
|
|
497
|
-
|
|
498
|
-
await docC.whenReady();
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
test('replicate document after request', async () => {
|
|
502
|
-
const [adapter1, adapter2] = TestAdapter.createPair();
|
|
503
|
-
const repoA = new Repo({
|
|
504
|
-
peerId: 'A' as any,
|
|
505
|
-
network: [adapter1],
|
|
506
|
-
sharePolicy: async () => true,
|
|
507
|
-
});
|
|
508
|
-
const repoB = new Repo({
|
|
509
|
-
peerId: 'B' as any,
|
|
510
|
-
network: [adapter2],
|
|
511
|
-
sharePolicy: async () => true,
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
const unavailable: HandleState = 'unavailable';
|
|
515
|
-
|
|
516
|
-
{
|
|
517
|
-
// Connect repos.
|
|
518
|
-
adapter1.ready();
|
|
519
|
-
adapter2.ready();
|
|
520
|
-
await adapter1.onConnect.wait();
|
|
521
|
-
await adapter2.onConnect.wait();
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const docA = repoA.create();
|
|
525
|
-
// NOTE: Doesn't work if the doc is empty.
|
|
526
|
-
docA.change((doc: any) => {
|
|
527
|
-
doc.text = 'Hello world';
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
const docB = repoB.find(docA.url);
|
|
531
|
-
{
|
|
532
|
-
// Request document from repoB.
|
|
533
|
-
await asyncTimeout(docB.whenReady([unavailable]), 1_000);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
{
|
|
537
|
-
// Failing to find a document.
|
|
538
|
-
// await (docB.whenReady([unavailable]), 1_000);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
{
|
|
542
|
-
adapter1.peerCandidate(adapter2.peerId!);
|
|
543
|
-
await sleep(100);
|
|
544
|
-
adapter2.peerCandidate(adapter1.peerId!);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
{
|
|
548
|
-
await asyncTimeout(docB.whenReady([unavailable]), 1_000);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
{
|
|
552
|
-
// Note: Retry hack.
|
|
553
|
-
adapter1.peerDisconnected(adapter2.peerId!);
|
|
554
|
-
adapter2.peerDisconnected(adapter1.peerId!);
|
|
555
|
-
adapter1.peerCandidate(adapter2.peerId!);
|
|
556
|
-
adapter2.peerCandidate(adapter1.peerId!);
|
|
557
|
-
|
|
558
|
-
await asyncTimeout(docB.whenReady(), 1_000);
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
63
|
});
|
|
562
|
-
|
|
563
|
-
class TestAdapter extends NetworkAdapter {
|
|
564
|
-
static createPair() {
|
|
565
|
-
const adapter1: TestAdapter = new TestAdapter({
|
|
566
|
-
send: (message: Message) => sleep(10).then(() => adapter2.receive(message)),
|
|
567
|
-
});
|
|
568
|
-
const adapter2: TestAdapter = new TestAdapter({
|
|
569
|
-
send: (message: Message) => sleep(10).then(() => adapter1.receive(message)),
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
return [adapter1, adapter2];
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
public onConnect = new Trigger();
|
|
576
|
-
|
|
577
|
-
constructor(private readonly _params: { send: (message: Message) => void }) {
|
|
578
|
-
super();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
|
|
582
|
-
// because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
|
|
583
|
-
ready() {
|
|
584
|
-
this.emit('ready', { network: this });
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
override connect(peerId: PeerId) {
|
|
588
|
-
this.peerId = peerId;
|
|
589
|
-
this.onConnect.wake();
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
peerCandidate(peerId: PeerId) {
|
|
593
|
-
invariant(peerId, 'PeerId is required');
|
|
594
|
-
this.emit('peer-candidate', { peerId, peerMetadata: {} });
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
peerDisconnected(peerId: PeerId) {
|
|
598
|
-
invariant(peerId, 'PeerId is required');
|
|
599
|
-
this.emit('peer-disconnected', { peerId });
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
override send(message: Message) {
|
|
603
|
-
log('send', { from: message.senderId, to: message.targetId, type: message.type });
|
|
604
|
-
this._params.send(message);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
override disconnect() {
|
|
608
|
-
this.peerId = undefined;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
receive(message: Message) {
|
|
612
|
-
invariant(this.peerId, 'Peer id is not set');
|
|
613
|
-
this.emit('message', message);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
@@ -76,6 +76,9 @@ export class AutomergeHost {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
async open() {
|
|
79
|
+
// TODO(burdon): Should this be stable?
|
|
80
|
+
this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
|
|
81
|
+
|
|
79
82
|
// TODO(mykola): remove this before 0.6 release.
|
|
80
83
|
this._directory && (await levelMigration({ db: this._db, directory: this._directory }));
|
|
81
84
|
this._storage = new LevelDBStorageAdapter({
|
|
@@ -86,20 +89,24 @@ export class AutomergeHost {
|
|
|
86
89
|
},
|
|
87
90
|
});
|
|
88
91
|
await this._storage.open?.();
|
|
89
|
-
this._peerId = `host-${PublicKey.random().toHex()}` as PeerId;
|
|
90
92
|
|
|
91
93
|
this._clientNetwork = new LocalHostNetworkAdapter();
|
|
92
94
|
|
|
95
|
+
// Construct the automerge repo.
|
|
93
96
|
this._repo = new Repo({
|
|
94
97
|
peerId: this._peerId as PeerId,
|
|
95
|
-
network: [this._clientNetwork, this._echoNetworkAdapter],
|
|
96
|
-
storage: this._storage,
|
|
97
|
-
|
|
98
98
|
sharePolicy: this._sharePolicy.bind(this),
|
|
99
|
+
storage: this._storage,
|
|
100
|
+
network: [
|
|
101
|
+
// Downstream client.
|
|
102
|
+
this._clientNetwork,
|
|
103
|
+
// Upstream swarm.
|
|
104
|
+
this._echoNetworkAdapter,
|
|
105
|
+
],
|
|
99
106
|
});
|
|
107
|
+
|
|
100
108
|
this._clientNetwork.ready();
|
|
101
109
|
await this._echoNetworkAdapter.open();
|
|
102
|
-
|
|
103
110
|
await this._clientNetwork.whenConnected();
|
|
104
111
|
await this._echoNetworkAdapter.whenConnected();
|
|
105
112
|
}
|