@dxos/echo-pipeline 0.4.4-main.fcf0b00 → 0.4.4-next.75bfacc
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-26G7ZQMP.mjs → chunk-WIB35LJH.mjs} +305 -254
- package/dist/lib/browser/{chunk-26G7ZQMP.mjs.map → chunk-WIB35LJH.mjs.map} +4 -4
- package/dist/lib/browser/index.mjs +5 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node/{chunk-V62AY27P.cjs → chunk-37RERU2L.cjs} +295 -248
- package/dist/lib/node/{chunk-V62AY27P.cjs.map → chunk-37RERU2L.cjs.map} +4 -4
- package/dist/lib/node/index.cjs +31 -27
- 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 +5 -31
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-storage-adapter.d.ts +16 -0
- package/dist/types/src/automerge/automerge-storage-adapter.d.ts.map +1 -0
- package/dist/types/src/automerge/index.d.ts +4 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/local-host-network-adapter.d.ts +23 -0
- package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -0
- package/dist/types/src/automerge/mesh-network-adapter.d.ts +18 -0
- package/dist/types/src/automerge/mesh-network-adapter.d.ts.map +1 -0
- package/package.json +34 -34
- package/src/automerge/automerge-host.test.ts +102 -2
- package/src/automerge/automerge-host.ts +27 -303
- package/src/automerge/automerge-storage-adapter.ts +105 -0
- package/src/automerge/index.ts +4 -1
- package/src/automerge/local-host-network-adapter.ts +109 -0
- package/src/automerge/mesh-network-adapter.ts +107 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NetworkAdapter, type Message, type PeerId } from '@dxos/automerge/automerge-repo';
|
|
2
|
+
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
3
|
+
/**
|
|
4
|
+
* Used to replicate with other peers over the network.
|
|
5
|
+
*/
|
|
6
|
+
export declare class MeshNetworkAdapter extends NetworkAdapter {
|
|
7
|
+
private readonly _extensions;
|
|
8
|
+
private _connected;
|
|
9
|
+
/**
|
|
10
|
+
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
11
|
+
*/
|
|
12
|
+
ready(): void;
|
|
13
|
+
connect(peerId: PeerId): void;
|
|
14
|
+
send(message: Message): void;
|
|
15
|
+
disconnect(): void;
|
|
16
|
+
createExtension(): AutomergeReplicator;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=mesh-network-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh-network-adapter.d.ts","sourceRoot":"","sources":["../../../../src/automerge/mesh-network-adapter.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,gCAAgC,CAAC;AAIjG,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AAEpF;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,cAAc;IACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+C;IAC3E,OAAO,CAAC,UAAU,CAAiB;IAEnC;;OAEG;IACH,KAAK;IAQI,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK7B,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAO5B,UAAU,IAAI,IAAI;IAI3B,eAAe,IAAI,mBAAmB;CA6DvC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo-pipeline",
|
|
3
|
-
"version": "0.4.4-
|
|
3
|
+
"version": "0.4.4-next.75bfacc",
|
|
4
4
|
"description": "ECHO database.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -36,41 +36,41 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"crc-32": "^1.2.2",
|
|
39
|
-
"@dxos/async": "0.4.4-
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/echo-db": "0.4.4-
|
|
48
|
-
"@dxos/feed-store": "0.4.4-
|
|
49
|
-
"@dxos/hypercore": "0.4.4-
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/keys": "0.4.4-
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/teleport": "0.4.4-
|
|
62
|
-
"@dxos/teleport
|
|
63
|
-
"@dxos/teleport-extension-
|
|
64
|
-
"@dxos/teleport-extension-
|
|
65
|
-
"@dxos/text-model": "0.4.4-
|
|
66
|
-
"@dxos/
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/tracing": "0.4.4-
|
|
69
|
-
"@dxos/typings": "0.4.4-
|
|
70
|
-
"@dxos/util": "0.4.4-
|
|
39
|
+
"@dxos/async": "0.4.4-next.75bfacc",
|
|
40
|
+
"@dxos/automerge": "0.4.4-next.75bfacc",
|
|
41
|
+
"@dxos/codec-protobuf": "0.4.4-next.75bfacc",
|
|
42
|
+
"@dxos/context": "0.4.4-next.75bfacc",
|
|
43
|
+
"@dxos/credentials": "0.4.4-next.75bfacc",
|
|
44
|
+
"@dxos/crypto": "0.4.4-next.75bfacc",
|
|
45
|
+
"@dxos/debug": "0.4.4-next.75bfacc",
|
|
46
|
+
"@dxos/document-model": "0.4.4-next.75bfacc",
|
|
47
|
+
"@dxos/echo-db": "0.4.4-next.75bfacc",
|
|
48
|
+
"@dxos/feed-store": "0.4.4-next.75bfacc",
|
|
49
|
+
"@dxos/hypercore": "0.4.4-next.75bfacc",
|
|
50
|
+
"@dxos/invariant": "0.4.4-next.75bfacc",
|
|
51
|
+
"@dxos/keyring": "0.4.4-next.75bfacc",
|
|
52
|
+
"@dxos/keys": "0.4.4-next.75bfacc",
|
|
53
|
+
"@dxos/log": "0.4.4-next.75bfacc",
|
|
54
|
+
"@dxos/messaging": "0.4.4-next.75bfacc",
|
|
55
|
+
"@dxos/network-manager": "0.4.4-next.75bfacc",
|
|
56
|
+
"@dxos/node-std": "0.4.4-next.75bfacc",
|
|
57
|
+
"@dxos/model-factory": "0.4.4-next.75bfacc",
|
|
58
|
+
"@dxos/protocols": "0.4.4-next.75bfacc",
|
|
59
|
+
"@dxos/rpc": "0.4.4-next.75bfacc",
|
|
60
|
+
"@dxos/random-access-storage": "0.4.4-next.75bfacc",
|
|
61
|
+
"@dxos/teleport-extension-automerge-replicator": "0.4.4-next.75bfacc",
|
|
62
|
+
"@dxos/teleport": "0.4.4-next.75bfacc",
|
|
63
|
+
"@dxos/teleport-extension-object-sync": "0.4.4-next.75bfacc",
|
|
64
|
+
"@dxos/teleport-extension-gossip": "0.4.4-next.75bfacc",
|
|
65
|
+
"@dxos/text-model": "0.4.4-next.75bfacc",
|
|
66
|
+
"@dxos/timeframe": "0.4.4-next.75bfacc",
|
|
67
|
+
"@dxos/teleport-extension-replicator": "0.4.4-next.75bfacc",
|
|
68
|
+
"@dxos/tracing": "0.4.4-next.75bfacc",
|
|
69
|
+
"@dxos/typings": "0.4.4-next.75bfacc",
|
|
70
|
+
"@dxos/util": "0.4.4-next.75bfacc"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"fast-check": "
|
|
73
|
+
"fast-check": "^3.15.1",
|
|
74
74
|
"hypercore-protocol": "^8.0.7",
|
|
75
75
|
"source-map-support": "^0.5.12",
|
|
76
76
|
"wait-for-expect": "^3.0.2"
|
|
@@ -7,7 +7,14 @@ import expect from 'expect';
|
|
|
7
7
|
import waitForExpect from 'wait-for-expect';
|
|
8
8
|
|
|
9
9
|
import { Trigger, asyncTimeout, sleep } from '@dxos/async';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Message,
|
|
12
|
+
NetworkAdapter,
|
|
13
|
+
type PeerId,
|
|
14
|
+
Repo,
|
|
15
|
+
type HandleState,
|
|
16
|
+
type DocumentId,
|
|
17
|
+
} from '@dxos/automerge/automerge-repo';
|
|
11
18
|
import { invariant } from '@dxos/invariant';
|
|
12
19
|
import { log } from '@dxos/log';
|
|
13
20
|
import { StorageType, createStorage } from '@dxos/random-access-storage';
|
|
@@ -15,7 +22,9 @@ import { TestBuilder as TeleportBuilder, TestPeer as TeleportPeer } from '@dxos/
|
|
|
15
22
|
import { afterTest, describe, test } from '@dxos/test';
|
|
16
23
|
import { arrayToBuffer, bufferToArray } from '@dxos/util';
|
|
17
24
|
|
|
18
|
-
import { AutomergeHost
|
|
25
|
+
import { AutomergeHost } from './automerge-host';
|
|
26
|
+
import { AutomergeStorageAdapter } from './automerge-storage-adapter';
|
|
27
|
+
import { MeshNetworkAdapter } from './mesh-network-adapter';
|
|
19
28
|
|
|
20
29
|
describe('AutomergeHost', () => {
|
|
21
30
|
test('can create documents', () => {
|
|
@@ -78,6 +87,97 @@ describe('AutomergeHost', () => {
|
|
|
78
87
|
expect((await asyncTimeout(docOnClient.doc(), 1000)).text).toEqual(text);
|
|
79
88
|
});
|
|
80
89
|
|
|
90
|
+
test('share policy gets enabled afterwards', async () => {
|
|
91
|
+
const [hostAdapter, clientAdapter] = TestAdapter.createPair();
|
|
92
|
+
let sharePolicy = false;
|
|
93
|
+
|
|
94
|
+
const host = new Repo({
|
|
95
|
+
network: [hostAdapter],
|
|
96
|
+
peerId: 'host' as PeerId,
|
|
97
|
+
sharePolicy: async () => sharePolicy,
|
|
98
|
+
});
|
|
99
|
+
const client = new Repo({
|
|
100
|
+
network: [clientAdapter],
|
|
101
|
+
peerId: 'client' as PeerId,
|
|
102
|
+
sharePolicy: async () => sharePolicy,
|
|
103
|
+
});
|
|
104
|
+
hostAdapter.ready();
|
|
105
|
+
clientAdapter.ready();
|
|
106
|
+
await hostAdapter.onConnect.wait();
|
|
107
|
+
await clientAdapter.onConnect.wait();
|
|
108
|
+
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
109
|
+
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
110
|
+
|
|
111
|
+
const handle = host.create();
|
|
112
|
+
const text = 'Hello world';
|
|
113
|
+
handle.change((doc: any) => {
|
|
114
|
+
doc.text = text;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
const docOnClient = client.find(handle.url);
|
|
119
|
+
await asyncTimeout(docOnClient.whenReady(['unavailable']), 1000);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
sharePolicy = true;
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
const docOnClient = client.find(handle.url);
|
|
126
|
+
// TODO(mykola): We expect the document to be available here, but it's not.
|
|
127
|
+
await asyncTimeout(docOnClient.whenReady(['unavailable']), 1000);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('two documents and share policy switching', async () => {
|
|
132
|
+
const [hostAdapter, clientAdapter] = TestAdapter.createPair();
|
|
133
|
+
const allowedDocs: DocumentId[] = [];
|
|
134
|
+
|
|
135
|
+
const host: Repo = new Repo({
|
|
136
|
+
network: [hostAdapter],
|
|
137
|
+
peerId: 'host' as PeerId,
|
|
138
|
+
sharePolicy: async (_, docId) => (docId ? allowedDocs.includes(docId) && !!host.handles[docId] : false),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const client: Repo = new Repo({
|
|
142
|
+
network: [clientAdapter],
|
|
143
|
+
peerId: 'client' as PeerId,
|
|
144
|
+
sharePolicy: async (_, docId) => (docId ? allowedDocs.includes(docId) && !!client.handles[docId] : false),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const firstHandle = host.create();
|
|
148
|
+
firstHandle.change((doc: any) => (doc.text = 'Hello world'));
|
|
149
|
+
await host.find(firstHandle.url).whenReady();
|
|
150
|
+
allowedDocs.push(firstHandle.documentId);
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
// Initiate connection.
|
|
154
|
+
hostAdapter.ready();
|
|
155
|
+
clientAdapter.ready();
|
|
156
|
+
await hostAdapter.onConnect.wait();
|
|
157
|
+
await clientAdapter.onConnect.wait();
|
|
158
|
+
hostAdapter.peerCandidate(clientAdapter.peerId!);
|
|
159
|
+
clientAdapter.peerCandidate(hostAdapter.peerId!);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
const firstDocOnClient = client.find(firstHandle.url);
|
|
164
|
+
await asyncTimeout(firstDocOnClient.whenReady(), 1000);
|
|
165
|
+
expect(firstDocOnClient.docSync().text).toEqual('Hello world');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const secondHandle = host.create();
|
|
169
|
+
secondHandle.change((doc: any) => (doc.text = 'Hello world'));
|
|
170
|
+
await host.find(secondHandle.url).whenReady();
|
|
171
|
+
// await sleep(100);
|
|
172
|
+
allowedDocs.push(secondHandle.documentId);
|
|
173
|
+
|
|
174
|
+
{
|
|
175
|
+
const secondDocOnClient = client.find(secondHandle.url);
|
|
176
|
+
await asyncTimeout(secondDocOnClient.whenReady(), 1000);
|
|
177
|
+
expect(secondDocOnClient.docSync().text).toEqual('Hello world');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
81
181
|
test('recovering from a lost connection', async () => {
|
|
82
182
|
let connectionState: 'on' | 'off' = 'on';
|
|
83
183
|
|
|
@@ -2,29 +2,23 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Trigger } from '@dxos/async';
|
|
6
5
|
import { next as automerge } from '@dxos/automerge/automerge';
|
|
7
|
-
import {
|
|
8
|
-
Repo,
|
|
9
|
-
NetworkAdapter,
|
|
10
|
-
StorageAdapter,
|
|
11
|
-
type Message,
|
|
12
|
-
type PeerId,
|
|
13
|
-
type Chunk,
|
|
14
|
-
type StorageKey,
|
|
15
|
-
cbor,
|
|
16
|
-
} from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { Repo, type StorageAdapter, type PeerId, type DocumentId } from '@dxos/automerge/automerge-repo';
|
|
17
7
|
import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
|
|
18
|
-
import { Stream } from '@dxos/codec-protobuf';
|
|
19
|
-
import { invariant } from '@dxos/invariant';
|
|
8
|
+
import { type Stream } from '@dxos/codec-protobuf';
|
|
20
9
|
import { PublicKey } from '@dxos/keys';
|
|
21
10
|
import { log } from '@dxos/log';
|
|
22
11
|
import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
|
|
23
|
-
import { type PeerInfo } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|
|
24
12
|
import { StorageType, type Directory } from '@dxos/random-access-storage';
|
|
25
|
-
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
13
|
+
import { type AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
26
14
|
import { trace } from '@dxos/tracing';
|
|
27
|
-
import { ComplexMap, ComplexSet,
|
|
15
|
+
import { ComplexMap, ComplexSet, defaultMap, mapValues } from '@dxos/util';
|
|
16
|
+
|
|
17
|
+
import { AutomergeStorageAdapter } from './automerge-storage-adapter';
|
|
18
|
+
import { LocalHostNetworkAdapter } from './local-host-network-adapter';
|
|
19
|
+
import { MeshNetworkAdapter } from './mesh-network-adapter';
|
|
20
|
+
|
|
21
|
+
export type { DocumentId };
|
|
28
22
|
|
|
29
23
|
@trace.resource()
|
|
30
24
|
export class AutomergeHost {
|
|
@@ -38,6 +32,8 @@ export class AutomergeHost {
|
|
|
38
32
|
*/
|
|
39
33
|
private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
|
|
40
34
|
|
|
35
|
+
public _requestedDocs = new Set<string>();
|
|
36
|
+
|
|
41
37
|
constructor(storageDirectory: Directory) {
|
|
42
38
|
this._meshNetwork = new MeshNetworkAdapter();
|
|
43
39
|
this._clientNetwork = new LocalHostNetworkAdapter();
|
|
@@ -47,8 +43,9 @@ export class AutomergeHost {
|
|
|
47
43
|
storageDirectory.type === StorageType.IDB
|
|
48
44
|
? new IndexedDBStorageAdapter(storageDirectory.path, 'data')
|
|
49
45
|
: new AutomergeStorageAdapter(storageDirectory);
|
|
46
|
+
const localPeerId = `host-${PublicKey.random().toHex()}` as PeerId;
|
|
50
47
|
this._repo = new Repo({
|
|
51
|
-
peerId:
|
|
48
|
+
peerId: localPeerId,
|
|
52
49
|
network: [this._clientNetwork, this._meshNetwork],
|
|
53
50
|
storage: this._storage,
|
|
54
51
|
|
|
@@ -65,8 +62,9 @@ export class AutomergeHost {
|
|
|
65
62
|
|
|
66
63
|
const doc = this._repo.handles[documentId]?.docSync();
|
|
67
64
|
if (!doc) {
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
const isRequested = this._requestedDocs.has(`automerge:${documentId}`);
|
|
66
|
+
log('doc share policy check', { peerId, documentId, isRequested });
|
|
67
|
+
return isRequested;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
try {
|
|
@@ -87,13 +85,20 @@ export class AutomergeHost {
|
|
|
87
85
|
const deviceKey = PublicKey.from(deviceKeyHex);
|
|
88
86
|
|
|
89
87
|
const isAuthorized = authorizedDevices?.has(deviceKey) ?? false;
|
|
90
|
-
log('share policy check', {
|
|
88
|
+
log('share policy check', {
|
|
89
|
+
localPeer: localPeerId,
|
|
90
|
+
remotePeer: peerId,
|
|
91
|
+
documentId,
|
|
92
|
+
deviceKey,
|
|
93
|
+
spaceKey,
|
|
94
|
+
isAuthorized,
|
|
95
|
+
});
|
|
91
96
|
return isAuthorized;
|
|
92
97
|
} catch (err) {
|
|
93
98
|
log.catch(err);
|
|
94
99
|
return false;
|
|
95
100
|
}
|
|
96
|
-
},
|
|
101
|
+
},
|
|
97
102
|
});
|
|
98
103
|
this._clientNetwork.ready();
|
|
99
104
|
this._meshNetwork.ready();
|
|
@@ -147,288 +152,7 @@ export class AutomergeHost {
|
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
authorizeDevice(spaceKey: PublicKey, deviceKey: PublicKey) {
|
|
155
|
+
log('authorizeDevice', { spaceKey, deviceKey });
|
|
150
156
|
defaultMap(this._authorizedDevices, spaceKey, () => new ComplexSet(PublicKey.hash)).add(deviceKey);
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
|
-
|
|
154
|
-
type ClientSyncState = {
|
|
155
|
-
connected: boolean;
|
|
156
|
-
send: (message: Message) => void;
|
|
157
|
-
disconnect: () => void;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Used to replicate with apps running on the same device.
|
|
162
|
-
*/
|
|
163
|
-
class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
164
|
-
private readonly _peers: Map<PeerId, ClientSyncState> = new Map();
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
168
|
-
*/
|
|
169
|
-
ready() {
|
|
170
|
-
// NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
|
|
171
|
-
// because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
|
|
172
|
-
this.emit('ready', {
|
|
173
|
-
network: this,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private _connected = new Trigger();
|
|
178
|
-
|
|
179
|
-
override connect(peerId: PeerId): void {
|
|
180
|
-
this.peerId = peerId;
|
|
181
|
-
this._connected.wake();
|
|
182
|
-
// No-op. Client always connects first
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
override send(message: Message): void {
|
|
186
|
-
const peer = this._peers.get(message.targetId);
|
|
187
|
-
invariant(peer, 'Peer not found.');
|
|
188
|
-
peer.send(message);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async close() {
|
|
192
|
-
this._peers.forEach((peer) => peer.disconnect());
|
|
193
|
-
this.emit('close');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
override disconnect(): void {
|
|
197
|
-
// TODO(mykola): `disconnect` is not used anywhere in `Repo` from `@automerge/automerge-repo`. Should we remove it?
|
|
198
|
-
// No-op
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
syncRepo({ id, syncMessage }: SyncRepoRequest): Stream<SyncRepoResponse> {
|
|
202
|
-
const peerId = this._getPeerId(id);
|
|
203
|
-
|
|
204
|
-
return new Stream(({ next, close }) => {
|
|
205
|
-
invariant(!this._peers.has(peerId), 'Peer already connected.');
|
|
206
|
-
this._peers.set(peerId, {
|
|
207
|
-
connected: true,
|
|
208
|
-
send: (message) => {
|
|
209
|
-
next({
|
|
210
|
-
syncMessage: cbor.encode(message),
|
|
211
|
-
});
|
|
212
|
-
},
|
|
213
|
-
disconnect: () => {
|
|
214
|
-
this._peers.delete(peerId);
|
|
215
|
-
close();
|
|
216
|
-
this.emit('peer-disconnected', {
|
|
217
|
-
peerId,
|
|
218
|
-
});
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
this._connected
|
|
223
|
-
.wait({ timeout: 1_000 })
|
|
224
|
-
.then(() => {
|
|
225
|
-
this.emit('peer-candidate', {
|
|
226
|
-
peerMetadata: {},
|
|
227
|
-
peerId,
|
|
228
|
-
});
|
|
229
|
-
})
|
|
230
|
-
.catch((err) => log.catch(err));
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async sendSyncMessage({ id, syncMessage }: SyncRepoRequest): Promise<void> {
|
|
235
|
-
await this._connected.wait({ timeout: 1_000 });
|
|
236
|
-
const message = cbor.decode(syncMessage!) as Message;
|
|
237
|
-
this.emit('message', message);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async getHostInfo(): Promise<HostInfo> {
|
|
241
|
-
await this._connected.wait({ timeout: 1_000 });
|
|
242
|
-
invariant(this.peerId, 'Peer id not set.');
|
|
243
|
-
return {
|
|
244
|
-
peerId: this.peerId,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private _getPeerId(id: string): PeerId {
|
|
249
|
-
return id as PeerId;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Used to replicate with other peers over the network.
|
|
255
|
-
*/
|
|
256
|
-
export class MeshNetworkAdapter extends NetworkAdapter {
|
|
257
|
-
private readonly _extensions: Map<string, AutomergeReplicator> = new Map();
|
|
258
|
-
private _connected = new Trigger();
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
262
|
-
*/
|
|
263
|
-
ready() {
|
|
264
|
-
// NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
|
|
265
|
-
// because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
|
|
266
|
-
this.emit('ready', {
|
|
267
|
-
network: this,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
override connect(peerId: PeerId): void {
|
|
272
|
-
this.peerId = peerId;
|
|
273
|
-
this._connected.wake();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
override send(message: Message): void {
|
|
277
|
-
const receiverId = message.targetId;
|
|
278
|
-
const extension = this._extensions.get(receiverId);
|
|
279
|
-
invariant(extension, 'Extension not found.');
|
|
280
|
-
extension.sendSyncMessage({ payload: cbor.encode(message) }).catch((err) => log.catch(err));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
override disconnect(): void {
|
|
284
|
-
// No-op
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
createExtension(): AutomergeReplicator {
|
|
288
|
-
invariant(this.peerId, 'Peer id not set.');
|
|
289
|
-
|
|
290
|
-
let peerInfo: PeerInfo;
|
|
291
|
-
const extension = new AutomergeReplicator(
|
|
292
|
-
{
|
|
293
|
-
peerId: this.peerId,
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
297
|
-
await this._connected.wait();
|
|
298
|
-
|
|
299
|
-
// Note: We store only one extension per peer.
|
|
300
|
-
// There can be a case where two connected peers have more than one teleport connection between them
|
|
301
|
-
// and each of them uses different teleport connections to send messages.
|
|
302
|
-
// It works because we receive messages from all teleport connections and Automerge Repo dedup them.
|
|
303
|
-
// TODO(mykola): Use only one teleport connection per peer.
|
|
304
|
-
if (!this._extensions.has(info.id)) {
|
|
305
|
-
peerInfo = info;
|
|
306
|
-
// TODO(mykola): Fix race condition?
|
|
307
|
-
this._extensions.set(info.id, extension);
|
|
308
|
-
} else {
|
|
309
|
-
// TODO(mykola): retry hack.
|
|
310
|
-
this.emit('peer-disconnected', { peerId: info.id as PeerId });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
this.emit('peer-candidate', {
|
|
314
|
-
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
315
|
-
peerMetadata: {
|
|
316
|
-
dxos_deviceKey: remotePeerId.toHex(),
|
|
317
|
-
} as any,
|
|
318
|
-
peerId: info.id as PeerId,
|
|
319
|
-
});
|
|
320
|
-
},
|
|
321
|
-
onSyncMessage: async ({ payload }) => {
|
|
322
|
-
const message = cbor.decode(payload) as Message;
|
|
323
|
-
// Note: automerge Repo dedup messages.
|
|
324
|
-
this.emit('message', message);
|
|
325
|
-
},
|
|
326
|
-
onClose: async () => {
|
|
327
|
-
if (!peerInfo) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
this.emit('peer-disconnected', {
|
|
331
|
-
peerId: peerInfo.id as PeerId,
|
|
332
|
-
});
|
|
333
|
-
this._extensions.delete(peerInfo.id);
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
);
|
|
337
|
-
return extension;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export class AutomergeStorageAdapter extends StorageAdapter {
|
|
342
|
-
// TODO(mykola): Hack for restricting automerge Repo to access storage if Host is `closed`.
|
|
343
|
-
// Automerge Repo do not have any lifetime management.
|
|
344
|
-
private _state: 'opened' | 'closed' = 'opened';
|
|
345
|
-
|
|
346
|
-
constructor(private readonly _directory: Directory) {
|
|
347
|
-
super();
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
override async load(key: StorageKey): Promise<Uint8Array | undefined> {
|
|
351
|
-
if (this._state !== 'opened') {
|
|
352
|
-
return undefined;
|
|
353
|
-
}
|
|
354
|
-
const filename = this._getFilename(key);
|
|
355
|
-
const file = this._directory.getOrCreateFile(filename);
|
|
356
|
-
const { size } = await file.stat();
|
|
357
|
-
if (!size || size === 0) {
|
|
358
|
-
return undefined;
|
|
359
|
-
}
|
|
360
|
-
const buffer = await file.read(0, size);
|
|
361
|
-
return bufferToArray(buffer);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
override async save(key: StorageKey, data: Uint8Array): Promise<void> {
|
|
365
|
-
if (this._state !== 'opened') {
|
|
366
|
-
return undefined;
|
|
367
|
-
}
|
|
368
|
-
const filename = this._getFilename(key);
|
|
369
|
-
const file = this._directory.getOrCreateFile(filename);
|
|
370
|
-
await file.write(0, arrayToBuffer(data));
|
|
371
|
-
await file.truncate?.(data.length);
|
|
372
|
-
|
|
373
|
-
await file.flush?.();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
override async remove(key: StorageKey): Promise<void> {
|
|
377
|
-
if (this._state !== 'opened') {
|
|
378
|
-
return undefined;
|
|
379
|
-
}
|
|
380
|
-
// TODO(dmaretskyi): Better deletion.
|
|
381
|
-
const filename = this._getFilename(key);
|
|
382
|
-
const file = this._directory.getOrCreateFile(filename);
|
|
383
|
-
await file.destroy();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
override async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
387
|
-
if (this._state !== 'opened') {
|
|
388
|
-
return [];
|
|
389
|
-
}
|
|
390
|
-
const filename = this._getFilename(keyPrefix);
|
|
391
|
-
const entries = await this._directory.list();
|
|
392
|
-
return Promise.all(
|
|
393
|
-
entries
|
|
394
|
-
.filter((entry) => entry.startsWith(filename))
|
|
395
|
-
.map(async (entry): Promise<Chunk> => {
|
|
396
|
-
const file = this._directory.getOrCreateFile(entry);
|
|
397
|
-
const { size } = await file.stat();
|
|
398
|
-
const buffer = await file.read(0, size);
|
|
399
|
-
return {
|
|
400
|
-
key: this._getKeyFromFilename(entry),
|
|
401
|
-
data: bufferToArray(buffer),
|
|
402
|
-
};
|
|
403
|
-
}),
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
override async removeRange(keyPrefix: StorageKey): Promise<void> {
|
|
408
|
-
if (this._state !== 'opened') {
|
|
409
|
-
return undefined;
|
|
410
|
-
}
|
|
411
|
-
const filename = this._getFilename(keyPrefix);
|
|
412
|
-
const entries = await this._directory.list();
|
|
413
|
-
await Promise.all(
|
|
414
|
-
entries
|
|
415
|
-
.filter((entry) => entry.startsWith(filename))
|
|
416
|
-
.map(async (entry): Promise<void> => {
|
|
417
|
-
const file = this._directory.getOrCreateFile(entry);
|
|
418
|
-
await file.destroy();
|
|
419
|
-
}),
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async close(): Promise<void> {
|
|
424
|
-
this._state = 'closed';
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private _getFilename(key: StorageKey): string {
|
|
428
|
-
return key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-');
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private _getKeyFromFilename(filename: string): StorageKey {
|
|
432
|
-
return filename.split('-').map((k) => k.replaceAll('%2D', '-').replaceAll('%25', '%'));
|
|
433
|
-
}
|
|
434
|
-
}
|