@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.
- package/index.js +103 -30
- 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.
|
|
246
|
-
(this.options.reconnectDelay ?? 0) *
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
1356
|
+
const result = await response.text();
|
|
1357
|
+
if (!checkAuthTicketShape(result)) {
|
|
1358
|
+
throw new Error("Auth failed: unexpected response");
|
|
1308
1359
|
}
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
|
1379
|
-
|
|
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;
|