@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.
- package/index.js +110 -34
- 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
|
-
|
|
761
|
-
|
|
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
|
-
|
|
1227
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
if (
|
|
1309
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
1365
|
+
const result = await response.text();
|
|
1366
|
+
if (!checkAuthTicketShape(result)) {
|
|
1367
|
+
throw new Error("Auth failed: unexpected response");
|
|
1314
1368
|
}
|
|
1315
|
-
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
|
1385
|
-
|
|
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;
|