@persistica/flux-agent 0.0.27 → 0.0.34

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,6 +149,15 @@ 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 = {
@@ -162,50 +173,48 @@ var WebSocketClient = class extends RPCServer {
162
173
  */
163
174
  connect() {
164
175
  return new Promise((resolve, reject) => {
176
+ this.closeHandled = false;
165
177
  this.emit("connecting", this.reconnectAttempts);
166
- this.ws = new WebSocket(
178
+ const socket = new WebSocket(
167
179
  this.options.url
168
180
  );
181
+ this.ws = socket;
169
182
  const timeout = setTimeout(() => {
170
- if (this.ws?.readyState !== WebSocket.OPEN) {
171
- this.ws?.close();
183
+ if (socket.readyState !== WebSocket.OPEN) {
184
+ socket.close();
172
185
  reject(new Error("Connection timeout"));
173
186
  }
174
187
  }, this.options.connectionTimeout);
175
- this.ws.onopen = () => {
188
+ socket.onopen = () => {
176
189
  clearTimeout(timeout);
190
+ if (this.ws !== socket) {
191
+ return;
192
+ }
177
193
  this.reconnectAttempts = 0;
178
194
  this.isOpen = true;
195
+ this.startHeartbeat();
179
196
  this.emit("open");
180
197
  resolve();
181
198
  };
182
- this.ws.onmessage = (event) => {
199
+ socket.onmessage = (event) => {
200
+ if (this.ws !== socket) {
201
+ return;
202
+ }
203
+ if (event.data === HEARTBEAT_PONG) {
204
+ this.awaitingPong = false;
205
+ this.pongSeen = true;
206
+ return;
207
+ }
183
208
  this.emit("message", event.data);
184
209
  };
185
- this.ws.onclose = (closeEvent) => {
186
- const wasKicked = closeEvent.reason === "Kicked by process";
210
+ socket.onclose = (closeEvent) => {
187
211
  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"));
212
+ if (this.ws !== socket) {
213
+ return;
206
214
  }
215
+ this.handleConnectionClosed(closeEvent.reason === "Kicked by process");
207
216
  };
208
- this.ws.onerror = (event) => {
217
+ socket.onerror = (event) => {
209
218
  if (event?.message?.includes("Failed to connect")) {
210
219
  } else {
211
220
  console.log("\u274C Error", event.message);
@@ -213,6 +222,79 @@ var WebSocketClient = class extends RPCServer {
213
222
  };
214
223
  });
215
224
  }
225
+ /**
226
+ * The single close path, reachable from `onclose` AND from a missed heartbeat
227
+ * pong (where `onclose` never fires): emit `close` once, then schedule the
228
+ * reconnect per the retry policy.
229
+ */
230
+ handleConnectionClosed(wasKicked) {
231
+ if (this.closeHandled) {
232
+ return;
233
+ }
234
+ this.closeHandled = true;
235
+ this.stopHeartbeat();
236
+ if (this.isOpen) {
237
+ this.emit(
238
+ "close",
239
+ wasKicked ? "kicked" : void 0
240
+ );
241
+ this.isOpen = false;
242
+ }
243
+ if (this.options.autoReconnect && (this.options.retries === void 0 || this.reconnectAttempts < this.options.retries)) {
244
+ const delay = wasKicked ? RECONNECTION_DELAY_ON_KICK_MS : Math.min(
245
+ this.options.reconnectDelay ?? Number.POSITIVE_INFINITY,
246
+ (this.options.reconnectDelay ?? 0) * (this.reconnectAttempts || 1)
247
+ );
248
+ setTimeout(() => {
249
+ this.reconnectAttempts++;
250
+ void this.connect();
251
+ }, delay);
252
+ } else if (this.options.autoReconnect) {
253
+ this.emit("error", new Error("Connection failed: retries exhausted"));
254
+ }
255
+ }
256
+ /**
257
+ * Starts the keepalive: every `heartbeatInterval` send an app-level ping (the
258
+ * browser/Bun `WebSocket` exposes no ping frame) and require the mesh's pong
259
+ * before the next tick. A missed pong means the socket is dead — force the
260
+ * close path directly, since a half-open socket produces no `onclose`.
261
+ */
262
+ startHeartbeat() {
263
+ this.stopHeartbeat();
264
+ this.pongSeen = false;
265
+ const interval = this.options.heartbeatInterval;
266
+ if (interval === void 0 || interval <= 0) {
267
+ return;
268
+ }
269
+ this.heartbeatTimer = setInterval(() => {
270
+ if (this.awaitingPong) {
271
+ if (!this.pongSeen) {
272
+ console.warn("\u{1F49B} Server does not answer heartbeat pings \u2014 dead-connection detection disabled for this connection");
273
+ this.stopHeartbeat();
274
+ return;
275
+ }
276
+ console.warn(`\u{1F494} No heartbeat pong within ${interval}ms \u2014 treating the connection as dead`);
277
+ this.stopHeartbeat();
278
+ try {
279
+ this.ws?.close(4e3, "Heartbeat timeout");
280
+ } catch {
281
+ }
282
+ this.handleConnectionClosed(false);
283
+ return;
284
+ }
285
+ if (this.ws?.readyState === WebSocket.OPEN) {
286
+ this.awaitingPong = true;
287
+ this.ws.send(HEARTBEAT_PING);
288
+ }
289
+ }, interval);
290
+ }
291
+ stopHeartbeat() {
292
+ if (this.heartbeatTimer !== void 0) {
293
+ clearInterval(this.heartbeatTimer);
294
+ this.heartbeatTimer = void 0;
295
+ }
296
+ this.awaitingPong = false;
297
+ }
216
298
  /**
217
299
  * Emits a WebSocket event to all registered listeners.
218
300
  */
@@ -253,6 +335,7 @@ var WebSocketClient = class extends RPCServer {
253
335
  */
254
336
  close() {
255
337
  this.options.autoReconnect = false;
338
+ this.stopHeartbeat();
256
339
  this.ws?.close();
257
340
  this.clearEventSubscribers();
258
341
  }
@@ -290,22 +373,12 @@ var peerConnectionConfig = {
290
373
  ]
291
374
  };
292
375
  var ICEConnection = class {
293
- constructor(_fluxWebSocketClientConnection, _stateChange) {
376
+ constructor(_fluxWebSocketClientConnection, _stateChange, _onMessage) {
294
377
  this._fluxWebSocketClientConnection = _fluxWebSocketClientConnection;
295
378
  this._stateChange = _stateChange;
379
+ this._onMessage = _onMessage;
296
380
  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
- };
381
+ this._wireDataChannel(event.channel);
309
382
  };
310
383
  this.peerConnection.onicecandidate = (event) => {
311
384
  if (event.candidate) {
@@ -323,10 +396,10 @@ var ICEConnection = class {
323
396
  }
324
397
  _fluxWebSocketClientConnection;
325
398
  _stateChange;
399
+ _onMessage;
326
400
  //extends RPCServer<'createOffer' | 'acceptOffer' | 'acceptAnswer'> {
327
401
  peerConnection = new RTCPeerConnection(peerConnectionConfig);
328
402
  dataChannel;
329
- // = this.peerConnection.createDataChannel('flux-channel');
330
403
  offerSDP;
331
404
  /**
332
405
  * This offer must be sent to the other peer.
@@ -335,7 +408,7 @@ var ICEConnection = class {
335
408
  */
336
409
  async createOffer() {
337
410
  console.log("creating offer and data channel");
338
- this.dataChannel = this.peerConnection.createDataChannel("flux-channel");
411
+ this._wireDataChannel(this.peerConnection.createDataChannel("flux-channel"));
339
412
  this._stateChange("creating-offer");
340
413
  const offer = await this.peerConnection.createOffer();
341
414
  await this.peerConnection.setLocalDescription(offer);
@@ -380,9 +453,28 @@ var ICEConnection = class {
380
453
  answerAcceptedByInitiator(answerAccepted) {
381
454
  this._stateChange(answerAccepted ? "connected" : "failed");
382
455
  }
456
+ /**
457
+ * Wires up a data channel's lifecycle handlers. Used for both the initiator's
458
+ * locally-created channel and the receiver's incoming channel so inbound
459
+ * messages are delivered the same way on both ends.
460
+ *
461
+ * @param { RTCDataChannel } channel - The data channel to wire up
462
+ */
463
+ _wireDataChannel(channel) {
464
+ this.dataChannel = channel;
465
+ channel.onopen = () => {
466
+ console.log("Data channel opened");
467
+ };
468
+ channel.onclose = () => {
469
+ console.log("Data channel closed");
470
+ };
471
+ channel.onmessage = (event) => {
472
+ this._onMessage(event.data);
473
+ };
474
+ }
383
475
  /**
384
476
  * Sends a message to the connected peer via the WebRTC data channel.
385
- *
477
+ *
386
478
  * @param { string } message - The message to send
387
479
  */
388
480
  sendMessage(message) {
@@ -397,14 +489,16 @@ var ICEConnection = class {
397
489
 
398
490
  // libs/flux/shared/connection/src/lib/agent/flux-agent-network.class.ts
399
491
  var FluxAgentNetworkConnection = class {
400
- constructor(_fluxWebSocketConnection, _fluxWebSocketClientConnection, _webRTCStateChange) {
492
+ constructor(_fluxWebSocketConnection, _fluxWebSocketClientConnection, _webRTCStateChange, _onDirectMessage) {
401
493
  this._fluxWebSocketConnection = _fluxWebSocketConnection;
402
494
  this._fluxWebSocketClientConnection = _fluxWebSocketClientConnection;
403
495
  this._webRTCStateChange = _webRTCStateChange;
496
+ this._onDirectMessage = _onDirectMessage;
404
497
  if (typeof RTCPeerConnection !== "undefined") {
405
498
  this.iceConnection = new ICEConnection(
406
499
  this._fluxWebSocketClientConnection,
407
- this._webRTCStateChange
500
+ this._webRTCStateChange,
501
+ this._onDirectMessage
408
502
  );
409
503
  console.log(`RTCPeerConnection is available`);
410
504
  } else {
@@ -414,6 +508,7 @@ var FluxAgentNetworkConnection = class {
414
508
  _fluxWebSocketConnection;
415
509
  _fluxWebSocketClientConnection;
416
510
  _webRTCStateChange;
511
+ _onDirectMessage;
417
512
  iceConnection;
418
513
  connectedChannelSet = /* @__PURE__ */ new Set();
419
514
  /**
@@ -514,8 +609,10 @@ var FluxNetworkChannel = class {
514
609
  onPublish(fn) {
515
610
  this._fluxWebSocketConnection.onPublish(
516
611
  this._channelName,
612
+ // The transport types payloads as `string` (TMessageCallback), but
613
+ // `o:`-prefixed messages are JSON-parsed before delivery, so the
614
+ // callback actually receives `T`. Bridge the two deliberately.
517
615
  fn
518
- // TODO
519
616
  );
520
617
  }
521
618
  };
@@ -768,7 +865,8 @@ var FluxWebSocketConnection = class {
768
865
  return Promise.resolve(new FluxAgentNetworkConnection(
769
866
  this,
770
867
  webSocketClient,
771
- this.stateManager.emitWebRTCState.bind(this.stateManager)
868
+ this.stateManager.emitWebRTCState.bind(this.stateManager),
869
+ this.stateManager.emitDirectMessage.bind(this.stateManager)
772
870
  ));
773
871
  }
774
872
  /**
@@ -796,7 +894,7 @@ var FluxWebSocketConnection = class {
796
894
  */
797
895
  publish(channelName, message) {
798
896
  if (this.webSocketClient) {
799
- const messageString = typeof message === "string" ? message : `o:${JSON.stringify(message)}`;
897
+ const messageString = typeof message === "string" ? `s:${message}` : `o:${JSON.stringify(message)}`;
800
898
  this.webSocketClient.send(`${NETWORK_CHANNEL_PUBLISH}:${channelName}:${messageString}`);
801
899
  }
802
900
  }
@@ -909,13 +1007,18 @@ var FluxWebSocketConnection = class {
909
1007
  if (validateChannelNameOrThrow(channelName)) {
910
1008
  const topicCallbacks = this.channelCallbacks.get(channelName);
911
1009
  if (topicCallbacks) {
912
- let data = message_.slice(thirdColon + 1);
913
- if (data.startsWith("o:")) {
1010
+ const raw = message_.slice(thirdColon + 1);
1011
+ let data;
1012
+ if (raw.startsWith("o:")) {
914
1013
  try {
915
- data = JSON.parse(data.substring(2));
1014
+ data = JSON.parse(raw.substring(2));
916
1015
  } catch {
917
- data = data.substring(2);
1016
+ data = raw.substring(2);
918
1017
  }
1018
+ } else if (raw.startsWith("s:")) {
1019
+ data = raw.substring(2);
1020
+ } else {
1021
+ data = raw;
919
1022
  }
920
1023
  for (const cb of topicCallbacks) {
921
1024
  cb(data);
@@ -1002,6 +1105,7 @@ async function encrypt(data, password) {
1002
1105
  var StateManager = class {
1003
1106
  networkStateListeners = /* @__PURE__ */ new Set();
1004
1107
  webRTCStateListeners = /* @__PURE__ */ new Set();
1108
+ directMessageListeners = /* @__PURE__ */ new Set();
1005
1109
  /**
1006
1110
  * Attaches a listener for network connection state changes.
1007
1111
  */
@@ -1026,6 +1130,28 @@ var StateManager = class {
1026
1130
  detachWebRTCStateListener(fn) {
1027
1131
  this.webRTCStateListeners.delete(fn);
1028
1132
  }
1133
+ /**
1134
+ * Attaches a listener for messages received directly from a peer over a
1135
+ * WebRTC data channel (i.e. off-Mesh, peer-to-peer).
1136
+ */
1137
+ attachDirectMessageListener(fn) {
1138
+ this.directMessageListeners.add(fn);
1139
+ }
1140
+ /**
1141
+ * Detaches a previously registered direct-message listener.
1142
+ */
1143
+ detachDirectMessageListener(fn) {
1144
+ this.directMessageListeners.delete(fn);
1145
+ }
1146
+ /**
1147
+ * Emits a message received over a peer-to-peer WebRTC data channel to all
1148
+ * registered listeners.
1149
+ */
1150
+ emitDirectMessage(message) {
1151
+ for (const fn of this.directMessageListeners) {
1152
+ fn(message);
1153
+ }
1154
+ }
1029
1155
  /**
1030
1156
  * Emits a network connection state change to all registered listeners.
1031
1157
  */
@@ -1123,12 +1249,12 @@ var getMachineUID = async () => {
1123
1249
  if (typeof globalThis !== "object") {
1124
1250
  throw new Error("Flux can only be used in an environment with globalThis");
1125
1251
  }
1126
- if (globalThis.__flux_client_id !== null) {
1252
+ if (globalThis.__flux_client_id !== null && globalThis.__flux_client_id !== void 0) {
1127
1253
  console.log("[flux-client] Reusing existing id");
1128
1254
  return globalThis.__flux_client_id;
1129
1255
  }
1130
1256
  globalThis.__flux_client_id = null;
1131
- return globalThis.__flux_client_id;
1257
+ return null;
1132
1258
  };
1133
1259
 
1134
1260
  // packages/flux/agent/src/lib/connector/auth/register-client.auth.ts
@@ -1206,6 +1332,7 @@ var FluxClientData = class {
1206
1332
 
1207
1333
  // packages/flux/agent/src/lib/flux-agent.class.ts
1208
1334
  var DEFAULT_FLUX_DOMAIN = "https://mesh.persistica.io";
1335
+ var MAX_CONNECT_ATTEMPTS = 100;
1209
1336
  var FluxAgent = class {
1210
1337
  id = nanoid();
1211
1338
  networkId;
@@ -1235,6 +1362,7 @@ var FluxAgent = class {
1235
1362
  if (clientUId && !validateAgentUIDOrThrow(clientUId)) {
1236
1363
  throw new Error("Will never be thrown");
1237
1364
  }
1365
+ let warnedWaitingForAuthority = false;
1238
1366
  const ticket = await retryOrThrow(
1239
1367
  async () => {
1240
1368
  return authenticateAgentOrThrow(
@@ -1250,10 +1378,21 @@ var FluxAgent = class {
1250
1378
  // Retry if no authority was found yet
1251
1379
  (error) => error instanceof NetworkAuthorityNotFoundError,
1252
1380
  {
1253
- retries: 100,
1381
+ // Fixed by the SDK, not the consuming app: Agents run on untrusted client
1382
+ // machines, so the mesh operator — not the app — controls how long an Agent
1383
+ // waits for an Authority to appear.
1384
+ retries: MAX_CONNECT_ATTEMPTS,
1254
1385
  delayMs: 500,
1255
- // Backoff until 3 seconds
1256
- onRetry: (attempt) => Math.min(3e3, 500 + attempt * 200)
1386
+ onRetry: (attempt, retries) => {
1387
+ this.stateManager.emitNetworkState("waiting-for-authority");
1388
+ if (!warnedWaitingForAuthority) {
1389
+ warnedWaitingForAuthority = true;
1390
+ console.warn(
1391
+ `[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).`
1392
+ );
1393
+ }
1394
+ return Math.min(3e3, 500 + attempt * 200);
1395
+ }
1257
1396
  }
1258
1397
  );
1259
1398
  this.fluxWebSocketConnection = createWSConnection(
@@ -1288,6 +1427,14 @@ var FluxAgent = class {
1288
1427
  * Attaches a listener for WebRTC connection state changes.
1289
1428
  */
1290
1429
  onWebRTConnectionState = this.stateManager.attachWebRTCStateListener.bind(this.stateManager);
1430
+ /**
1431
+ * Attaches a listener for messages received directly from a peer over a
1432
+ * WebRTC data channel (off-Mesh, peer-to-peer).
1433
+ *
1434
+ * NB: unlike channel messages, the sender identity is NOT Mesh-stamped
1435
+ * (see ADR-0004) — the message is whatever arrived on the peer connection.
1436
+ */
1437
+ onDirectPublish = this.stateManager.attachDirectMessageListener.bind(this.stateManager);
1291
1438
  /**
1292
1439
  * Attaches a listener for network connection state changes.
1293
1440
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistica/flux-agent",
3
- "version": "0.0.27",
3
+ "version": "0.0.34",
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
  */