@north-light/api 0.3.559 → 0.3.560
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/dist/api/client.js +34 -0
- package/package.json +1 -1
package/dist/api/client.js
CHANGED
|
@@ -908,6 +908,8 @@ export class CrtrClient {
|
|
|
908
908
|
const opts = this.resolveBudget(callOpts);
|
|
909
909
|
const idempotent = method === 'GET' || method === 'HEAD' || method === 'DELETE';
|
|
910
910
|
const maxRetries = opts.maxRetries ?? this.maxRetries;
|
|
911
|
+
// A hosted runtime being created or replaced answers reads with a refusal that never reached it; wait for it inside this allowance.
|
|
912
|
+
const replacementWaitEndsAt = Math.min(opts.deadlineAt ?? Infinity, Date.now() + HOSTED_REPLACEMENT_WAIT_MS);
|
|
911
913
|
for (let attempt = 0;; attempt++) {
|
|
912
914
|
let response;
|
|
913
915
|
try {
|
|
@@ -942,6 +944,15 @@ export class CrtrClient {
|
|
|
942
944
|
throw toTransportApiError(err, opts.signal);
|
|
943
945
|
}
|
|
944
946
|
}
|
|
947
|
+
if (isRead(method) && maxRetries > 0 && response.status === 503 && await isRefusedBeforeForwarding(response, opts.deadlineAt)) {
|
|
948
|
+
const delayMs = Math.max(250, (retryAfterHeaderS(response) ?? 0) * 1_000) + Math.random() * 250;
|
|
949
|
+
// Out of allowance: the refusal itself is the answer, so the caller sees its cause.
|
|
950
|
+
if (Date.now() + delayMs >= replacementWaitEndsAt)
|
|
951
|
+
return response;
|
|
952
|
+
await drainResponseBody(response, opts.deadlineAt);
|
|
953
|
+
await sleepWithin(delayMs, { signal: opts.signal });
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
945
956
|
if (idempotent && attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
946
957
|
await drainResponseBody(response, opts.deadlineAt);
|
|
947
958
|
await sleepWithin(retryDelayMs(attempt + 1), opts, retryAfterHeaderS(response));
|
|
@@ -1266,6 +1277,29 @@ async function parse(res, deadlineAt) {
|
|
|
1266
1277
|
}
|
|
1267
1278
|
throw new ApiError(res.status, 'internal', `request failed with status ${res.status}`, undefined, res.headers);
|
|
1268
1279
|
}
|
|
1280
|
+
/** How long a read waits for a hosted runtime that is being created or replaced. */
|
|
1281
|
+
const HOSTED_REPLACEMENT_WAIT_MS = 60_000;
|
|
1282
|
+
/** Only reads are replayed after a refusal; a write is never retried on this state. */
|
|
1283
|
+
function isRead(method) {
|
|
1284
|
+
return method === 'GET' || method === 'HEAD';
|
|
1285
|
+
}
|
|
1286
|
+
/** True when the response is the hosting router's 503 for a runtime that provisioning is creating or replacing
|
|
1287
|
+
* (`details.forwarded === false`): the request reached no runtime, so replaying it cannot run anything twice.
|
|
1288
|
+
* Reads a clone, so the caller still gets the body of the response it is handed. */
|
|
1289
|
+
async function isRefusedBeforeForwarding(response, deadlineAt) {
|
|
1290
|
+
try {
|
|
1291
|
+
// A router refusal is a few hundred bytes; anything declared larger, or a body that stalls, is not one.
|
|
1292
|
+
if (Number(response.headers.get('content-length') ?? 0) > 8_192)
|
|
1293
|
+
return false;
|
|
1294
|
+
const text = await readTextWithin(response.clone(), Math.min(deadlineAt ?? Infinity, Date.now() + 5_000));
|
|
1295
|
+
const error = JSON.parse(text).error;
|
|
1296
|
+
return error?.origin === 'router' && error.details?.forwarded === false
|
|
1297
|
+
&& (error.details.runtime_state === 'creating' || error.details.runtime_state === 'recovering');
|
|
1298
|
+
}
|
|
1299
|
+
catch {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1269
1303
|
/** `429`/`5xx` are the only statuses the §7 retry policy replays. */
|
|
1270
1304
|
function isRetryableStatus(status) {
|
|
1271
1305
|
return status === 429 || status >= 500;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.560",
|
|
4
4
|
"description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/api/index.js",
|