@persistica/flux-agent 0.0.35 → 0.0.37

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 +110 -34
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -726,6 +726,7 @@ var ChannelStateManager = class {
726
726
  };
727
727
 
728
728
  // libs/flux/shared/connection/src/lib/flux-ws-connection.ts
729
+ var RECONNECT_DELAY_MS = 2e3;
729
730
  var createWSConnection = (id, ticket, stateManager, onReconnectCallback, options) => {
730
731
  return new FluxWebSocketConnection(
731
732
  id,
@@ -757,8 +758,11 @@ var FluxWebSocketConnection = class {
757
758
  this.socket = new FluxWebSocketClientConnection(
758
759
  {
759
760
  url: url.toString(),
760
- autoReconnect: true,
761
- reconnectDelay: 2e3,
761
+ // The URL carries a mesh-issued ticket that expires (15 minutes)
762
+ // long before this connection does. Re-dialling it is pointless —
763
+ // the mesh answers 'jwt expired' every time — so coming back is
764
+ // `onReconnectCallback`'s job: it signs on again from scratch.
765
+ autoReconnect: false,
762
766
  retries: this.options.retries
763
767
  }
764
768
  );
@@ -780,18 +784,12 @@ var FluxWebSocketConnection = class {
780
784
  webSocketClient;
781
785
  connectPromise;
782
786
  connectPromiseResolver;
783
- first = true;
784
787
  channelStateManager = new ChannelStateManager();
785
788
  // ****************************************************************************
786
789
  // *** Reused socket callbacks
787
790
  // ****************************************************************************
788
791
  readyInterceptor = () => {
789
792
  this.resolveConnectedSocket();
790
- if (this.first) {
791
- this.first = false;
792
- return;
793
- }
794
- this.onReconnectCallback();
795
793
  };
796
794
  socketMessageHandler = this.handleMessage.bind(this);
797
795
  socketCloseHandler = (reason) => this.handleSocketClose(reason);
@@ -976,6 +974,17 @@ var FluxWebSocketConnection = class {
976
974
  this.fluxInstanceId
977
975
  );
978
976
  this.stateManager.emitNetworkState(reason === "kicked" ? "kicked" : "disconnected");
977
+ setTimeout(
978
+ () => {
979
+ Promise.resolve(this.onReconnectCallback()).catch((error) => {
980
+ PicoLogger.error(
981
+ `Failed to sign on again: ${error instanceof Error ? error.message : String(error)}`,
982
+ this.fluxInstanceId
983
+ );
984
+ });
985
+ },
986
+ reason === "kicked" ? RECONNECTION_DELAY_ON_KICK_MS : RECONNECT_DELAY_MS
987
+ );
979
988
  }
980
989
  handleSocketConnecting(retryAttempt) {
981
990
  this.stateManager.emitNetworkState("connecting");
@@ -1223,8 +1232,12 @@ var retryOrThrow = async (fn, shouldRetry, options) => {
1223
1232
  if (attempt >= options.retries || !shouldRetry(err)) {
1224
1233
  throw err;
1225
1234
  }
1226
- if (options.delayMs > 0 || overrideDelay && overrideDelay > 0) {
1227
- const delay = overrideDelay ?? options.delayMs;
1235
+ const backedOff = Math.min(
1236
+ options.maxDelayMs ?? Number.POSITIVE_INFINITY,
1237
+ options.delayMs * (options.backoffFactor ?? 1) ** attempt
1238
+ );
1239
+ const delay = overrideDelay ?? backedOff;
1240
+ if (delay > 0) {
1228
1241
  await new Promise((res) => setTimeout(
1229
1242
  res,
1230
1243
  delay
@@ -1235,6 +1248,35 @@ var retryOrThrow = async (fn, shouldRetry, options) => {
1235
1248
  throw new Error("Retry loop exited unexpectedly.");
1236
1249
  };
1237
1250
 
1251
+ // libs/flux/shared/utils/src/lib/connection-errors.ts
1252
+ var RetryableError = class extends Error {
1253
+ };
1254
+ var ConnectionError = class extends RetryableError {
1255
+ constructor(message, statusCode) {
1256
+ super(message);
1257
+ this.statusCode = statusCode;
1258
+ this.name = "ConnectionError";
1259
+ }
1260
+ statusCode;
1261
+ };
1262
+ var AuthenticationError = class extends Error {
1263
+ constructor(message) {
1264
+ super(message);
1265
+ this.name = "AuthenticationError";
1266
+ }
1267
+ };
1268
+ var EndpointNotFoundError = class extends Error {
1269
+ constructor(message) {
1270
+ super(message);
1271
+ this.name = "EndpointNotFoundError";
1272
+ }
1273
+ };
1274
+ var isRetryableConnectionError = (error) => error instanceof RetryableError;
1275
+ var isRetryableAuthStatus = (status) => status >= 500 || status === 429;
1276
+ var asConnectionError = (error, origin) => new ConnectionError(
1277
+ `Failed to reach mesh server at ${origin}: ${error instanceof Error ? error.message : "Unknown connection error"}`
1278
+ );
1279
+
1238
1280
  // libs/flux/shared/utils/src/lib/machine-id.util.ts
1239
1281
  var validateMachineUID = (machineUID) => {
1240
1282
  if (typeof machineUID !== "string") {
@@ -1293,32 +1335,47 @@ var authenticateAgentOrThrow = async (networkId, domain, unknownIdentificationPa
1293
1335
  if (clientInfo.machineUID && validateMachineUID(clientInfo.machineUID)) {
1294
1336
  url.searchParams.set("machineUID", clientInfo.machineUID);
1295
1337
  }
1296
- const response = await fetch(url, {
1297
- method: "POST",
1298
- headers: {
1299
- "Content-Type": "text/plain",
1300
- "x-flux-content-type": typeof unknownIdentificationPayload === "string" ? "text/plain" : "application/json",
1301
- "Accept": "text/plain"
1302
- },
1303
- body: customPayload
1304
- });
1305
- if (!response.ok) {
1306
- const textResponse = await response.text();
1307
- if (response.status === 401) {
1308
- if (textResponse.startsWith(NetworkAuthorityNotFoundError.message)) {
1309
- throw new NetworkAuthorityNotFoundError(networkId);
1338
+ try {
1339
+ const response = await fetch(url, {
1340
+ method: "POST",
1341
+ headers: {
1342
+ "Content-Type": "text/plain",
1343
+ "x-flux-content-type": typeof unknownIdentificationPayload === "string" ? "text/plain" : "application/json",
1344
+ "Accept": "text/plain"
1345
+ },
1346
+ body: customPayload
1347
+ });
1348
+ if (!response.ok) {
1349
+ const textResponse = await response.text();
1350
+ if (response.status === 401) {
1351
+ if (textResponse.startsWith(NetworkAuthorityNotFoundError.message)) {
1352
+ throw new NetworkAuthorityNotFoundError(networkId);
1353
+ }
1354
+ throw new AuthenticationError(textResponse ? `Auth failed: 401 - ${textResponse}` : "Auth failed: 401");
1355
+ }
1356
+ if (response.status === 404) {
1357
+ throw new EndpointNotFoundError(`Mesh server not found at ${url.origin}.`);
1358
+ }
1359
+ const message = textResponse ? `Auth failed: ${response.status} - ${textResponse}` : `Auth failed: ${response.status}`;
1360
+ if (isRetryableAuthStatus(response.status)) {
1361
+ throw new ConnectionError(message, response.status);
1310
1362
  }
1363
+ throw new Error(message);
1311
1364
  }
1312
- if (response.status === 404) {
1313
- throw new Error(`Mesh server not found at ${url.origin}.`);
1365
+ const result = await response.text();
1366
+ if (!checkAuthTicketShape(result)) {
1367
+ throw new Error("Auth failed: unexpected response");
1314
1368
  }
1315
- throw new Error(textResponse ? `Auth failed: ${response.status} - ${textResponse}` : `Auth failed: ${response.status}`);
1316
- }
1317
- const result = await response.text();
1318
- if (!checkAuthTicketShape(result)) {
1319
- throw new Error("Auth failed: unexpected response");
1369
+ return result;
1370
+ } catch (error) {
1371
+ if (error instanceof NetworkAuthorityNotFoundError || error instanceof AuthenticationError || error instanceof EndpointNotFoundError || error instanceof RetryableError) {
1372
+ throw error;
1373
+ }
1374
+ if (error instanceof Error && error.message.startsWith("Auth failed:")) {
1375
+ throw error;
1376
+ }
1377
+ throw asConnectionError(error, url.origin);
1320
1378
  }
1321
- return result;
1322
1379
  };
1323
1380
 
1324
1381
  // packages/flux/agent/src/lib/connector/flux-client-data.class.ts
@@ -1369,6 +1426,8 @@ var FluxAgent = class {
1369
1426
  throw new Error("Will never be thrown");
1370
1427
  }
1371
1428
  let warnedWaitingForAuthority = false;
1429
+ let warnedMeshUnreachable = false;
1430
+ let lastError;
1372
1431
  const ticket = await retryOrThrow(
1373
1432
  async () => {
1374
1433
  return authenticateAgentOrThrow(
@@ -1381,8 +1440,15 @@ var FluxAgent = class {
1381
1440
  }
1382
1441
  );
1383
1442
  },
1384
- // Retry if no authority was found yet
1385
- (error) => error instanceof NetworkAuthorityNotFoundError,
1443
+ // Retry while no Authority has appeared yet, and while the mesh
1444
+ // itself is unreachable. The latter used to fall straight through
1445
+ // to the caller, so a network blip during startup left the Agent
1446
+ // permanently disconnected while an Authority in the same
1447
+ // situation would have retried through it.
1448
+ (error) => {
1449
+ lastError = error;
1450
+ return error instanceof NetworkAuthorityNotFoundError || isRetryableConnectionError(error);
1451
+ },
1386
1452
  {
1387
1453
  // Fixed by the SDK, not the consuming app: Agents run on untrusted client
1388
1454
  // machines, so the mesh operator — not the app — controls how long an Agent
@@ -1390,6 +1456,16 @@ var FluxAgent = class {
1390
1456
  retries: MAX_CONNECT_ATTEMPTS,
1391
1457
  delayMs: 500,
1392
1458
  onRetry: (attempt, retries) => {
1459
+ if (isRetryableConnectionError(lastError)) {
1460
+ this.stateManager.emitNetworkState("authorizing");
1461
+ if (!warnedMeshUnreachable) {
1462
+ warnedMeshUnreachable = true;
1463
+ console.warn(
1464
+ `[flux-agent] Cannot reach the mesh at '${domain}'. Retrying in the background up to ${retries} times; the Agent connects on its own once the mesh answers. If this never resolves, check the 'domain' option points at the Mesh (not the Portal).`
1465
+ );
1466
+ }
1467
+ return Math.min(3e4, 500 * 2 ** attempt);
1468
+ }
1393
1469
  this.stateManager.emitNetworkState("waiting-for-authority");
1394
1470
  if (!warnedWaitingForAuthority) {
1395
1471
  warnedWaitingForAuthority = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistica/flux-agent",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "Persistica Flux Agent",
5
5
  "type": "module",
6
6
  "keywords": [],