@persistica/flux-agent 0.0.39 → 0.0.41

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 (2) hide show
  1. package/index.js +91 -31
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -703,6 +703,18 @@ var ChannelStateManager = class {
703
703
  }
704
704
  return Promise.reject(new Error("Not connected"));
705
705
  }
706
+ /**
707
+ * Re-subscribes every joined channel on a fresh socket. The mesh ties
708
+ * channel membership to the socket, so after a re-sign-on nothing is joined
709
+ * even though the channels — and their use counts — live on here unchanged.
710
+ * The mesh answers each with a subscribe ack nobody awaits, which
711
+ * `FluxWebSocketConnection.handleMessage` swallows.
712
+ */
713
+ resubscribeJoinedChannels(webSocketClient) {
714
+ for (const channelName of this.joinedChannels.keys()) {
715
+ webSocketClient.send(`${SUBSCRIBE_NETWORK_CHANNEL_NAME}:${channelName}`);
716
+ }
717
+ }
706
718
  /**
707
719
  * Leave a channel.
708
720
  */
@@ -780,20 +792,7 @@ var FluxWebSocketConnection = class {
780
792
  secretKey: this.options.secretKey,
781
793
  retries: this.options.retries ?? 1e4
782
794
  };
783
- const url = new URL(this.options.domain);
784
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
785
- url.searchParams.set("token", this.token);
786
- this.socket = new FluxWebSocketClientConnection(
787
- {
788
- url: url.toString(),
789
- // The URL carries a mesh-issued ticket that expires (15 minutes)
790
- // long before this connection does. Re-dialling it is pointless —
791
- // the mesh answers 'jwt expired' every time — so coming back is
792
- // `onReconnectCallback`'s job: it signs on again from scratch.
793
- autoReconnect: false,
794
- retries: this.options.retries
795
- }
796
- );
795
+ this.socket = this.createSocket(this.token);
797
796
  this.interceptPackageTypeMessages("isReady", this.readyInterceptor);
798
797
  }
799
798
  fluxInstanceId;
