@persistica/flux-agent 0.0.28 → 0.0.35

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/README.md CHANGED
@@ -1,22 +1,28 @@
1
- # Flux Agent
1
+ # @persistica/flux-agent
2
2
 
3
- ```TypeScript
4
- import { FluxAgent, FluxAgentNetworkConnection } from '@persistica/flux-agent';
3
+ Connect an **Agent** to a Flux network, join channels, and publish/subscribe over the Flux mesh.
5
4
 
6
- const NETWORK_ID = 'network-id-to-connect-to';
5
+ ```bash
6
+ bun add @persistica/flux-agent # or: npm i @persistica/flux-agent
7
+ ```
7
8
 
8
- const fluxAgent = new FluxAgent(NETWORK_ID);
9
+ ```ts
10
+ import { FluxAgent } from '@persistica/flux-agent';
9
11
 
10
- // * Connect to network
11
- const fluxNetworkConnection: FluxAgentNetworkConnection = await fluxAgent
12
- .connect(
13
- 'CODE_TO_ACCESS_NETWORK',
14
- );
12
+ const agent = new FluxAgent('my-network-id');
15
13
 
16
- console.log(`✅ Agent connected to network ID: '${NETWORK_ID}'`);
14
+ // The argument is your claim — whatever your Authority accepts.
15
+ const connection = await agent.connect({ user: 'alice' });
17
16
 
18
- // * Join network channel
19
- const fluxConnectedAgentNetworkChannel: FluxNetworkChannel = await fluxNetworkConnection
20
- .joinChannel('connected-agents');
17
+ const channel = await connection.joinChannel('connected-agents');
18
+ channel.onPublish((msg) => console.log('received', msg));
19
+ channel.publish({ text: 'hello mesh' });
20
+ ```
21
21
 
22
- ```
22
+ > A network needs an **Authority** ([`@persistica/flux-authority`](../authority/README.md)) running
23
+ > before any Agent can join. With none present, `connect()` stays in the `waiting-for-authority`
24
+ > state and keeps retrying. Attach `agent.onNetworkState(...)` and wrap `connect()` in a `try/catch`
25
+ > to observe this.
26
+
27
+ Full reference (options, connection states, error handling, troubleshooting):
28
+ [docs/sdk/flux-agent.md](../../../docs/sdk/flux-agent.md).
package/index.js CHANGED
@@ -43,6 +43,8 @@ var UNSUBSCRIBE_NETWORK_CHANNEL_NAME = "unsubscribe-nc-name";
43
43
  var UNSUBSCRIBED_NETWORK_CHANNEL_NAME = "unsubscribed-nc-name";
44
44
  var AUTHORITY_CHANNEL_SUBSCRIBE = "acs";
45
45
  var AUTHORITY_DISCONNECT_AGENT = "ada";
46
+ var HEARTBEAT_PING = "hb-ping";
47
+ var HEARTBEAT_PONG = "hb-pong";
46
48
  var ERROR = "e";
47
49
 
48
50
  // libs/flux/shared/types/src/lib/client.type.ts
