@dxos/echo-pipeline 0.5.2 → 0.5.3-main.088a2c8
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-VQQD32DM.mjs → chunk-VUJXFVSK.mjs} +49 -30
- package/dist/lib/browser/{chunk-VQQD32DM.mjs.map → chunk-VUJXFVSK.mjs.map} +3 -3
- package/dist/lib/browser/index.mjs +435 -288
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +3 -1
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-P7L7ICAH.cjs → chunk-6733E3WM.cjs} +50 -32
- package/dist/lib/node/{chunk-P7L7ICAH.cjs.map → chunk-6733E3WM.cjs.map} +3 -3
- package/dist/lib/node/index.cjs +446 -299
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +13 -11
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/automerge/automerge-host.d.ts +4 -11
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +6 -0
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +2 -0
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +2 -2
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts +23 -0
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -0
- package/dist/types/src/automerge/migrations.d.ts +2 -2
- package/dist/types/src/space/space-manager.d.ts +3 -2
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +2 -0
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +4 -3
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-host.test.ts +22 -9
- package/src/automerge/automerge-host.ts +62 -88
- package/src/automerge/echo-network-adapter.ts +19 -0
- package/src/automerge/echo-replicator.ts +3 -0
- package/src/automerge/index.ts +1 -1
- package/src/automerge/leveldb-storage-adapter.ts +2 -2
- package/src/automerge/mesh-echo-replicator.ts +232 -0
- package/src/automerge/migrations.ts +2 -2
- package/src/space/space-manager.ts +4 -1
- package/src/space/space-protocol.ts +11 -8
- package/src/space/space.ts +8 -3
- package/src/testing/test-agent-builder.ts +1 -0
- package/dist/types/src/automerge/mesh-network-adapter.d.ts +0 -18
- package/dist/types/src/automerge/mesh-network-adapter.d.ts.map +0 -1
- package/src/automerge/mesh-network-adapter.ts +0 -107
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Message, cbor } from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { Resource } from '@dxos/context';
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
8
|
+
import { PublicKey } from '@dxos/keys';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
11
|
+
import { ComplexMap, ComplexSet, defaultMap } from '@dxos/util';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type EchoReplicator,
|
|
15
|
+
type EchoReplicatorContext,
|
|
16
|
+
type ReplicatorConnection,
|
|
17
|
+
type ShouldAdvertizeParams,
|
|
18
|
+
} from './echo-replicator';
|
|
19
|
+
|
|
20
|
+
// TODO(dmaretskyi): Move out of @dxos/echo-pipeline.
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Used to replicate with other peers over the network.
|
|
24
|
+
*/
|
|
25
|
+
export class MeshEchoReplicator implements EchoReplicator {
|
|
26
|
+
private readonly _connections = new Set<MeshReplicatorConnection>();
|
|
27
|
+
/**
|
|
28
|
+
* Using automerge peerId as a key.
|
|
29
|
+
*/
|
|
30
|
+
private readonly _connectionsPerPeer = new Map<string, MeshReplicatorConnection>();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* spaceKey -> deviceKey[]
|
|
34
|
+
*/
|
|
35
|
+
private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
|
|
36
|
+
|
|
37
|
+
private _context: EchoReplicatorContext | null = null;
|
|
38
|
+
|
|
39
|
+
async connect(context: EchoReplicatorContext): Promise<void> {
|
|
40
|
+
this._context = context;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async disconnect() {
|
|
44
|
+
for (const connection of this._connections) {
|
|
45
|
+
await connection.close();
|
|
46
|
+
}
|
|
47
|
+
this._connections.clear();
|
|
48
|
+
this._connectionsPerPeer.clear();
|
|
49
|
+
|
|
50
|
+
this._context = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
createExtension(): AutomergeReplicator {
|
|
54
|
+
invariant(this._context);
|
|
55
|
+
|
|
56
|
+
const connection: MeshReplicatorConnection = new MeshReplicatorConnection({
|
|
57
|
+
ownPeerId: this._context.peerId,
|
|
58
|
+
onRemoteConnected: async () => {
|
|
59
|
+
log('onRemoteConnected', { peerId: connection.peerId });
|
|
60
|
+
invariant(this._context);
|
|
61
|
+
|
|
62
|
+
if (!this._connectionsPerPeer.has(connection.peerId)) {
|
|
63
|
+
this._connectionsPerPeer.set(connection.peerId, connection);
|
|
64
|
+
await connection.enable();
|
|
65
|
+
this._context.onConnectionOpen(connection);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onRemoteDisconnected: async () => {
|
|
69
|
+
log('onRemoteDisconnected', { peerId: connection.peerId });
|
|
70
|
+
invariant(this._context);
|
|
71
|
+
this._context.onConnectionClosed(connection);
|
|
72
|
+
await connection.disable();
|
|
73
|
+
this._connectionsPerPeer.delete(connection.peerId);
|
|
74
|
+
this._connections.delete(connection);
|
|
75
|
+
},
|
|
76
|
+
shouldAdvertize: async (params: ShouldAdvertizeParams) => {
|
|
77
|
+
log('shouldAdvertize', { peerId: connection.peerId, documentId: params.documentId });
|
|
78
|
+
invariant(this._context);
|
|
79
|
+
try {
|
|
80
|
+
const spaceKey = await this._context.getContainingSpaceForDocument(params.documentId);
|
|
81
|
+
if (!spaceKey) {
|
|
82
|
+
log('space key not found for share policy check', {
|
|
83
|
+
peerId: connection.peerId,
|
|
84
|
+
documentId: params.documentId,
|
|
85
|
+
});
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const authorizedDevices = this._authorizedDevices.get(spaceKey);
|
|
90
|
+
|
|
91
|
+
if (!connection.remoteDeviceKey) {
|
|
92
|
+
log('device key not found for share policy check', {
|
|
93
|
+
peerId: connection.peerId,
|
|
94
|
+
documentId: params.documentId,
|
|
95
|
+
});
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isAuthorized = authorizedDevices?.has(connection.remoteDeviceKey) ?? false;
|
|
100
|
+
log('share policy check', {
|
|
101
|
+
localPeer: this._context.peerId,
|
|
102
|
+
remotePeer: connection.peerId,
|
|
103
|
+
documentId: params.documentId,
|
|
104
|
+
deviceKey: connection.remoteDeviceKey,
|
|
105
|
+
spaceKey,
|
|
106
|
+
isAuthorized,
|
|
107
|
+
});
|
|
108
|
+
return isAuthorized;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
log.catch(err);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
this._connections.add(connection);
|
|
116
|
+
|
|
117
|
+
return connection.replicatorExtension;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
authorizeDevice(spaceKey: PublicKey, deviceKey: PublicKey) {
|
|
121
|
+
log('authorizeDevice', { spaceKey, deviceKey });
|
|
122
|
+
defaultMap(this._authorizedDevices, spaceKey, () => new ComplexSet(PublicKey.hash)).add(deviceKey);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type MeshReplicatorConnectionParams = {
|
|
127
|
+
ownPeerId: string;
|
|
128
|
+
onRemoteConnected: () => Promise<void>;
|
|
129
|
+
onRemoteDisconnected: () => Promise<void>;
|
|
130
|
+
shouldAdvertize: (params: ShouldAdvertizeParams) => Promise<boolean>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
class MeshReplicatorConnection extends Resource implements ReplicatorConnection {
|
|
134
|
+
public readable: ReadableStream<Message>;
|
|
135
|
+
public writable: WritableStream<Message>;
|
|
136
|
+
public remoteDeviceKey: PublicKey | null = null;
|
|
137
|
+
|
|
138
|
+
public readonly replicatorExtension: AutomergeReplicator;
|
|
139
|
+
|
|
140
|
+
private _remotePeerId: string | null = null;
|
|
141
|
+
private _isEnabled = false;
|
|
142
|
+
|
|
143
|
+
constructor(private readonly _params: MeshReplicatorConnectionParams) {
|
|
144
|
+
super();
|
|
145
|
+
|
|
146
|
+
let readableStreamController!: ReadableStreamDefaultController<Message>;
|
|
147
|
+
this.readable = new ReadableStream<Message>({
|
|
148
|
+
start: (controller) => {
|
|
149
|
+
readableStreamController = controller;
|
|
150
|
+
this._ctx.onDispose(() => controller.close());
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.writable = new WritableStream<Message>({
|
|
155
|
+
write: async (message: Message, controller) => {
|
|
156
|
+
// TODO(dmaretskyi): Show we block on RPC completing here?
|
|
157
|
+
this.replicatorExtension.sendSyncMessage({ payload: cbor.encode(message) }).catch((err) => {
|
|
158
|
+
controller.error(err);
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.replicatorExtension = new AutomergeReplicator(
|
|
164
|
+
{
|
|
165
|
+
peerId: this._params.ownPeerId,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
169
|
+
// Note: We store only one extension per peer.
|
|
170
|
+
// There can be a case where two connected peers have more than one teleport connection between them
|
|
171
|
+
// and each of them uses different teleport connections to send messages.
|
|
172
|
+
// It works because we receive messages from all teleport connections and Automerge Repo dedup them.
|
|
173
|
+
// TODO(mykola): Use only one teleport connection per peer.
|
|
174
|
+
|
|
175
|
+
// TODO(dmaretskyi): Critical bug.
|
|
176
|
+
// - two peers get connected via swarm 1
|
|
177
|
+
// - they get connected via swarm 2
|
|
178
|
+
// - swarm 1 gets disconnected
|
|
179
|
+
// - automerge repo thinks that peer 2 got disconnected even though swarm 2 is still active
|
|
180
|
+
|
|
181
|
+
this.remoteDeviceKey = remotePeerId;
|
|
182
|
+
|
|
183
|
+
// Set automerge id.
|
|
184
|
+
this._remotePeerId = info.id;
|
|
185
|
+
|
|
186
|
+
log('onStartReplication', { id: info.id, thisPeerId: this.peerId, remotePeerId: remotePeerId.toHex() });
|
|
187
|
+
|
|
188
|
+
await this._params.onRemoteConnected();
|
|
189
|
+
},
|
|
190
|
+
onSyncMessage: async ({ payload }) => {
|
|
191
|
+
if (!this._isEnabled) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const message = cbor.decode(payload) as Message;
|
|
195
|
+
// Note: automerge Repo dedup messages.
|
|
196
|
+
readableStreamController.enqueue(message);
|
|
197
|
+
},
|
|
198
|
+
onClose: async () => {
|
|
199
|
+
if (!this._isEnabled) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await this._params.onRemoteDisconnected();
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
get peerId(): string {
|
|
209
|
+
invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
|
|
210
|
+
return this._remotePeerId;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async shouldAdvertize(params: ShouldAdvertizeParams): Promise<boolean> {
|
|
214
|
+
return this._params.shouldAdvertize(params);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Start exchanging messages with the remote peer.
|
|
219
|
+
* Call after the remote peer has connected.
|
|
220
|
+
*/
|
|
221
|
+
async enable() {
|
|
222
|
+
invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
|
|
223
|
+
this._isEnabled = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Stop exchanging messages with the remote peer.
|
|
228
|
+
*/
|
|
229
|
+
async disable() {
|
|
230
|
+
this._isEnabled = false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { type StorageKey } from '@dxos/automerge/automerge-repo';
|
|
6
6
|
import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
|
|
7
|
-
import { type
|
|
7
|
+
import { type SublevelDB } from '@dxos/kv-store';
|
|
8
8
|
import { log } from '@dxos/log';
|
|
9
9
|
import { StorageType, type Directory } from '@dxos/random-access-storage';
|
|
10
10
|
|
|
11
11
|
import { AutomergeStorageAdapter } from './automerge-storage-adapter';
|
|
12
12
|
import { encodingOptions } from './leveldb-storage-adapter';
|
|
13
13
|
|
|
14
|
-
export const levelMigration = async ({ db, directory }: { db:
|
|
14
|
+
export const levelMigration = async ({ db, directory }: { db: SublevelDB; directory: Directory }) => {
|
|
15
15
|
// Note: Make automigration from previous storage to leveldb here.
|
|
16
16
|
const isNewLevel = !(await db
|
|
17
17
|
.iterator<StorageKey, Uint8Array>({
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { synchronized, trackLeaks } from '@dxos/async';
|
|
6
|
-
import { type DelegateInvitationCredential } from '@dxos/credentials';
|
|
6
|
+
import { type DelegateInvitationCredential, type MemberInfo } from '@dxos/credentials';
|
|
7
7
|
import { failUndefined } from '@dxos/debug';
|
|
8
8
|
import { type FeedStore } from '@dxos/feed-store';
|
|
9
9
|
import { PublicKey } from '@dxos/keys';
|
|
@@ -44,6 +44,7 @@ export type ConstructSpaceParams = {
|
|
|
44
44
|
onAuthorizedConnection: (session: Teleport) => void;
|
|
45
45
|
onAuthFailure?: (session: Teleport) => void;
|
|
46
46
|
onDelegatedInvitationStatusChange: (invitation: DelegateInvitationCredential, isActive: boolean) => Promise<void>;
|
|
47
|
+
onMemberRolesChanged: (member: MemberInfo[]) => Promise<void>;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -87,6 +88,7 @@ export class SpaceManager {
|
|
|
87
88
|
onAuthorizedConnection,
|
|
88
89
|
onAuthFailure,
|
|
89
90
|
onDelegatedInvitationStatusChange,
|
|
91
|
+
onMemberRolesChanged,
|
|
90
92
|
memberKey,
|
|
91
93
|
}: ConstructSpaceParams) {
|
|
92
94
|
log.trace('dxos.echo.space-manager.construct-space', trace.begin({ id: this._instanceId }));
|
|
@@ -115,6 +117,7 @@ export class SpaceManager {
|
|
|
115
117
|
snapshotManager,
|
|
116
118
|
memberKey,
|
|
117
119
|
onDelegatedInvitationStatusChange,
|
|
120
|
+
onMemberRolesChanged,
|
|
118
121
|
});
|
|
119
122
|
this._spaces.set(space.key, space);
|
|
120
123
|
|
|
@@ -70,6 +70,12 @@ export class SpaceProtocol {
|
|
|
70
70
|
|
|
71
71
|
private readonly _feeds = new Set<FeedWrapper<FeedMessage>>();
|
|
72
72
|
private readonly _sessions = new ComplexMap<PublicKey, SpaceProtocolSession>(PublicKey.hash);
|
|
73
|
+
// TODO(burdon): Move to config (with sensible defaults).
|
|
74
|
+
private readonly _topology = new MMSTTopology({
|
|
75
|
+
originateConnections: 4,
|
|
76
|
+
maxPeers: 10,
|
|
77
|
+
sampleSize: 20,
|
|
78
|
+
});
|
|
73
79
|
|
|
74
80
|
private _connection?: SwarmConnection;
|
|
75
81
|
|
|
@@ -117,13 +123,6 @@ export class SpaceProtocol {
|
|
|
117
123
|
// TODO(burdon): Document why empty buffer.
|
|
118
124
|
const credentials = await this._swarmIdentity.credentialProvider(Buffer.from(''));
|
|
119
125
|
|
|
120
|
-
// TODO(burdon): Move to config (with sensible defaults).
|
|
121
|
-
const topologyConfig = {
|
|
122
|
-
originateConnections: 4,
|
|
123
|
-
maxPeers: 10,
|
|
124
|
-
sampleSize: 20,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
126
|
await this.blobSync.open();
|
|
128
127
|
|
|
129
128
|
log('starting...');
|
|
@@ -132,13 +131,17 @@ export class SpaceProtocol {
|
|
|
132
131
|
protocolProvider: this._createProtocolProvider(credentials),
|
|
133
132
|
peerId: this._swarmIdentity.peerKey,
|
|
134
133
|
topic,
|
|
135
|
-
topology:
|
|
134
|
+
topology: this._topology,
|
|
136
135
|
label: `swarm ${topic.truncate()} for space ${this._spaceKey.truncate()}`,
|
|
137
136
|
});
|
|
138
137
|
|
|
139
138
|
log('started');
|
|
140
139
|
}
|
|
141
140
|
|
|
141
|
+
public updateTopology() {
|
|
142
|
+
this._topology.forceUpdate();
|
|
143
|
+
}
|
|
144
|
+
|
|
142
145
|
async stop() {
|
|
143
146
|
await this.blobSync.close();
|
|
144
147
|
|
package/src/space/space.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Event, Mutex, synchronized, trackLeaks } from '@dxos/async';
|
|
6
|
-
import {
|
|
7
|
-
import { type FeedInfo, type
|
|
6
|
+
import { type Context, LifecycleState, Resource } from '@dxos/context';
|
|
7
|
+
import { type DelegateInvitationCredential, type FeedInfo, type MemberInfo } from '@dxos/credentials';
|
|
8
8
|
import { type FeedOptions, type FeedWrapper } from '@dxos/feed-store';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { type PublicKey } from '@dxos/keys';
|
|
@@ -13,7 +13,7 @@ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
|
13
13
|
import { AdmittedFeed, type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
14
14
|
import { type Timeframe } from '@dxos/timeframe';
|
|
15
15
|
import { trace } from '@dxos/tracing';
|
|
16
|
-
import {
|
|
16
|
+
import { type AsyncCallback, Callback } from '@dxos/util';
|
|
17
17
|
|
|
18
18
|
import { ControlPipeline } from './control-pipeline';
|
|
19
19
|
import { type SpaceProtocol } from './space-protocol';
|
|
@@ -37,6 +37,7 @@ export type SpaceParams = {
|
|
|
37
37
|
snapshotId?: string | undefined;
|
|
38
38
|
|
|
39
39
|
onDelegatedInvitationStatusChange: (invitation: DelegateInvitationCredential, isActive: boolean) => Promise<void>;
|
|
40
|
+
onMemberRolesChanged: (member: MemberInfo[]) => Promise<void>;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export type CreatePipelineParams = {
|
|
@@ -109,6 +110,10 @@ export class Space extends Resource {
|
|
|
109
110
|
log('onDelegatedInvitationRemoved', { invitation });
|
|
110
111
|
await params.onDelegatedInvitationStatusChange(invitation, false);
|
|
111
112
|
});
|
|
113
|
+
this._controlPipeline.onMemberRoleChanged.set(async (changedMembers) => {
|
|
114
|
+
log('onMemberRoleChanged', () => ({ changedMembers: changedMembers.map((m) => [m.key, m.role]) }));
|
|
115
|
+
await params.onMemberRolesChanged(changedMembers);
|
|
116
|
+
});
|
|
112
117
|
|
|
113
118
|
// Start replicating the genesis feed.
|
|
114
119
|
this.protocol = params.protocol;
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Trigger } from '@dxos/async';
|
|
6
|
-
import { NetworkAdapter, type Message, type PeerId, cbor } from '@dxos/automerge/automerge-repo';
|
|
7
|
-
import { invariant } from '@dxos/invariant';
|
|
8
|
-
import { log } from '@dxos/log';
|
|
9
|
-
import { type PeerInfo } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|
|
10
|
-
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Used to replicate with other peers over the network.
|
|
14
|
-
*/
|
|
15
|
-
export class MeshNetworkAdapter extends NetworkAdapter {
|
|
16
|
-
private readonly _extensions: Map<string, AutomergeReplicator> = new Map();
|
|
17
|
-
private _connected = new Trigger();
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
21
|
-
*/
|
|
22
|
-
ready() {
|
|
23
|
-
// NOTE: Emitting `ready` event in NetworkAdapter`s constructor causes a race condition
|
|
24
|
-
// because `Repo` waits for `ready` event (which it never receives) before it starts using the adapter.
|
|
25
|
-
this.emit('ready', {
|
|
26
|
-
network: this,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
override connect(peerId: PeerId): void {
|
|
31
|
-
this.peerId = peerId;
|
|
32
|
-
this._connected.wake();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
override send(message: Message): void {
|
|
36
|
-
const receiverId = message.targetId;
|
|
37
|
-
const extension = this._extensions.get(receiverId);
|
|
38
|
-
invariant(extension, 'Extension not found.');
|
|
39
|
-
extension.sendSyncMessage({ payload: cbor.encode(message) }).catch((err) => log.catch(err));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
override disconnect(): void {
|
|
43
|
-
// No-op
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
createExtension(): AutomergeReplicator {
|
|
47
|
-
invariant(this.peerId, 'Peer id not set.');
|
|
48
|
-
|
|
49
|
-
let peerInfo: PeerInfo;
|
|
50
|
-
const extension = new AutomergeReplicator(
|
|
51
|
-
{
|
|
52
|
-
peerId: this.peerId,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
56
|
-
await this._connected.wait();
|
|
57
|
-
|
|
58
|
-
// Note: We store only one extension per peer.
|
|
59
|
-
// There can be a case where two connected peers have more than one teleport connection between them
|
|
60
|
-
// and each of them uses different teleport connections to send messages.
|
|
61
|
-
// It works because we receive messages from all teleport connections and Automerge Repo dedup them.
|
|
62
|
-
// TODO(mykola): Use only one teleport connection per peer.
|
|
63
|
-
|
|
64
|
-
// TODO(dmaretskyi): Critical bug.
|
|
65
|
-
// - two peers get connected via swarm 1
|
|
66
|
-
// - they get connected via swarm 2
|
|
67
|
-
// - swarm 1 gets disconnected
|
|
68
|
-
// - automerge repo thinks that peer 2 got disconnected even though swarm 2 is still active
|
|
69
|
-
|
|
70
|
-
log('onStartReplication', { id: info.id, thisPeerId: this.peerId, remotePeerId: remotePeerId.toHex() });
|
|
71
|
-
if (!this._extensions.has(info.id)) {
|
|
72
|
-
peerInfo = info;
|
|
73
|
-
// TODO(mykola): Fix race condition?
|
|
74
|
-
this._extensions.set(info.id, extension);
|
|
75
|
-
|
|
76
|
-
log('peer-candidate', { id: info.id, thisPeerId: this.peerId, remotePeerId: remotePeerId.toHex() });
|
|
77
|
-
this.emit('peer-candidate', {
|
|
78
|
-
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
79
|
-
peerMetadata: {
|
|
80
|
-
dxos_deviceKey: remotePeerId.toHex(),
|
|
81
|
-
} as any,
|
|
82
|
-
peerId: info.id as PeerId,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
onSyncMessage: async ({ payload }) => {
|
|
87
|
-
if (!peerInfo) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const message = cbor.decode(payload) as Message;
|
|
91
|
-
// Note: automerge Repo dedup messages.
|
|
92
|
-
this.emit('message', message);
|
|
93
|
-
},
|
|
94
|
-
onClose: async () => {
|
|
95
|
-
if (!peerInfo) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
this.emit('peer-disconnected', {
|
|
99
|
-
peerId: peerInfo.id as PeerId,
|
|
100
|
-
});
|
|
101
|
-
this._extensions.delete(peerInfo.id);
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
return extension;
|
|
106
|
-
}
|
|
107
|
-
}
|