@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 +21 -15
- package/index.js +203 -56
- package/package.json +1 -1
- package/src/lib/connector/low-level-com/web-rtc/ice-connection.d.ts +17 -1
- package/src/lib/flux-agent.class.d.ts +8 -0
package/README.md
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @persistica/flux-agent
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
```bash
|
|
6
|
+
bun add @persistica/flux-agent # or: npm i @persistica/flux-agent
|
|
7
|
+
```
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
```ts
|
|
10
|
+
import { FluxAgent } from '@persistica/flux-agent';
|
|
9
11
|
|
|
10
|
-
|
|
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
|
-
|
|
14
|
+
// The argument is your claim — whatever your Authority accepts.
|
|
15
|
+
const connection = await agent.connect({ user: 'alice' });
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
178
|
+
const socket = new WebSocket(
|
|
167
179
|
this.options.url
|
|
168
180
|
);
|
|
181
|
+
this.ws = socket;
|
|
169
182
|
const timeout = setTimeout(() => {
|
|
170
|
-
if (
|
|
171
|
-
|
|
183
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
184
|
+
socket.close();
|
|
172
185
|
reject(new Error("Connection timeout"));
|
|
173
186
|
}
|
|
174
187
|
}, this.options.connectionTimeout);
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
const wasKicked = closeEvent.reason === "Kicked by process";
|
|
210
|
+
socket.onclose = (closeEvent) => {
|
|
187
211
|
clearTimeout(timeout);
|
|
188
|
-
if (this.
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
913
|
-
|
|
1010
|
+
const raw = message_.slice(thirdColon + 1);
|
|
1011
|
+
let data;
|
|
1012
|
+
if (raw.startsWith("o:")) {
|
|
914
1013
|
try {
|
|
915
|
-
data = JSON.parse(
|
|
1014
|
+
data = JSON.parse(raw.substring(2));
|
|
916
1015
|
} catch {
|
|
917
|
-
data =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
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
|
@@ -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
|
*/
|