@persistica/flux-agent 0.0.35 → 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 +93 -26
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1223,8 +1223,12 @@ var retryOrThrow = async (fn, shouldRetry, options) => {
1223
1223
  if (attempt >= options.retries || !shouldRetry(err)) {
1224
1224
  throw err;
1225
1225
  }
1226
- if (options.delayMs > 0 || overrideDelay && overrideDelay > 0) {
1227
- 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) {
1228
1232
  await new Promise((res) => setTimeout(
1229
1233
  res,
1230
1234
  delay
@@ -1235,6 +1239,35 @@ var retryOrThrow = async (fn, shouldRetry, options) => {
1235
1239
  throw new Error("Retry loop exited unexpectedly.");
1236
1240
  };
1237
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
+
1238
1271
  // libs/flux/shared/utils/src/lib/machine-id.util.ts
1239
1272
  var validateMachineUID = (machineUID) => {
1240
1273
  if (typeof machineUID !== "string") {
@@ -1293,32 +1326,47 @@ var authenticateAgentOrThrow = async (networkId, domain, unknownIdentificationPa
1293
1326
  if (clientInfo.machineUID && validateMachineUID(clientInfo.machineUID)) {
1294
1327
  url.searchParams.set("machineUID", clientInfo.machineUID);
1295
1328
  }
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);
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");
1346
+ }
1347
+ if (response.status === 404) {
1348
+ throw new EndpointNotFoundError(`Mesh server not found at ${url.origin}.`);
1310
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);
1311
1355
  }
1312
- if (response.status === 404) {
1313
- 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");
1314
1359
  }
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");
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);
1320
1369
  }
1321
- return result;
1322
1370
  };
1323
1371
 
1324
1372
  // packages/flux/agent/src/lib/connector/flux-client-data.class.ts
@@ -1369,6 +1417,8 @@ var FluxAgent = class {
1369
1417
  throw new Error("Will never be thrown");
1370
1418
  }
1371
1419
  let warnedWaitingForAuthority = false;
1420
+ let warnedMeshUnreachable = false;
1421
+ let lastError;
1372
1422
  const ticket = await retryOrThrow(
1373
1423
  async () => {
1374
1424
  return authenticateAgentOrThrow(
@@ -1381,8 +1431,15 @@ var FluxAgent = class {
1381
1431
  }
1382
1432
  );
1383
1433
  },
1384
- // Retry if no authority was found yet
1385
- (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
+ },
1386
1443
  {
1387
1444
  // Fixed by the SDK, not the consuming app: Agents run on untrusted client
1388
1445
  // machines, so the mesh operator — not the app — controls how long an Agent
@@ -1390,6 +1447,16 @@ var FluxAgent = class {
1390
1447
  retries: MAX_CONNECT_ATTEMPTS,
1391
1448
  delayMs: 500,
1392
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
+ }
1393
1460
  this.stateManager.emitNetworkState("waiting-for-authority");
1394
1461
  if (!warnedWaitingForAuthority) {
1395
1462
  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.36",
4
4
  "description": "Persistica Flux Agent",
5
5
  "type": "module",
6
6
  "keywords": [],