@@ -804,6 +803,9 @@ var FluxWebSocketConnection = class {
804
803
  // ****************************************************************************
805
804
  // *** State
806
805
  // ****************************************************************************
806
+ // Mutable: a re-sign-on builds a fresh socket — the ticket is baked into its
807
+ // URL — but keeps this connection object, so every handle and listener
808
+ // registered against it survives (#536).
807
809
  socket;
808
810
  callbacks = /* @__PURE__ */ new Set();
809
811
  channelCallbacks = /* @__PURE__ */ new Map();
@@ -813,6 +815,9 @@ var FluxWebSocketConnection = class {
813
815
  connectPromise;
814
816
  connectPromiseResolver;
815
817
  channelStateManager = new ChannelStateManager();
818
+ // Whether the authority-side channel-change subscription was requested. The
819
+ // mesh ties it to the socket, so a re-signed-on connection has to ask again.
820
+ subscribedToChannelChanges = false;
816
821
  // ****************************************************************************
817
822
  // *** Reused socket callbacks
818
823
  // ****************************************************************************
@@ -824,6 +829,27 @@ var FluxWebSocketConnection = class {
824
829
  socketConnectFailedHandler = () => this.handleSocketConnectFailed();
825
830
  socketConnectingHandler = (retryAttempt) => this.handleSocketConnecting(retryAttempt);
826
831
  socketErrorHandler = (error) => this.handleSocketError(error);
832
+ /**
833
+ * Builds the low-level socket. The URL embeds the single-use, expiring
834
+ * ticket, so every sign-on — the first one and every one after a drop —
835
+ * needs its own socket. `reconnect()` swaps the fresh one in.
836
+ */
837
+ createSocket(ticket) {
838
+ const url = new URL(this.options.domain);
839
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
840
+ url.searchParams.set("token", ticket);
841
+ return new FluxWebSocketClientConnection(
842
+ {
843
+ url: url.toString(),
844
+ // The URL carries a mesh-issued ticket that expires (15 minutes)
845
+ // long before this connection does. Re-dialling it is pointless —
846
+ // the mesh answers 'jwt expired' every time — so coming back is
847
+ // `onReconnectCallback`'s job: it signs on again from scratch.
848
+ autoReconnect: false,
849
+ retries: this.options.retries
850
+ }
851
+ );
852
+ }
827
853
  /**
828
854
  * Intercepts messages of a given package type.
829
855
  */
@@ -867,6 +893,23 @@ var FluxWebSocketConnection = class {
867
893
  this.socket.connect().catch(this.socketErrorHandler);
868
894
  return this.connectPromise;
869
895
  }
896
+ /**
897
+ * Signs on again with a fresh ticket, keeping this connection object — and
898
+ * with it every channel handle and listener registered before the drop.
899
+ *
900
+ * A sign-on used to build a whole new connection, whose empty channel state
901
+ * left the old handles pointing at a dead socket: `publish()` was a silent
902
+ * no-op and `onPublish` listeners never fired again (#536).
903
+ *
904
+ * Only swaps the socket in; the caller's next `connect()` dials it.
905
+ */
906
+ reconnect(ticket) {
907
+ this.socket.close();
908
+ this.webSocketClient = void 0;
909
+ this.connectPromise = void 0;
910
+ this.connectPromiseResolver = void 0;
911
+ this.socket = this.createSocket(ticket);
912
+ }
870
913
  /**
871
914
  * Disconnects from the Flux platform.
872
915
  */
@@ -929,6 +972,8 @@ var FluxWebSocketConnection = class {
929
972
  if (this.webSocketClient) {
930
973
  const messageString = typeof message === "string" ? `s:${message}` : `o:${JSON.stringify(message)}`;
931
974
  this.webSocketClient.send(`${NETWORK_CHANNEL_PUBLISH}:${channelName}:${messageString}`);
975
+ } else {
976
+ console.warn(`Cannot publish to channel "${channelName}": the connection is down. Message dropped.`);
932
977
  }
933
978
  }
934
979
  /**
@@ -966,6 +1011,7 @@ var FluxWebSocketConnection = class {
966
1011
  * Subscribes to the channel changes.
967
1012
  */
968
1013
  subscribeToChannelChanges() {
1014
+ this.subscribedToChannelChanges = true;
969
1015
  if (this.webSocketClient) {
970
1016
  this.webSocketClient.send(AUTHORITY_CHANNEL_SUBSCRIBE);
971
1017
  } else {
@@ -991,6 +1037,10 @@ var FluxWebSocketConnection = class {
991
1037
  resolveConnectedSocket() {
992
1038
  this.stateManager.emitNetworkState("connected");
993
1039
  this.webSocketClient = this.socket;
1040
+ this.channelStateManager.resubscribeJoinedChannels(this.socket);
1041
+ if (this.subscribedToChannelChanges) {
1042
+ this.socket.send(AUTHORITY_CHANNEL_SUBSCRIBE);
1043
+ }
994
1044
  const connectPromiseResolver = this.connectPromiseResolver;
995
1045
  this.connectPromiseResolver = void 0;
996
1046
  this.connectPromise = void 0;
@@ -1099,6 +1149,9 @@ var FluxWebSocketConnection = class {
1099
1149
  }
1100
1150
  return;
1101
1151
  }
1152
+ case SUBSCRIBED_NETWORK_CHANNEL_NAME: {
1153
+ return;
1154
+ }
1102
1155
  case RPC_REQUEST: {
1103
1156
  const payload = JSON.parse(message_.substring(message_.indexOf(":") + 1));
1104
1157
  void this.socket.handleMessage(payload, (data) => {
@@ -1330,6 +1383,7 @@ var EndpointNotFoundError = class extends Error {
1330
1383
  };
1331
1384
  var isRetryableConnectionError = (error) => error instanceof RetryableError;
1332
1385
  var isRetryableAuthStatus = (status) => status >= 500 || status === 429;
1386
+ var AUTH_FETCH_TIMEOUT_MS = 6e4;
1333
1387
  var asConnectionError = (error, origin) => new ConnectionError(
1334
1388
  `Failed to reach mesh server at ${origin}: ${error instanceof Error ? error.message : "Unknown connection error"}`
1335
1389
  );
@@ -1400,7 +1454,9 @@ var authenticateAgentOrThrow = async (networkId, domain, unknownIdentificationPa
1400
1454
  "x-flux-content-type": typeof unknownIdentificationPayload === "string" ? "text/plain" : "application/json",
1401
1455
  "Accept": "text/plain"
1402
1456
  },
1403
- body: customPayload
1457
+ body: customPayload,
1458
+ // A timeout rejects like any transport failure, so the caller retries it (#541).
1459
+ signal: AbortSignal.timeout(AUTH_FETCH_TIMEOUT_MS)
1404
1460
  });
1405
1461
  if (!response.ok) {
1406
1462
  const textResponse = await response.text();
@@ -1534,22 +1590,26 @@ var FluxAgent = class {
1534
1590
  }
1535
1591
  }
1536
1592
  );
1537
- this.fluxWebSocketConnection = createWSConnection(
1538
- this.id,
1539
- ticket,
1540
- this.stateManager,
1541
- async () => {
1542
- await this.connect(
1543
- identification,
1544
- clientUId
1545
- );
1546
- },
1547
- {
1548
- ...this.options,
1549
- domain
1550
- }
1551
- );
1552
- this.fluxClientData.updateWsConnection(this.fluxWebSocketConnection);
1593
+ if (this.fluxWebSocketConnection) {
1594
+ this.fluxWebSocketConnection.reconnect(ticket);
1595
+ } else {
1596
+ this.fluxWebSocketConnection = createWSConnection(
1597
+ this.id,
1598
+ ticket,
1599
+ this.stateManager,
1600
+ async () => {
1601
+ await this.connect(
1602
+ identification,
1603
+ clientUId
1604
+ );
1605
+ },
1606
+ {
1607
+ ...this.options,
1608
+ domain
1609
+ }
1610
+ );
1611
+ this.fluxClientData.updateWsConnection(this.fluxWebSocketConnection);
1612
+ }
1553
1613
  return await this.fluxWebSocketConnection.connectToNetwork();
1554
1614
  } catch (error) {
1555
1615
  this.stateManager.emitNetworkState("auth-error");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistica/flux-agent",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "Persistica Flux Agent",
5
5
  "type": "module",
6
6
  "keywords": [],