@dxos/echo-pipeline 0.3.11-main.d56f337 → 0.3.11-main.d8b8a39
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-44D2XTQP.mjs → chunk-MPBRK5OV.mjs} +10 -7
- package/dist/lib/browser/{chunk-44D2XTQP.mjs.map → chunk-MPBRK5OV.mjs.map} +3 -3
- package/dist/lib/browser/index.mjs +1 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node/{chunk-PZONK23P.cjs → chunk-GJQNRSA3.cjs} +12 -10
- package/dist/lib/node/{chunk-PZONK23P.cjs.map → chunk-GJQNRSA3.cjs.map} +2 -2
- package/dist/lib/node/index.cjs +26 -28
- package/dist/lib/node/index.cjs.map +2 -2
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +16 -16
- package/dist/types/src/automerge/automerge-host.d.ts +15 -1
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +1 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-host.test.ts +184 -36
- package/src/automerge/automerge-host.ts +11 -8
- package/src/automerge/index.ts +1 -1
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import { randomBytes } from 'crypto';
|
|
6
6
|
import expect from 'expect';
|
|
7
|
+
import waitForExpect from 'wait-for-expect';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { asyncTimeout, sleep } from '@dxos/async';
|
|
9
10
|
import { type Message, NetworkAdapter, type PeerId, Repo } from '@dxos/automerge/automerge-repo';
|
|
10
11
|
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { log } from '@dxos/log';
|
|
12
12
|
import { StorageType, createStorage } from '@dxos/random-access-storage';
|
|
13
|
-
import {
|
|
13
|
+
import { TestBuilder as TeleportBuilder, TestPeer as TeleportPeer } from '@dxos/teleport/testing';
|
|
14
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
14
15
|
import { arrayToBuffer, bufferToArray } from '@dxos/util';
|
|
15
16
|
|
|
16
|
-
import { AutomergeHost, AutomergeStorageAdapter } from './automerge-host';
|
|
17
|
+
import { AutomergeHost, AutomergeStorageAdapter, MeshNetworkAdapter } from './automerge-host';
|
|
17
18
|
|
|
18
19
|
describe('AutomergeHost', () => {
|
|
19
20
|
test('can create documents', () => {
|
|
@@ -46,13 +47,12 @@ describe('AutomergeHost', () => {
|
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
test('basic networking', async () => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const clientAdapter = new TestAdapter(context, 'client');
|
|
50
|
+
const hostAdapter: TestAdapter = new TestAdapter({
|
|
51
|
+
send: (message: Message) => clientAdapter.receive(message),
|
|
52
|
+
});
|
|
53
|
+
const clientAdapter: TestAdapter = new TestAdapter({
|
|
54
|
+
send: (message: Message) => hostAdapter.receive(message),
|
|
55
|
+
});
|
|
56
56
|
|
|
57
57
|
const host = new Repo({
|
|
58
58
|
network: [hostAdapter],
|
|
@@ -62,6 +62,8 @@ describe('AutomergeHost', () => {
|
|
|
62
62
|
});
|
|
63
63
|
hostAdapter.ready();
|
|
64
64
|
clientAdapter.ready();
|
|
65
|
+
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
66
|
+
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
65
67
|
|
|
66
68
|
const handle = host.create();
|
|
67
69
|
const text = 'Hello world';
|
|
@@ -70,11 +72,145 @@ describe('AutomergeHost', () => {
|
|
|
70
72
|
});
|
|
71
73
|
|
|
72
74
|
const docOnClient = client.find(handle.url);
|
|
73
|
-
await asyncTimeout(docOnClient.
|
|
74
|
-
|
|
75
|
+
expect((await asyncTimeout(docOnClient.doc(), 1000)).text).toEqual(text);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('recovering from a lost connection', async () => {
|
|
79
|
+
let connectionState: 'on' | 'off' = 'on';
|
|
80
|
+
|
|
81
|
+
const hostAdapter: TestAdapter = new TestAdapter({
|
|
82
|
+
send: (message: Message) => connectionState === 'on' && sleep(10).then(() => clientAdapter.receive(message)),
|
|
83
|
+
});
|
|
84
|
+
const clientAdapter: TestAdapter = new TestAdapter({
|
|
85
|
+
send: (message: Message) => connectionState === 'on' && sleep(10).then(() => hostAdapter.receive(message)),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const host = new Repo({
|
|
89
|
+
network: [hostAdapter],
|
|
90
|
+
});
|
|
91
|
+
const client = new Repo({
|
|
92
|
+
network: [clientAdapter],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Establish connection.
|
|
96
|
+
hostAdapter.ready();
|
|
97
|
+
clientAdapter.ready();
|
|
98
|
+
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
99
|
+
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
100
|
+
|
|
101
|
+
const handle = host.create();
|
|
102
|
+
const docOnClient = client.find(handle.url);
|
|
103
|
+
{
|
|
104
|
+
const sanityText = 'Hello world';
|
|
105
|
+
handle.change((doc: any) => {
|
|
106
|
+
doc.sanityText = sanityText;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect((await asyncTimeout(docOnClient.doc(), 1000)).sanityText).toEqual(sanityText);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Disrupt connection.
|
|
113
|
+
const offlineText = 'This has been written while the connection was off';
|
|
114
|
+
{
|
|
115
|
+
connectionState = 'off';
|
|
116
|
+
|
|
117
|
+
handle.change((doc: any) => {
|
|
118
|
+
doc.offlineText = offlineText;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await sleep(100);
|
|
122
|
+
expect((await asyncTimeout(docOnClient.doc(), 1000)).offlineText).toBeUndefined();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Re-establish connection.
|
|
126
|
+
const onlineText = 'This has been written after the connection was re-established';
|
|
127
|
+
{
|
|
128
|
+
connectionState = 'on';
|
|
129
|
+
hostAdapter.peerDisconnected(clientAdapter.peerId!);
|
|
130
|
+
clientAdapter.peerDisconnected(hostAdapter.peerId!);
|
|
131
|
+
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
132
|
+
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
133
|
+
|
|
134
|
+
handle.change((doc: any) => {
|
|
135
|
+
doc.onlineText = onlineText;
|
|
136
|
+
});
|
|
137
|
+
await sleep(100);
|
|
138
|
+
expect((await asyncTimeout(docOnClient.doc(), 1000)).onlineText).toEqual(onlineText);
|
|
139
|
+
expect((await asyncTimeout(docOnClient.doc(), 1000)).offlineText).toEqual(offlineText);
|
|
140
|
+
}
|
|
75
141
|
});
|
|
76
142
|
|
|
77
|
-
test('
|
|
143
|
+
test('integration test with teleport', async () => {
|
|
144
|
+
const createAutomergeRepo = () => {
|
|
145
|
+
const meshAdapter = new MeshNetworkAdapter();
|
|
146
|
+
const repo = new Repo({
|
|
147
|
+
network: [meshAdapter],
|
|
148
|
+
});
|
|
149
|
+
meshAdapter.ready();
|
|
150
|
+
return { repo, meshAdapter };
|
|
151
|
+
};
|
|
152
|
+
const peer1 = createAutomergeRepo();
|
|
153
|
+
const peer2 = createAutomergeRepo();
|
|
154
|
+
const handle = peer1.repo.create();
|
|
155
|
+
|
|
156
|
+
const teleportBuilder = new TeleportBuilder();
|
|
157
|
+
afterTest(() => teleportBuilder.destroy());
|
|
158
|
+
|
|
159
|
+
const [teleportPeer1, teleportPeer2] = teleportBuilder.createPeers({ factory: () => new TeleportPeer() });
|
|
160
|
+
{
|
|
161
|
+
// Initiate connection.
|
|
162
|
+
const [connection1, connection2] = await teleportBuilder.connect(teleportPeer1, teleportPeer2);
|
|
163
|
+
connection1.teleport.addExtension('automerge', peer1.meshAdapter.createExtension());
|
|
164
|
+
connection2.teleport.addExtension('automerge', peer2.meshAdapter.createExtension());
|
|
165
|
+
|
|
166
|
+
// Test connection.
|
|
167
|
+
const text = 'Hello world';
|
|
168
|
+
handle.change((doc: any) => {
|
|
169
|
+
doc.text = text;
|
|
170
|
+
});
|
|
171
|
+
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
172
|
+
await waitForExpect(async () => expect((await asyncTimeout(docOnPeer2.doc(), 1000)).text).toEqual(text), 1000);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const offlineText = 'This has been written while the connection was off';
|
|
176
|
+
{
|
|
177
|
+
// Disconnect peers.
|
|
178
|
+
await teleportBuilder.disconnect(teleportPeer1, teleportPeer2);
|
|
179
|
+
|
|
180
|
+
// Make offline changes.
|
|
181
|
+
const offlineText = 'This has been written while the connection was off';
|
|
182
|
+
handle.change((doc: any) => {
|
|
183
|
+
doc.offlineText = offlineText;
|
|
184
|
+
});
|
|
185
|
+
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
186
|
+
await sleep(100);
|
|
187
|
+
expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).toBeUndefined();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
// Reconnect peers.
|
|
192
|
+
const [connection1, connection2] = await teleportBuilder.connect(teleportPeer1, teleportPeer2);
|
|
193
|
+
connection1.teleport.addExtension('automerge', peer1.meshAdapter.createExtension());
|
|
194
|
+
connection2.teleport.addExtension('automerge', peer2.meshAdapter.createExtension());
|
|
195
|
+
|
|
196
|
+
// Wait for offline changes to be synced.
|
|
197
|
+
const docOnPeer2 = peer2.repo.find(handle.url);
|
|
198
|
+
await waitForExpect(
|
|
199
|
+
async () => expect((await asyncTimeout(docOnPeer2.doc(), 1000)).offlineText).toEqual(offlineText),
|
|
200
|
+
1000,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Test connection.
|
|
204
|
+
const onlineText = 'This has been written after the connection was re-established';
|
|
205
|
+
handle.change((doc: any) => {
|
|
206
|
+
doc.onlineText = onlineText;
|
|
207
|
+
});
|
|
208
|
+
await waitForExpect(
|
|
209
|
+
async () => expect((await asyncTimeout(docOnPeer2.doc(), 1000)).onlineText).toEqual(onlineText),
|
|
210
|
+
1000,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
78
214
|
|
|
79
215
|
describe('storage', () => {
|
|
80
216
|
test('load range on node', async () => {
|
|
@@ -100,13 +236,33 @@ describe('AutomergeHost', () => {
|
|
|
100
236
|
]);
|
|
101
237
|
}
|
|
102
238
|
});
|
|
239
|
+
|
|
240
|
+
test('removeRange on node', async () => {
|
|
241
|
+
const root = `/tmp/${randomBytes(16).toString('hex')}`;
|
|
242
|
+
{
|
|
243
|
+
const storage = createStorage({ type: StorageType.NODE, root });
|
|
244
|
+
const adapter = new AutomergeStorageAdapter(storage.createDirectory());
|
|
245
|
+
await adapter.save(['test', '1'], bufferToArray(Buffer.from('one')));
|
|
246
|
+
await adapter.save(['test', '2'], bufferToArray(Buffer.from('two')));
|
|
247
|
+
await adapter.save(['bar', '1'], bufferToArray(Buffer.from('bar')));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
{
|
|
251
|
+
const storage = createStorage({ type: StorageType.NODE, root });
|
|
252
|
+
const adapter = new AutomergeStorageAdapter(storage.createDirectory());
|
|
253
|
+
await adapter.removeRange(['test']);
|
|
254
|
+
const range = await adapter.loadRange(['test']);
|
|
255
|
+
expect(range.map((chunk) => arrayToBuffer(chunk.data!).toString())).toEqual([]);
|
|
256
|
+
const range2 = await adapter.loadRange(['bar']);
|
|
257
|
+
expect(range2.map((chunk) => arrayToBuffer(chunk.data!).toString())).toEqual(['bar']);
|
|
258
|
+
expect(range2.map((chunk) => chunk.key)).toEqual([['bar', '1']]);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
103
261
|
});
|
|
104
262
|
});
|
|
105
263
|
|
|
106
|
-
type Context = { client: Trigger<TestAdapter>; host: Trigger<TestAdapter> };
|
|
107
|
-
|
|
108
264
|
class TestAdapter extends NetworkAdapter {
|
|
109
|
-
constructor(
|
|
265
|
+
constructor(private readonly _params: { send: (message: Message) => void }) {
|
|
110
266
|
super();
|
|
111
267
|
}
|
|
112
268
|
|
|
@@ -118,32 +274,24 @@ class TestAdapter extends NetworkAdapter {
|
|
|
118
274
|
|
|
119
275
|
override connect(peerId: PeerId) {
|
|
120
276
|
this.peerId = peerId;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
peerCandidate(peerId: PeerId) {
|
|
280
|
+
invariant(peerId, 'PeerId is required');
|
|
281
|
+
this.emit('peer-candidate', { peerId });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
peerDisconnected(peerId: PeerId) {
|
|
285
|
+
invariant(peerId, 'PeerId is required');
|
|
286
|
+
this.emit('peer-disconnected', { peerId });
|
|
131
287
|
}
|
|
132
288
|
|
|
133
289
|
override send(message: Message) {
|
|
134
|
-
this.
|
|
135
|
-
.wait()
|
|
136
|
-
.then((adapter) => {
|
|
137
|
-
adapter.receive(message);
|
|
138
|
-
})
|
|
139
|
-
.catch((error) => {
|
|
140
|
-
log.catch(error);
|
|
141
|
-
});
|
|
290
|
+
this._params.send(message);
|
|
142
291
|
}
|
|
143
292
|
|
|
144
293
|
override disconnect() {
|
|
145
294
|
this.peerId = undefined;
|
|
146
|
-
this.context[this.role].reset();
|
|
147
295
|
}
|
|
148
296
|
|
|
149
297
|
receive(message: Message) {
|
|
@@ -172,7 +172,7 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
172
172
|
/**
|
|
173
173
|
* Used to replicate with other peers over the network.
|
|
174
174
|
*/
|
|
175
|
-
class MeshNetworkAdapter extends NetworkAdapter {
|
|
175
|
+
export class MeshNetworkAdapter extends NetworkAdapter {
|
|
176
176
|
private readonly _extensions: Map<string, AutomergeReplicator> = new Map();
|
|
177
177
|
|
|
178
178
|
/**
|
|
@@ -233,10 +233,13 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
233
233
|
this.emit('message', message);
|
|
234
234
|
},
|
|
235
235
|
onClose: async () => {
|
|
236
|
-
peerInfo
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
if (!peerInfo) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.emit('peer-disconnected', {
|
|
240
|
+
peerId: peerInfo.id as PeerId,
|
|
241
|
+
});
|
|
242
|
+
this._extensions.delete(peerInfo.id);
|
|
240
243
|
},
|
|
241
244
|
},
|
|
242
245
|
);
|
|
@@ -273,7 +276,7 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
273
276
|
// TODO(dmaretskyi): Better deletion.
|
|
274
277
|
const filename = this._getFilename(key);
|
|
275
278
|
const file = this._directory.getOrCreateFile(filename);
|
|
276
|
-
await file.
|
|
279
|
+
await file.destroy();
|
|
277
280
|
}
|
|
278
281
|
|
|
279
282
|
override async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
@@ -301,8 +304,8 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
301
304
|
entries
|
|
302
305
|
.filter((entry) => entry.startsWith(filename))
|
|
303
306
|
.map(async (entry): Promise<void> => {
|
|
304
|
-
const file = this._directory.getOrCreateFile(
|
|
305
|
-
await file.
|
|
307
|
+
const file = this._directory.getOrCreateFile(entry);
|
|
308
|
+
await file.destroy();
|
|
306
309
|
}),
|
|
307
310
|
);
|
|
308
311
|
}
|
package/src/automerge/index.ts
CHANGED