@@ -147,11 +149,21 @@ var WebSocketClient = class extends RPCServer {
147
149
  ws;
148
150
  isOpen = false;
149
151
  // Is the connection open
152
+ // * Heartbeat (see #488): a half-open socket (1006, no close frame) never fires
153
+ // `onclose`, so the client pings on an interval and treats a missed pong as the
154
+ // disconnect signal itself — it cannot rely on `ws.close()` producing an event.
155
+ heartbeatTimer;
156
+ awaitingPong = false;
157
+ pongSeen = false;
158
+ // The server proved it answers pings on this connection
159
+ closeHandled = false;
160
+ // Guards against running the close path twice (heartbeat + late onclose)
150
161
  constructor(options) {
151
162
  super();
152
163
  this.options = {
153
164
  autoReconnect: true,
154
165
  reconnectDelay: 1e3,
166
+ maxReconnectDelay: 3e4,
155
167
  heartbeatInterval: 6e4,
156
168
  connectionTimeout: 5e3,
157
169
  ...options
@@ -162,50 +174,48 @@ var WebSocketClient = class extends RPCServer {
162
174
  */
163
175
  connect() {
164
176
  return new Promise((resolve, reject) => {
177
+ this.closeHandled = false;
165
178
  this.emit("connecting", this.reconnectAttempts);
166
- this.ws = new WebSocket(
179
+ const socket = new WebSocket(
167
180
  this.options.url
168
181
  );
182
+ this.ws = socket;
169
183
  const timeout = setTimeout(() => {
170
- if (this.ws?.readyState !== WebSocket.OPEN) {
171
- this.ws?.close();
184
+ if (socket.readyState !== WebSocket.OPEN) {
185
+ socket.close();
172
186
  reject(new Error("Connection timeout"));
173
187
  }
174
188
  }, this.options.connectionTimeout);
175
- this.ws.onopen = () => {
189
+ socket.onopen = () => {
176
190
  clearTimeout(timeout);
191
+ if (this.ws !== socket) {
192
+ return;
193
+ }
177
194
  this.reconnectAttempts = 0;
178
195
  this.isOpen = true;
196
+ this.startHeartbeat();
179
197
  this.emit("open");
180
198
  resolve();
181
199
  };
182
- this.ws.onmessage = (event) => {
200
+ socket.onmessage = (event) => {
201
+ if (this.ws !== socket) {
202
+ return;
203
+ }
204
+ if (event.data === HEARTBEAT_PONG) {
205
+ this.awaitingPong = false;
206
+ this.pongSeen = true;
207
+ return;
208
+ }
183
209
  this.emit("message", event.data);
184
210
  };
185
- this.ws.onclose = (closeEvent) => {
186
- const wasKicked = closeEvent.reason === "Kicked by process";
211
+ socket.onclose = (closeEvent) => {
187
212
  clearTimeout(timeout);
188
- if (this.isOpen) {
189
- this.emit(
190
- "close",
191
- wasKicked ? "kicked" : void 0
192
- );
193
- this.isOpen = false;
194
- }
195
- if (this.options.autoReconnect && (this.options.retries === void 0 || this.reconnectAttempts < this.options.retries)) {
196
- const delay = wasKicked ? RECONNECTION_DELAY_ON_KICK_MS : Math.min(
197
- this.options.reconnectDelay ?? Number.POSITIVE_INFINITY,
198
- (this.options.reconnectDelay ?? 0) * (this.reconnectAttempts || 1)
199
- );
200
- setTimeout(() => {
201
- this.reconnectAttempts++;
202
- void this.connect();
203
- }, delay);
204
- } else if (this.options.autoReconnect) {
205
- this.emit("error", new Error("Connection failed: retries exhausted"));
213
+ if (this.ws !== socket) {
214
+ return;
206
215
  }
216
+ this.handleConnectionClosed(closeEvent.reason === "Kicked by process");
207
217
  };
208
- this.ws.onerror = (event) => {
218
+ socket.onerror = (event) => {
209
219
  if (event?.message?.includes("Failed to connect")) {
210
220
  } else {
211
221
  console.log("\u274C Error", event.message);
@@ -213,6 +223,84 @@ var WebSocketClient = class extends RPCServer {
213
223
  };
214
224
  });
215
225
  }
226
+ /**
227
+ * The single close path, reachable from `onclose` AND from a missed heartbeat
228
+ * pong (where `onclose` never fires): emit `close` once, then schedule the
229
+ * reconnect per the retry policy.
230
+ */
231
+ handleConnectionClosed(wasKicked) {
232
+ if (this.closeHandled) {
233
+ return;
234
+ }
235
+ this.closeHandled = true;
236
+ this.stopHeartbeat();
237
+ if (this.isOpen) {
238
+ this.emit(
239
+ "close",
240
+ wasKicked ? "kicked" : void 0
241
+ );
242
+ this.isOpen = false;
243
+ }
244
+ if (this.options.autoReconnect && (this.options.retries === void 0 || this.reconnectAttempts < this.options.retries)) {
245
+ const delay = wasKicked ? RECONNECTION_DELAY_ON_KICK_MS : Math.min(
246
+ this.options.maxReconnectDelay ?? Number.POSITIVE_INFINITY,
247
+ (this.options.reconnectDelay ?? 0) * 2 ** this.reconnectAttempts
248
+ );
249
+ setTimeout(() => {
250
+ this.reconnectAttempts++;
251
+ this.connect().catch((error) => {
252
+ this.emit(
253
+ "error",
254
+ error instanceof Error ? error : new Error(String(error))
255
+ );
256
+ });
257
+ }, delay);
258
+ } else if (this.options.autoReconnect) {
259
+ this.emit("error", new Error("Connection failed: retries exhausted"));
260
+ }
261
+ }
262
+ /**
263
+ * Starts the keepalive: every `heartbeatInterval` send an app-level ping (the
264
+ * browser/Bun `WebSocket` exposes no ping frame) and require the mesh's pong
265
+ * before the next tick. A missed pong means the socket is dead — force the
266
+ * close path directly, since a half-open socket produces no `onclose`.
267
+ */
268
+ startHeartbeat() {
269
+ this.stopHeartbeat();
270
+ this.pongSeen = false;
271
+ const interval = this.options.heartbeatInterval;
272
+ if (interval === void 0 || interval <= 0) {
273
+ return;
274
+ }
275
+ this.heartbeatTimer = setInterval(() => {
276
+ if (this.awaitingPong) {
277
+ if (!this.pongSeen) {
278
+ console.warn("\u{1F49B} Server does not answer heartbeat pings \u2014 dead-connection detection disabled for this connection");
279
+ this.stopHeartbeat();
280
+ return;
281
+ }
282
+ console.warn(`\u{1F494} No heartbeat pong within ${interval}ms \u2014 treating the connection as dead`);
283
+ this.stopHeartbeat();
284
+ try {
285
+ this.ws?.close(4e3, "Heartbeat timeout");
286
+ } catch {
287
+ }
288
+ this.handleConnectionClosed(false);
289
+ return;
290
+ }
291
+ if (this.ws?.readyState === WebSocket.OPEN) {
292
+ this.awaitingPong = true;
293
+ this.ws.send(HEARTBEAT_PING);
294
+ }
295
+ }, interval);
296
+ }
297
+ stopHeartbeat() {
298
+ if (this.heartbeatTimer !== void 0) {
299
+ clearInterval(this.heartbeatTimer);
300
+ this.heartbeatTimer = void 0;
301
+ }
302
+ this.awaitingPong = false;
303
+ }
216
304
  /**
217
305
  * Emits a WebSocket event to all registered listeners.
218
306
  */
@@ -253,6 +341,7 @@ var WebSocketClient = class extends RPCServer {
253
341
  */
254
342
  close() {
255
343
  this.options.autoReconnect = false;
344
+ this.stopHeartbeat();
256
345
  this.ws?.close();
257
346
  this.clearEventSubscribers();
258
347
  }
@@ -290,22 +379,12 @@ var peerConnectionConfig = {
290
379
  ]
291
380
  };
292
381
  var ICEConnection = class {
293
- constructor(_fluxWebSocketClientConnection, _stateChange) {
382
+ constructor(_fluxWebSocketClientConnection, _stateChange, _onMessage) {
294
383
  this._fluxWebSocketClientConnection = _fluxWebSocketClientConnection;
295
384
  this._stateChange = _stateChange;
385
+ this._onMessage = _onMessage;
296
386
  this.peerConnection.ondatachannel = (event) => {
297
- console.log("*********** GOT DATAT SCHANNNEL ");
298
- this.dataChannel = event.channel;
299
- this.dataChannel.onopen = () => console.log("ReceiveChannel opened");
300
- this.dataChannel.onmessage = (event2) => {
301
- console.log("Receiving ICE event", event2);
302
- };
303
- this.dataChannel.onopen = () => {
304
- console.log("Opened");
305
- };
306
- this.dataChannel.onclose = () => {
307
- console.log("Opened");
308
- };
387
+ this._wireDataChannel(event.channel);
309
388
  };
310
389
  this.peerConnection.onicecandidate = (event) => {
311
390
  if (event.candidate) {
@@ -323,10 +402,10 @@ var ICEConnection = class {
323
402
  }
324
403
  _fluxWebSocketClientConnection;
325
404
  _stateChange;
405
+ _onMessage;
326
406
  //extends RPCServer<'createOffer' | 'acceptOffer' | 'acceptAnswer'> {
327
407
  peerConnection = new RTCPeerConnection(peerConnectionConfig);
328
408
  dataChannel;
329
- // = this.peerConnection.createDataChannel('flux-channel');
330
409
  offerSDP;
331
410
  /**
332
411
  * This offer must be sent to the other peer.
@@ -335,7 +414,7 @@ var ICEConnection = class {
335
414
  */
336
415
  async createOffer() {
337
416
  console.log("creating offer and data channel");
338
- this.dataChannel = this.peerConnection.createDataChannel("flux-channel");
417
+ this._wireDataChannel(this.peerConnection.createDataChannel("flux-channel"));
339
418
  this._stateChange("creating-offer");
340
419
  const offer = await this.peerConnection.createOffer();
341
420
  await this.peerConnection.setLocalDescription(offer);
@@ -380,9 +459,28 @@ var ICEConnection = class {
380
459
  answerAcceptedByInitiator(answerAccepted) {
381
460
  this._stateChange(answerAccepted ? "connected" : "failed");
382
461
  }
462
+ /**
463
+ * Wires up a data channel's lifecycle handlers. Used for both the initiator's
464
+ * locally-created channel and the receiver's incoming channel so inbound
465
+ * messages are delivered the same way on both ends.
466
+ *
467
+ * @param { RTCDataChannel } channel - The data channel to wire up
468
+ */
469
+ _wireDataChannel(channel) {
470
+ this.dataChannel = channel;
471
+ channel.onopen = () => {
472
+ console.log("Data channel opened");
473
+ };
474
+ channel.onclose = () => {
475
+ console.log("Data channel closed");
476
+ };
477
+ channel.onmessage = (event) => {
478
+ this._onMessage(event.data);
479
+ };
480
+ }
383
481
  /**
384
482
  * Sends a message to the connected peer via the WebRTC data channel.
385
- *
483
+ *
386
484
  * @param { string } message - The message to send
387
485
  */
388
486
  sendMessage(message) {
@@ -397,14 +495,16 @@ var ICEConnection = class {
397
495
 
398
496
  // libs/flux/shared/connection/src/lib/agent/flux-agent-network.class.ts
399
497
  var FluxAgentNetworkConnection = class {
400
- constructor(_fluxWebSocketConnection, _fluxWebSocketClientConnection, _webRTCStateChange) {
498
+ constructor(_fluxWebSocketConnection, _fluxWebSocketClientConnection, _webRTCStateChange, _onDirectMessage) {
401
499
  this._fluxWebSocketConnection = _fluxWebSocketConnection;
402
500
  this._fluxWebSocketClientConnection = _fluxWebSocketClientConnection;
403
501
  this._webRTCStateChange = _webRTCStateChange;
502
+ this._onDirectMessage = _onDirectMessage;
404
503
  if (typeof RTCPeerConnection !== "undefined") {
405
504
  this.iceConnection = new ICEConnection(
406
505
  this._fluxWebSocketClientConnection,
407
- this._webRTCStateChange
506
+ this._webRTCStateChange,
507
+ this._onDirectMessage
408
508
  );
409
509
  console.log(`RTCPeerConnection is available`);
410
510
  } else {
@@ -414,6 +514,7 @@ var FluxAgentNetworkConnection = class {
414
514
  _fluxWebSocketConnection;
415
515
  _fluxWebSocketClientConnection;
416
516
  _webRTCStateChange;
517
+ _onDirectMessage;
417
518
  iceConnection;
418
519
  connectedChannelSet = /* @__PURE__ */ new Set();
419
520
  /**
@@ -514,8 +615,10 @@ var FluxNetworkChannel = class {
514
615
  onPublish(fn) {
515
616
  this._fluxWebSocketConnection.onPublish(
516
617
  this._channelName,
618
+ // The transport types payloads as `string` (TMessageCallback), but
619
+ // `o:`-prefixed messages are JSON-parsed before delivery, so the
620
+ // callback actually receives `T`. Bridge the two deliberately.
517
621
  fn
518
- // TODO
519
622
  );
520
623
  }
521
624
  };
@@ -734,7 +837,7 @@ var FluxWebSocketConnection = class {
734
837
  });
735
838
  this.socket.clearEventSubscribers();
736
839
  this.socket.on("message", this.socketMessageHandler).on("close", this.socketCloseHandler).on("connecting", this.socketConnectingHandler).on("error", this.socketErrorHandler);
737
- void this.socket.connect();
840
+ this.socket.connect().catch(this.socketErrorHandler);
738
841
  return this.connectPromise;
739
842
  }
740
843
  /**
@@ -768,7 +871,8 @@ var FluxWebSocketConnection = class {
768
871
  return Promise.resolve(new FluxAgentNetworkConnection(
769
872
  this,
770
873
  webSocketClient,
771
- this.stateManager.emitWebRTCState.bind(this.stateManager)
874
+ this.stateManager.emitWebRTCState.bind(this.stateManager),
875
+ this.stateManager.emitDirectMessage.bind(this.stateManager)
772
876
  ));
773
877
  }
774
878
  /**
@@ -796,7 +900,7 @@ var FluxWebSocketConnection = class {
796
900
  */
797
901
  publish(channelName, message) {
798
902
  if (this.webSocketClient) {
799
- const messageString = typeof message === "string" ? message : `o:${JSON.stringify(message)}`;
903
+ const messageString = typeof message === "string" ? `s:${message}` : `o:${JSON.stringify(message)}`;
800
904
  this.webSocketClient.send(`${NETWORK_CHANNEL_PUBLISH}:${channelName}:${messageString}`);
801
905
  }
802
906
  }
@@ -909,13 +1013,18 @@ var FluxWebSocketConnection = class {
909
1013
  if (validateChannelNameOrThrow(channelName)) {
910
1014
  const topicCallbacks = this.channelCallbacks.get(channelName);
911
1015
  if (topicCallbacks) {
912
- let data = message_.slice(thirdColon + 1);
913
- if (data.startsWith("o:")) {
1016
+ const raw = message_.slice(thirdColon + 1);
1017
+ let data;
1018
+ if (raw.startsWith("o:")) {
914
1019
  try {
915
- data = JSON.parse(data.substring(2));
1020
+ data = JSON.parse(raw.substring(2));
916
1021
  } catch {
917
- data = data.substring(2);
1022
+ data = raw.substring(2);
918
1023
  }
1024
+ } else if (raw.startsWith("s:")) {
1025
+ data = raw.substring(2);
1026
+ } else {
1027
+ data = raw;
919
1028
  }
920
1029
  for (const cb of topicCallbacks) {
921
1030
  cb(data);
@@ -1002,6 +1111,7 @@ async function encrypt(data, password) {
1002
1111
  var StateManager = class {
1003
1112
  networkStateListeners = /* @__PURE__ */ new Set();
1004
1113
  webRTCStateListeners = /* @__PURE__ */ new Set();
1114
+ directMessageListeners = /* @__PURE__ */ new Set();
1005
1115
  /**
1006
1116
  * Attaches a listener for network connection state changes.
1007
1117
  */
@@ -1026,6 +1136,28 @@ var StateManager = class {
1026
1136
  detachWebRTCStateListener(fn) {
1027
1137
  this.webRTCStateListeners.delete(fn);
1028
1138
  }
1139
+ /**
1140
+ * Attaches a listener for messages received directly from a peer over a
1141
+ * WebRTC data channel (i.e. off-Mesh, peer-to-peer).
1142
+ */
1143
+ attachDirectMessageListener(fn) {
1144
+ this.directMessageListeners.add(fn);
1145
+ }
1146
+ /**
1147
+ * Detaches a previously registered direct-message listener.
1148
+ */
1149
+ detachDirectMessageListener(fn) {
1150
+ this.directMessageListeners.delete(fn);
1151
+ }
1152
+ /**
1153
+ * Emits a message received over a peer-to-peer WebRTC data channel to all
1154
+ * registered listeners.
1155
+ */
1156
+ emitDirectMessage(message) {
1157
+ for (const fn of this.directMessageListeners) {
1158
+ fn(message);
1159
+ }
1160
+ }
1029
1161
  /**
1030
1162
  * Emits a network connection state change to all registered listeners.
1031
1163
  */
@@ -1123,12 +1255,12 @@ var getMachineUID = async () => {
1123
1255
  if (typeof globalThis !== "object") {
1124
1256
  throw new Error("Flux can only be used in an environment with globalThis");
1125
1257
  }
1126
- if (globalThis.__flux_client_id !== null) {
1258
+ if (globalThis.__flux_client_id !== null && globalThis.__flux_client_id !== void 0) {
1127
1259
  console.log("[flux-client] Reusing existing id");
1128
1260
  return globalThis.__flux_client_id;
1129
1261
  }
1130
1262
  globalThis.__flux_client_id = null;
1131
- return globalThis.__flux_client_id;
1263
+ return null;
1132
1264
  };
1133
1265
 
1134
1266
  // packages/flux/agent/src/lib/connector/auth/register-client.auth.ts
@@ -1206,6 +1338,7 @@ var FluxClientData = class {
1206
1338
 
1207
1339
  // packages/flux/agent/src/lib/flux-agent.class.ts
1208
1340
  var DEFAULT_FLUX_DOMAIN = "https://mesh.persistica.io";
1341
+ var MAX_CONNECT_ATTEMPTS = 100;
1209
1342
  var FluxAgent = class {
1210
1343
  id = nanoid();
1211
1344
  networkId;
@@ -1235,6 +1368,7 @@ var FluxAgent = class {
1235
1368
  if (clientUId && !validateAgentUIDOrThrow(clientUId)) {
1236
1369
  throw new Error("Will never be thrown");
1237
1370
  }
1371
+ let warnedWaitingForAuthority = false;
1238
1372
  const ticket = await retryOrThrow(
1239
1373
  async () => {
1240
1374
  return authenticateAgentOrThrow(
@@ -1250,10 +1384,21 @@ var FluxAgent = class {
1250
1384
  // Retry if no authority was found yet
1251
1385
  (error) => error instanceof NetworkAuthorityNotFoundError,
1252
1386
  {
1253
- retries: 100,
1387
+ // Fixed by the SDK, not the consuming app: Agents run on untrusted client
1388
+ // machines, so the mesh operator — not the app — controls how long an Agent
1389
+ // waits for an Authority to appear.
1390
+ retries: MAX_CONNECT_ATTEMPTS,
1254
1391
  delayMs: 500,
1255
- // Backoff until 3 seconds
1256
- onRetry: (attempt) => Math.min(3e3, 500 + attempt * 200)
1392
+ onRetry: (attempt, retries) => {
1393
+ this.stateManager.emitNetworkState("waiting-for-authority");
1394
+ if (!warnedWaitingForAuthority) {
1395
+ warnedWaitingForAuthority = true;
1396
+ console.warn(
1397
+ `[flux-agent] No Authority is registered on network '${this.networkId}' yet. An Authority must be running before Agents can join; retrying up to ${retries} times until one appears. If this never resolves, start your Authority process (or check its Network Access Token).`
1398
+ );
1399
+ }
1400
+ return Math.min(3e3, 500 + attempt * 200);
1401
+ }
1257
1402
  }
1258
1403
  );
1259
1404
  this.fluxWebSocketConnection = createWSConnection(
@@ -1288,6 +1433,14 @@ var FluxAgent = class {
1288
1433
  * Attaches a listener for WebRTC connection state changes.
1289
1434
  */
1290
1435
  onWebRTConnectionState = this.stateManager.attachWebRTCStateListener.bind(this.stateManager);
1436
+ /**
1437
+ * Attaches a listener for messages received directly from a peer over a
1438
+ * WebRTC data channel (off-Mesh, peer-to-peer).
1439
+ *
1440
+ * NB: unlike channel messages, the sender identity is NOT Mesh-stamped
1441
+ * (see ADR-0004) — the message is whatever arrived on the peer connection.
1442
+ */
1443
+ onDirectPublish = this.stateManager.attachDirectMessageListener.bind(this.stateManager);
1291
1444
  /**
1292
1445
  * Attaches a listener for network connection state changes.
1293
1446
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistica/flux-agent",
3
- "version": "0.0.28",
3
+ "version": "0.0.35",
4
4
  "description": "Persistica Flux Agent",
5
5
  "type": "module",
6
6
  "keywords": [],
@@ -9,10 +9,18 @@ import type { FluxWebSocketClientConnection } from '@flux/shared/ws';
9
9
  export declare class ICEConnection {
10
10
  private readonly _fluxWebSocketClientConnection;
11
11
  private readonly _stateChange;
12
+ /**
13
+ * Called with every message received from the peer over the data channel.
14
+ */
15
+ private readonly _onMessage;
12
16
  private readonly peerConnection;
13
17
  private dataChannel;
14
18
  private offerSDP;
15
- constructor(_fluxWebSocketClientConnection: FluxWebSocketClientConnection, _stateChange: (state: TRTCState) => void);
19
+ constructor(_fluxWebSocketClientConnection: FluxWebSocketClientConnection, _stateChange: (state: TRTCState) => void,
20
+ /**
21
+ * Called with every message received from the peer over the data channel.
22
+ */
23
+ _onMessage: (message: string) => void);
16
24
  /**
17
25
  * This offer must be sent to the other peer.
18
26
  *
@@ -41,6 +49,14 @@ export declare class ICEConnection {
41
49
  * @param { boolean } answerAccepted - Whether the answer was accepted
42
50
  */
43
51
  private answerAcceptedByInitiator;
52
+ /**
53
+ * Wires up a data channel's lifecycle handlers. Used for both the initiator's
54
+ * locally-created channel and the receiver's incoming channel so inbound
55
+ * messages are delivered the same way on both ends.
56
+ *
57
+ * @param { RTCDataChannel } channel - The data channel to wire up
58
+ */
59
+ private _wireDataChannel;
44
60
  /**
45
61
  * Sends a message to the connected peer via the WebRTC data channel.
46
62
  *
@@ -30,6 +30,14 @@ export declare class FluxAgent {
30
30
  * Attaches a listener for WebRTC connection state changes.
31
31
  */
32
32
  onWebRTConnectionState: (fn: (rtcState: import("@flux/shared/utils").TRTCState) => void) => void;
33
+ /**
34
+ * Attaches a listener for messages received directly from a peer over a
35
+ * WebRTC data channel (off-Mesh, peer-to-peer).
36
+ *
37
+ * NB: unlike channel messages, the sender identity is NOT Mesh-stamped
38
+ * (see ADR-0004) — the message is whatever arrived on the peer connection.
39
+ */
40
+ onDirectPublish: (fn: (message: string) => void) => void;
33
41
  /**
34
42
  * Attaches a listener for network connection state changes.
35
43
  */