@persistica/flux-agent 0.0.34 → 0.0.36

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 +103 -30
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -163,6 +163,7 @@ var WebSocketClient = class extends RPCServer {
163
163
  this.options = {
164
164
  autoReconnect: true,
165
165
  reconnectDelay: 1e3,
166
+ maxReconnectDelay: 3e4,
166
167
  heartbeatInterval: 6e4,
167
168
  connectionTimeout: 5e3,
168
169
  ...options
@@ -242,12 +243,17 @@ var WebSocketClient = class extends RPCServer {
242
243
  }
243
244
  if (this.options.autoReconnect && (this.options.retries === void 0 || this.reconnectAttempts < this.options.retries)) {
244
245
  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)
246
+ this.options.maxReconnectDelay ?? Number.POSITIVE_INFINITY,
247
+ (this.options.reconnectDelay ?? 0) * 2 ** this.reconnectAttempts
247
248
  );
248
249
  setTimeout(() => {
249
250
  this.reconnectAttempts++;
250
- void this.connect();
251
+ this.connect().catch((error) => {
252
+ this.emit(
253
+ "error",
254
+ error instanceof Error ? error : new Error(String(error))
255
+ );
256
+ });
251
257
  }, delay);
252
258
  } else if (this.options.autoReconnect) {
253
259
  this.emit("error", new Error("Connection failed: retries exhausted"));
@@ -831,7 +837,7 @@ var FluxWebSocketConnection = class {
831
837
  });
832
838
  this.socket.clearEventSubscribers();
833
839
  this.socket.on("message", this.socketMessageHandler).on("close", this.socketCloseHandler).on("connecting", this.socketConnectingHandler).on("error", this.socketErrorHandler);
834
- void this.socket.connect();
840
+ this.socket.connect().catch(this.socketErrorHandler);
835
841
  return this.connectPromise;
836
842
  }
