@dxos/echo-pipeline 0.3.9-main.eb5df3b → 0.3.9-main.ee839a8
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-T627FAUD.mjs → chunk-6XOL2DVG.mjs} +142 -28
- package/dist/lib/browser/chunk-6XOL2DVG.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 +1 -1
- package/dist/lib/node/{chunk-XWUUSV4Y.cjs → chunk-2RIO52RK.cjs} +145 -31
- package/dist/lib/node/chunk-2RIO52RK.cjs.map +7 -0
- package/dist/lib/node/index.cjs +26 -26
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +15 -15
- package/dist/types/src/automerge/automerge-host.d.ts +5 -1
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service-host.d.ts +2 -1
- package/dist/types/src/db-host/data-service-host.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts +2 -1
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +3 -0
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/package.json +33 -32
- package/src/automerge/automerge-host.test.ts +81 -1
- package/src/automerge/automerge-host.ts +102 -13
- package/src/db-host/data-service-host.ts +5 -0
- package/src/db-host/data-service.ts +9 -2
- package/src/space/space-manager.ts +3 -0
- package/dist/lib/browser/chunk-T627FAUD.mjs.map +0 -7
- package/dist/lib/node/chunk-XWUUSV4Y.cjs.map +0 -7
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import expect from 'expect';
|
|
6
6
|
|
|
7
|
-
import { sleep } from '@dxos/async';
|
|
7
|
+
import { Trigger, asyncTimeout, sleep } from '@dxos/async';
|
|
8
|
+
import { type Message, NetworkAdapter, type PeerId, Repo } from '@dxos/automerge/automerge-repo';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
8
11
|
import { StorageType, createStorage } from '@dxos/random-access-storage';
|
|
9
12
|
import { describe, test } from '@dxos/test';
|
|
10
13
|
|
|
@@ -39,4 +42,81 @@ describe('AutomergeHost', () => {
|
|
|
39
42
|
await handle2.whenReady();
|
|
40
43
|
expect(handle2.docSync().text).toEqual('Hello world');
|
|
41
44
|
});
|
|
45
|
+
|
|
46
|
+
test('basic networking', async () => {
|
|
47
|
+
type Context = { client: Trigger<TestAdapter>; host: Trigger<TestAdapter> };
|
|
48
|
+
const context: Context = {
|
|
49
|
+
client: new Trigger<TestAdapter>(),
|
|
50
|
+
host: new Trigger<TestAdapter>(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
class TestAdapter extends NetworkAdapter {
|
|
54
|
+
constructor(public readonly context: Context, public readonly role: 'host' | 'client') {
|
|
55
|
+
super();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
|
|
59
|
+
// because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
|
|
60
|
+
ready() {
|
|
61
|
+
this.emit('ready', { network: this });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override connect(peerId: PeerId) {
|
|
65
|
+
this.peerId = peerId;
|
|
66
|
+
context[this.role].wake(this);
|
|
67
|
+
context[this.role === 'host' ? 'client' : 'host']
|
|
68
|
+
.wait()
|
|
69
|
+
.then((adapter) => {
|
|
70
|
+
invariant(adapter.peerId, 'Peer id is not set');
|
|
71
|
+
this.emit('peer-candidate', { peerId: adapter.peerId });
|
|
72
|
+
})
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
log.catch(error);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override send(message: Message) {
|
|
79
|
+
context[this.role === 'host' ? 'client' : 'host']
|
|
80
|
+
.wait()
|
|
81
|
+
.then((adapter) => {
|
|
82
|
+
adapter.receive(message);
|
|
83
|
+
})
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
log.catch(error);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override disconnect() {
|
|
90
|
+
this.peerId = undefined;
|
|
91
|
+
this.context[this.role].reset();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
receive(message: Message) {
|
|
95
|
+
invariant(this.peerId, 'Peer id is not set');
|
|
96
|
+
this.emit('message', message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hostAdapter = new TestAdapter(context, 'host');
|
|
101
|
+
const clientAdapter = new TestAdapter(context, 'client');
|
|
102
|
+
|
|
103
|
+
const host = new Repo({
|
|
104
|
+
network: [hostAdapter],
|
|
105
|
+
});
|
|
106
|
+
const client = new Repo({
|
|
107
|
+
network: [clientAdapter],
|
|
108
|
+
});
|
|
109
|
+
hostAdapter.ready();
|
|
110
|
+
clientAdapter.ready();
|
|
111
|
+
|
|
112
|
+
const handle = host.create();
|
|
113
|
+
const text = 'Hello world';
|
|
114
|
+
handle.change((doc: any) => {
|
|
115
|
+
doc.text = text;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const docOnClient = client.find(handle.url);
|
|
119
|
+
await asyncTimeout(docOnClient.whenReady(), 3_000);
|
|
120
|
+
expect(docOnClient.docSync().text).toEqual(text);
|
|
121
|
+
});
|
|
42
122
|
});
|
|
@@ -14,8 +14,11 @@ import {
|
|
|
14
14
|
} from '@dxos/automerge/automerge-repo';
|
|
15
15
|
import { Stream } from '@dxos/codec-protobuf';
|
|
16
16
|
import { invariant } from '@dxos/invariant';
|
|
17
|
-
import {
|
|
17
|
+
import { log } from '@dxos/log';
|
|
18
|
+
import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
|
|
19
|
+
import { type PeerInfo } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|
|
18
20
|
import { type Directory } from '@dxos/random-access-storage';
|
|
21
|
+
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
19
22
|
import { arrayToBuffer, bufferToArray } from '@dxos/util';
|
|
20
23
|
|
|
21
24
|
export class AutomergeHost {
|
|
@@ -29,22 +32,28 @@ export class AutomergeHost {
|
|
|
29
32
|
this._clientNetwork = new LocalHostNetworkAdapter();
|
|
30
33
|
this._storage = new AutomergeStorageAdapter(storageDirectory);
|
|
31
34
|
this._repo = new Repo({
|
|
32
|
-
network: [
|
|
33
|
-
// this._meshNetwork,
|
|
34
|
-
this._clientNetwork,
|
|
35
|
-
],
|
|
36
|
-
|
|
35
|
+
network: [this._clientNetwork, this._meshNetwork],
|
|
37
36
|
storage: this._storage,
|
|
38
37
|
|
|
39
38
|
// TODO(dmaretskyi): Share based on HALO permissions and space affinity.
|
|
40
39
|
sharePolicy: async (peerId, documentId) => true, // Share everything.
|
|
41
40
|
});
|
|
41
|
+
this._clientNetwork.ready();
|
|
42
|
+
this._meshNetwork.ready();
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
get repo(): Repo {
|
|
45
46
|
return this._repo;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
async close() {
|
|
50
|
+
await this._clientNetwork.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//
|
|
54
|
+
// Methods for client-services.
|
|
55
|
+
//
|
|
56
|
+
|
|
48
57
|
syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
|
|
49
58
|
return this._clientNetwork.syncRepo(request);
|
|
50
59
|
}
|
|
@@ -52,6 +61,18 @@ export class AutomergeHost {
|
|
|
52
61
|
sendSyncMessage(request: SyncRepoRequest): Promise<void> {
|
|
53
62
|
return this._clientNetwork.sendSyncMessage(request);
|
|
54
63
|
}
|
|
64
|
+
|
|
65
|
+
getHostInfo(): HostInfo {
|
|
66
|
+
return this._clientNetwork.getHostInfo();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//
|
|
70
|
+
// Mesh replication.
|
|
71
|
+
//
|
|
72
|
+
|
|
73
|
+
createExtension(): AutomergeReplicator {
|
|
74
|
+
return this._meshNetwork.createExtension();
|
|
75
|
+
}
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
type ClientSyncState = {
|
|
@@ -66,15 +87,19 @@ type ClientSyncState = {
|
|
|
66
87
|
class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
67
88
|
private readonly _peers: Map<PeerId, ClientSyncState> = new Map();
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
92
|
+
*/
|
|
93
|
+
ready() {
|
|
94
|
+
// NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
|
|
95
|
+
// because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
|
|
72
96
|
this.emit('ready', {
|
|
73
97
|
network: this,
|
|
74
98
|
});
|
|
75
99
|
}
|
|
76
100
|
|
|
77
101
|
override connect(peerId: PeerId): void {
|
|
102
|
+
this.peerId = peerId;
|
|
78
103
|
// No-op. Client always connects first
|
|
79
104
|
}
|
|
80
105
|
|
|
@@ -84,8 +109,14 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
84
109
|
peer.send(message);
|
|
85
110
|
}
|
|
86
111
|
|
|
112
|
+
async close() {
|
|
113
|
+
this._peers.forEach((peer) => peer.disconnect());
|
|
114
|
+
this.emit('close');
|
|
115
|
+
}
|
|
116
|
+
|
|
87
117
|
override disconnect(): void {
|
|
88
|
-
|
|
118
|
+
// TODO(mykola): `disconnect` is not used anywhere in `Repo` from `@automerge/automerge-repo`. Should we remove it?
|
|
119
|
+
// No-op
|
|
89
120
|
}
|
|
90
121
|
|
|
91
122
|
syncRepo({ id, syncMessage }: SyncRepoRequest): Stream<SyncRepoResponse> {
|
|
@@ -120,6 +151,13 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
120
151
|
this.emit('message', message);
|
|
121
152
|
}
|
|
122
153
|
|
|
154
|
+
getHostInfo(): HostInfo {
|
|
155
|
+
invariant(this.peerId, 'Peer id not set.');
|
|
156
|
+
return {
|
|
157
|
+
peerId: this.peerId,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
123
161
|
private _getPeerId(id: string): PeerId {
|
|
124
162
|
return id as PeerId;
|
|
125
163
|
}
|
|
@@ -129,16 +167,64 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
129
167
|
* Used to replicate with other peers over the network.
|
|
130
168
|
*/
|
|
131
169
|
class MeshNetworkAdapter extends NetworkAdapter {
|
|
170
|
+
private readonly _extensions: Map<string, AutomergeReplicator> = new Map();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
174
|
+
*/
|
|
175
|
+
ready() {
|
|
176
|
+
// NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
|
|
177
|
+
// because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
|
|
178
|
+
this.emit('ready', {
|
|
179
|
+
network: this,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
132
183
|
override connect(peerId: PeerId): void {
|
|
133
|
-
|
|
184
|
+
this.peerId = peerId;
|
|
134
185
|
}
|
|
135
186
|
|
|
136
187
|
override send(message: Message): void {
|
|
137
|
-
|
|
188
|
+
const receiverId = message.targetId;
|
|
189
|
+
const extension = this._extensions.get(receiverId);
|
|
190
|
+
invariant(extension, 'Extension not found.');
|
|
191
|
+
extension.sendSyncMessage({ payload: cbor.encode(message) }).catch((err) => log.catch(err));
|
|
138
192
|
}
|
|
139
193
|
|
|
140
194
|
override disconnect(): void {
|
|
141
|
-
|
|
195
|
+
// No-op
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
createExtension(): AutomergeReplicator {
|
|
199
|
+
invariant(this.peerId, 'Peer id not set.');
|
|
200
|
+
|
|
201
|
+
let peerInfo: PeerInfo;
|
|
202
|
+
const extension = new AutomergeReplicator(
|
|
203
|
+
{
|
|
204
|
+
peerId: this.peerId,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
onStartReplication: async (info) => {
|
|
208
|
+
peerInfo = info;
|
|
209
|
+
// TODO(mykola): Fix race condition?
|
|
210
|
+
this._extensions.set(info.id, extension);
|
|
211
|
+
this.emit('peer-candidate', {
|
|
212
|
+
peerId: info.id as PeerId,
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
onSyncMessage: async ({ payload }) => {
|
|
216
|
+
const message = cbor.decode(payload) as Message;
|
|
217
|
+
this.emit('message', message);
|
|
218
|
+
},
|
|
219
|
+
onClose: async () => {
|
|
220
|
+
peerInfo &&
|
|
221
|
+
this.emit('peer-disconnected', {
|
|
222
|
+
peerId: peerInfo.id as PeerId,
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
return extension;
|
|
142
228
|
}
|
|
143
229
|
}
|
|
144
230
|
|
|
@@ -151,6 +237,9 @@ class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
151
237
|
const filename = this._getFilename(key);
|
|
152
238
|
const file = this._directory.getOrCreateFile(filename);
|
|
153
239
|
const { size } = await file.stat();
|
|
240
|
+
if (!size || size === 0) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
154
243
|
const buffer = await file.read(0, size);
|
|
155
244
|
return bufferToArray(buffer);
|
|
156
245
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type MutationReceipt,
|
|
19
19
|
type WriteRequest,
|
|
20
20
|
type SyncRepoResponse,
|
|
21
|
+
type HostInfo,
|
|
21
22
|
} from '@dxos/protocols/proto/dxos/echo/service';
|
|
22
23
|
import { ComplexMap } from '@dxos/util';
|
|
23
24
|
|
|
@@ -205,6 +206,10 @@ export class DataServiceHost {
|
|
|
205
206
|
await this._flush();
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
getHostInfo(): Promise<HostInfo> {
|
|
210
|
+
throw new Error('Method not implemented.');
|
|
211
|
+
}
|
|
212
|
+
|
|
208
213
|
syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
|
|
209
214
|
throw new Error('Method not implemented.');
|
|
210
215
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type FlushRequest,
|
|
17
17
|
type SyncRepoRequest,
|
|
18
18
|
type SyncRepoResponse,
|
|
19
|
+
type HostInfo,
|
|
19
20
|
} from '@dxos/protocols/proto/dxos/echo/service';
|
|
20
21
|
import { ComplexMap } from '@dxos/util';
|
|
21
22
|
|
|
@@ -81,11 +82,17 @@ export class DataServiceImpl implements DataService {
|
|
|
81
82
|
return host.flush();
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// Automerge specific.
|
|
86
|
+
|
|
87
|
+
async getHostInfo(request: void): Promise<HostInfo> {
|
|
88
|
+
return this._automergeHost.getHostInfo();
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse> {
|
|
85
|
-
|
|
92
|
+
return this._automergeHost.syncRepo(request);
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
sendSyncMessage(request: SyncRepoRequest): Promise<void> {
|
|
89
|
-
|
|
96
|
+
return this._automergeHost.sendSyncMessage(request);
|
|
90
97
|
}
|
|
91
98
|
}
|
|
@@ -39,6 +39,9 @@ export type ConstructSpaceParams = {
|
|
|
39
39
|
metadata: SpaceMetadata;
|
|
40
40
|
swarmIdentity: SwarmIdentity;
|
|
41
41
|
memberKey: PublicKey;
|
|
42
|
+
/**
|
|
43
|
+
* Called when connection auth passed successful.
|
|
44
|
+
*/
|
|
42
45
|
onNetworkConnection: (session: Teleport) => void;
|
|
43
46
|
onAuthFailure?: (session: Teleport) => void;
|
|
44
47
|
};
|