@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.
Files changed (48) hide show
  1. package/dist/lib/browser/{chunk-VQQD32DM.mjs → chunk-VUJXFVSK.mjs} +49 -30
  2. package/dist/lib/browser/{chunk-VQQD32DM.mjs.map → chunk-VUJXFVSK.mjs.map} +3 -3
  3. package/dist/lib/browser/index.mjs +435 -288
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +3 -1
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-P7L7ICAH.cjs → chunk-6733E3WM.cjs} +50 -32
  9. package/dist/lib/node/{chunk-P7L7ICAH.cjs.map → chunk-6733E3WM.cjs.map} +3 -3
  10. package/dist/lib/node/index.cjs +446 -299
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +13 -11
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/types/src/automerge/automerge-host.d.ts +4 -11
  16. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  17. package/dist/types/src/automerge/echo-network-adapter.d.ts +6 -0
  18. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  19. package/dist/types/src/automerge/echo-replicator.d.ts +2 -0
  20. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  21. package/dist/types/src/automerge/index.d.ts +1 -1
  22. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +2 -2
  23. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +23 -0
  24. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -0
  25. package/dist/types/src/automerge/migrations.d.ts +2 -2
  26. package/dist/types/src/space/space-manager.d.ts +3 -2
  27. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  28. package/dist/types/src/space/space-protocol.d.ts +2 -0
  29. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  30. package/dist/types/src/space/space.d.ts +4 -3
  31. package/dist/types/src/space/space.d.ts.map +1 -1
  32. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  33. package/package.json +33 -33
  34. package/src/automerge/automerge-host.test.ts +22 -9
  35. package/src/automerge/automerge-host.ts +62 -88
  36. package/src/automerge/echo-network-adapter.ts +19 -0
  37. package/src/automerge/echo-replicator.ts +3 -0
  38. package/src/automerge/index.ts +1 -1
  39. package/src/automerge/leveldb-storage-adapter.ts +2 -2
  40. package/src/automerge/mesh-echo-replicator.ts +232 -0
  41. package/src/automerge/migrations.ts +2 -2
  42. package/src/space/space-manager.ts +4 -1
  43. package/src/space/space-protocol.ts +11 -8
  44. package/src/space/space.ts +8 -3
  45. package/src/testing/test-agent-builder.ts +1 -0
  46. package/dist/types/src/automerge/mesh-network-adapter.d.ts +0 -18
  47. package/dist/types/src/automerge/mesh-network-adapter.d.ts.map +0 -1
  48. 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 SubLevelDB } from '@dxos/kv-store';
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: SubLevelDB; directory: Directory }) => {
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: new MMSTTopology(topologyConfig),
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
 
@@ -3,8 +3,8 @@
3
3
  //
4
4
 
5
5
  import { Event, Mutex, synchronized, trackLeaks } from '@dxos/async';
6
- import { Resource, type Context, LifecycleState } from '@dxos/context';
7
- import { type FeedInfo, type DelegateInvitationCredential } from '@dxos/credentials';
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 { Callback, type AsyncCallback } from '@dxos/util';
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;
@@ -191,6 +191,7 @@ export class TestAgent {
191
191
  );
192
192
  },
193
193
  onDelegatedInvitationStatusChange: async () => {},
194
+ onMemberRolesChanged: async () => {},
194
195
  });
195
196
  await space.setControlFeed(controlFeed);
196
197
  await space.setDataFeed(dataFeed);
@@ -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
- }