837
843
  /**
@@ -1217,8 +1223,12 @@ var retryOrThrow = async (fn, shouldRetry, options) => {
1217
1223
  if (attempt >= options.retries || !shouldRetry(err)) {
1218
1224
  throw err;
1219
1225
  }
1220
- if (options.delayMs > 0 || overrideDelay && overrideDelay > 0) {
1221
- const delay = overrideDelay ?? options.delayMs;
1226
+ const backedOff = Math.min(
1227
+ options.maxDelayMs ?? Number.POSITIVE_INFINITY,
1228
+ options.delayMs * (options.backoffFactor ?? 1) ** attempt
1229
+ );
1230
+ const delay = overrideDelay ?? backedOff;
1231
+ if (delay > 0) {
1222
1232
  await new Promise((res) => setTimeout(
1223
1233
  res,
1224
1234
  delay
@@ -1229,6 +1239,35 @@ var retryOrThrow = async (fn, shouldRetry, options) => {
1229
1239
  throw new Error("Retry loop exited unexpectedly.");
1230
1240
  };
1231
1241
 
1242
+ // libs/flux/shared/utils/src/lib/connection-errors.ts
1243
+ var RetryableError = class extends Error {
1244
+ };
1245
+ var ConnectionError = class extends RetryableError {
1246
+ constructor(message, statusCode) {
1247
+ super(message);
1248
+ this.statusCode = statusCode;
1249
+ this.name = "ConnectionError";
1250
+ }
1251
+ statusCode;
1252
+ };
1253
+ var AuthenticationError = class extends Error {
1254
+ constructor(message) {
1255
+ super(message);
1256
+ this.name = "AuthenticationError";
1257
+ }
1258
+ };
1259
+ var EndpointNotFoundError = class extends Error {
1260
+ constructor(message) {
1261
+ super(message);
1262
+ this.name = "EndpointNotFoundError";
1263
+ }
1264
+ };
1265
+ var isRetryableConnectionError = (error) => error instanceof RetryableError;
1266
+ var isRetryableAuthStatus = (status) => status >= 500 || status === 429;
1267
+ var asConnectionError = (error, origin) => new ConnectionError(
1268
+ `Failed to reach mesh server at ${origin}: ${error instanceof Error ? error.message : "Unknown connection error"}`
1269
+ );
1270
+
1232
1271
  // libs/flux/shared/utils/src/lib/machine-id.util.ts
1233
1272
  var validateMachineUID = (machineUID) => {
1234
1273
  if (typeof machineUID !== "string") {
@@ -1287,32 +1326,47 @@ var authenticateAgentOrThrow = async (networkId, domain, unknownIdentificationPa
1287
1326
  if (clientInfo.machineUID && validateMachineUID(clientInfo.machineUID)) {
1288
1327
  url.searchParams.set("machineUID", clientInfo.machineUID);
1289
1328
  }
1290
- const response = await fetch(url, {
1291
- method: "POST",
1292
- headers: {
1293
- "Content-Type": "text/plain",
1294
- "x-flux-content-type": typeof unknownIdentificationPayload === "string" ? "text/plain" : "application/json",
1295
- "Accept": "text/plain"
1296
- },
1297
- body: customPayload
1298
- });
1299
- if (!response.ok) {
1300
- const textResponse = await response.text();
1301
- if (response.status === 401) {
1302
- if (textResponse.startsWith(NetworkAuthorityNotFoundError.message)) {
1303
- throw new NetworkAuthorityNotFoundError(networkId);
1329
+ try {
1330
+ const response = await fetch(url, {
1331
+ method: "POST",
1332
+ headers: {
1333
+ "Content-Type": "text/plain",
1334
+ "x-flux-content-type": typeof unknownIdentificationPayload === "string" ? "text/plain" : "application/json",
1335
+ "Accept": "text/plain"
1336
+ },
1337
+ body: customPayload
1338
+ });
1339
+ if (!response.ok) {
1340
+ const textResponse = await response.text();
1341
+ if (response.status === 401) {
1342
+ if (textResponse.startsWith(NetworkAuthorityNotFoundError.message)) {
1343
+ throw new NetworkAuthorityNotFoundError(networkId);
1344
+ }
1345
+ throw new AuthenticationError(textResponse ? `Auth failed: 401 - ${textResponse}` : "Auth failed: 401");
1304
1346
  }
1347
+ if (response.status === 404) {
1348
+ throw new EndpointNotFoundError(`Mesh server not found at ${url.origin}.`);
1349
+ }
1350
+ const message = textResponse ? `Auth failed: ${response.status} - ${textResponse}` : `Auth failed: ${response.status}`;
1351
+ if (isRetryableAuthStatus(response.status)) {
1352
+ throw new ConnectionError(message, response.status);
1353
+ }
1354
+ throw new Error(message);
1305
1355
  }
1306
- if (response.status === 404) {
1307
- throw new Error(`Mesh server not found at ${url.origin}.`);
1356
+ const result = await response.text();
1357
+ if (!checkAuthTicketShape(result)) {
1358
+ throw new Error("Auth failed: unexpected response");
1308
1359
  }
1309
- throw new Error(textResponse ? `Auth failed: ${response.status} - ${textResponse}` : `Auth failed: ${response.status}`);
1310
- }
1311
- const result = await response.text();
1312
- if (!checkAuthTicketShape(result)) {
1313
- throw new Error("Auth failed: unexpected response");
1360
+ return result;
1361
+ } catch (error) {
1362
+ if (error instanceof NetworkAuthorityNotFoundError || error instanceof AuthenticationError || error instanceof EndpointNotFoundError || error instanceof RetryableError) {
1363
+ throw error;
1364
+ }
1365
+ if (error instanceof Error && error.message.startsWith("Auth failed:")) {
1366
+ throw error;
1367
+ }
1368
+ throw asConnectionError(error, url.origin);
1314
1369
  }
1315
- return result;
1316
1370
  };
1317
1371
 
1318
1372
  // packages/flux/agent/src/lib/connector/flux-client-data.class.ts
@@ -1363,6 +1417,8 @@ var FluxAgent = class {
1363
1417
  throw new Error("Will never be thrown");
1364
1418
  }
1365
1419
  let warnedWaitingForAuthority = false;
1420
+ let warnedMeshUnreachable = false;
1421
+ let lastError;
1366
1422
  const ticket = await retryOrThrow(
1367
1423
  async () => {
1368
1424
  return authenticateAgentOrThrow(
@@ -1375,8 +1431,15 @@ var FluxAgent = class {
1375
1431
  }
1376
1432
  );
1377
1433
  },
1378
- // Retry if no authority was found yet
1379
- (error) => error instanceof NetworkAuthorityNotFoundError,
1434
+ // Retry while no Authority has appeared yet, and while the mesh
1435
+ // itself is unreachable. The latter used to fall straight through
1436
+ // to the caller, so a network blip during startup left the Agent
1437
+ // permanently disconnected while an Authority in the same
1438
+ // situation would have retried through it.
1439
+ (error) => {
1440
+ lastError = error;
1441
+ return error instanceof NetworkAuthorityNotFoundError || isRetryableConnectionError(error);
1442
+ },
1380
1443
  {
1381
1444
  // Fixed by the SDK, not the consuming app: Agents run on untrusted client
1382
1445
  // machines, so the mesh operator — not the app — controls how long an Agent
@@ -1384,6 +1447,16 @@ var FluxAgent = class {
1384
1447
  retries: MAX_CONNECT_ATTEMPTS,
1385
1448
  delayMs: 500,
1386
1449
  onRetry: (attempt, retries) => {
1450
+ if (isRetryableConnectionError(lastError)) {
1451
+ this.stateManager.emitNetworkState("authorizing");
1452
+ if (!warnedMeshUnreachable) {
1453
+ warnedMeshUnreachable = true;
1454
+ console.warn(
1455
+ `[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).`
1456
+ );
1457
+ }
1458
+ return Math.min(3e4, 500 * 2 ** attempt);
1459
+ }
1387
1460
  this.stateManager.emitNetworkState("waiting-for-authority");
1388
1461
  if (!warnedWaitingForAuthority) {
1389
1462
  warnedWaitingForAuthority = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistica/flux-agent",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "Persistica Flux Agent",
5
5
  "type": "module",
6
6
  "keywords": [],