@bettercms-ai/sdk 1.13.2 → 1.14.0
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/index.cjs +118 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -7
- package/dist/index.d.ts +38 -7
- package/dist/index.js +118 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -207,6 +207,35 @@ var BetterCMSError = class _BetterCMSError extends Error {
|
|
|
207
207
|
}
|
|
208
208
|
};
|
|
209
209
|
|
|
210
|
+
// src/retry.ts
|
|
211
|
+
var RETRY_STATUSES = [429, 503];
|
|
212
|
+
var MAX_RETRIES = 3;
|
|
213
|
+
var MAX_DELAY_MS = 65e3;
|
|
214
|
+
var BASE_DELAY_MS = 1e3;
|
|
215
|
+
var JITTER_MS = 250;
|
|
216
|
+
function isRetryableStatus(status) {
|
|
217
|
+
return RETRY_STATUSES.includes(status);
|
|
218
|
+
}
|
|
219
|
+
function retryAfterMs(res) {
|
|
220
|
+
const raw = res.headers?.get?.("retry-after");
|
|
221
|
+
if (!raw) return null;
|
|
222
|
+
const seconds = Number(raw);
|
|
223
|
+
if (Number.isFinite(seconds)) {
|
|
224
|
+
return Math.min(Math.max(seconds, 0) * 1e3, MAX_DELAY_MS);
|
|
225
|
+
}
|
|
226
|
+
const at = Date.parse(raw);
|
|
227
|
+
if (!Number.isNaN(at)) {
|
|
228
|
+
return Math.min(Math.max(0, at - Date.now()), MAX_DELAY_MS);
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function backoffMs(attempt, res) {
|
|
233
|
+
const fromServer = res ? retryAfterMs(res) : null;
|
|
234
|
+
const base = Math.min(fromServer ?? BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS - JITTER_MS);
|
|
235
|
+
return base + Math.random() * JITTER_MS;
|
|
236
|
+
}
|
|
237
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
238
|
+
|
|
210
239
|
// src/methods/getContent.ts
|
|
211
240
|
async function getContent(client, slug) {
|
|
212
241
|
const data = await client.fetchJSON(client.url(slug));
|
|
@@ -271,14 +300,17 @@ function listContentAll(client, opts) {
|
|
|
271
300
|
}
|
|
272
301
|
|
|
273
302
|
// src/delivery-client.ts
|
|
303
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
274
304
|
var BetterCMSDeliveryClient = class {
|
|
275
305
|
workspace;
|
|
276
306
|
baseUrl;
|
|
307
|
+
timeout;
|
|
277
308
|
apiKey;
|
|
278
309
|
constructor(opts) {
|
|
279
310
|
this.workspace = opts.workspace;
|
|
280
311
|
this.baseUrl = opts.baseUrl;
|
|
281
312
|
this.apiKey = opts.apiKey;
|
|
313
|
+
this.timeout = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
282
314
|
}
|
|
283
315
|
getContent(slug) {
|
|
284
316
|
return getContent(this, slug);
|
|
@@ -311,24 +343,46 @@ var BetterCMSDeliveryClient = class {
|
|
|
311
343
|
return headers;
|
|
312
344
|
}
|
|
313
345
|
/**
|
|
314
|
-
* Generic fetch helper —
|
|
315
|
-
* and throws a typed BetterCMSError on non-OK responses.
|
|
346
|
+
* Generic fetch helper — retries 429/503 honouring Retry-After, applies a
|
|
347
|
+
* per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
|
|
348
|
+
*
|
|
349
|
+
* This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
|
|
350
|
+
* 429 from the delivery limiter threw straight through and killed the caller. In a
|
|
351
|
+
* static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
|
|
352
|
+
* never going to be enough against a 60-second fixed window.
|
|
316
353
|
*/
|
|
317
354
|
async fetchJSON(url) {
|
|
318
|
-
let
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
355
|
+
for (let attempt = 0; ; attempt++) {
|
|
356
|
+
const controller = new AbortController();
|
|
357
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
358
|
+
let res;
|
|
359
|
+
try {
|
|
360
|
+
res = await globalThis.fetch(url, {
|
|
361
|
+
headers: this.headers(),
|
|
362
|
+
signal: controller.signal
|
|
363
|
+
});
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const timedOut = err instanceof Error && err.name === "AbortError";
|
|
366
|
+
if (attempt < MAX_RETRIES) {
|
|
367
|
+
await sleep(backoffMs(attempt));
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (timedOut) throw new BetterCMSError("Request timeout", 408, "NETWORK_ERROR");
|
|
371
|
+
throw new BetterCMSError(
|
|
372
|
+
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
373
|
+
0,
|
|
374
|
+
"NETWORK_ERROR"
|
|
375
|
+
);
|
|
376
|
+
} finally {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
}
|
|
379
|
+
if (res.ok) return await res.json();
|
|
380
|
+
if (attempt < MAX_RETRIES && isRetryableStatus(res.status)) {
|
|
381
|
+
await sleep(backoffMs(attempt, res));
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
329
384
|
throw await BetterCMSError.from(res);
|
|
330
385
|
}
|
|
331
|
-
return await res.json();
|
|
332
386
|
}
|
|
333
387
|
};
|
|
334
388
|
|
|
@@ -409,19 +463,15 @@ function addWebhookMethods(client) {
|
|
|
409
463
|
|
|
410
464
|
// src/admin-client.ts
|
|
411
465
|
var DEFAULT_BASE_URL = "https://api.bettercms.ai/v1";
|
|
412
|
-
var DEFAULT_TIMEOUT = 1e4;
|
|
413
|
-
var RETRY_STATUSES = [429, 503];
|
|
414
|
-
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
415
466
|
var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
416
|
-
timeout;
|
|
417
467
|
_token;
|
|
418
468
|
constructor(options) {
|
|
419
469
|
super({
|
|
420
470
|
workspace: "",
|
|
421
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL
|
|
471
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
472
|
+
timeout: options.timeout
|
|
422
473
|
});
|
|
423
474
|
this._token = options.token;
|
|
424
|
-
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
425
475
|
}
|
|
426
476
|
headers() {
|
|
427
477
|
return {
|
|
@@ -431,12 +481,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
431
481
|
};
|
|
432
482
|
}
|
|
433
483
|
/**
|
|
434
|
-
* Admin fetchJSON with retry (429, 503) and per-request
|
|
435
|
-
* Each retry gets a fresh timeout.
|
|
484
|
+
* Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
|
|
485
|
+
* timeout (AbortController). Each retry gets a fresh timeout.
|
|
436
486
|
*/
|
|
437
487
|
async fetchJSON(url, init) {
|
|
438
488
|
let lastError;
|
|
439
|
-
for (let attempt = 0; attempt <=
|
|
489
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
440
490
|
const controller = new AbortController();
|
|
441
491
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
442
492
|
let nonRetryable = false;
|
|
@@ -450,12 +500,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
450
500
|
if (res.ok) {
|
|
451
501
|
return res.json();
|
|
452
502
|
}
|
|
453
|
-
const shouldRetry = attempt <
|
|
503
|
+
const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
454
504
|
if (!shouldRetry) {
|
|
455
505
|
nonRetryable = true;
|
|
456
506
|
throw await BetterCMSError.from(res);
|
|
457
507
|
}
|
|
458
|
-
await sleep(
|
|
508
|
+
await sleep(backoffMs(attempt, res));
|
|
459
509
|
} catch (err) {
|
|
460
510
|
clearTimeout(timeoutId);
|
|
461
511
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -465,8 +515,8 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
465
515
|
throw err;
|
|
466
516
|
}
|
|
467
517
|
lastError = err;
|
|
468
|
-
if (attempt <
|
|
469
|
-
await sleep(
|
|
518
|
+
if (attempt < MAX_RETRIES) {
|
|
519
|
+
await sleep(backoffMs(attempt));
|
|
470
520
|
continue;
|
|
471
521
|
}
|
|
472
522
|
throw err;
|
|
@@ -483,9 +533,6 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
483
533
|
return addWebhookMethods(this);
|
|
484
534
|
}
|
|
485
535
|
};
|
|
486
|
-
function sleep(ms) {
|
|
487
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
488
|
-
}
|
|
489
536
|
|
|
490
537
|
// src/methods/management/content.ts
|
|
491
538
|
async function listModels(client) {
|
|
@@ -862,8 +909,6 @@ async function commandManagedLayout(client, input) {
|
|
|
862
909
|
// src/management-client.ts
|
|
863
910
|
var DEFAULT_BASE_URL2 = "https://api.bettercms.ai/v1";
|
|
864
911
|
var DEFAULT_TIMEOUT2 = 1e4;
|
|
865
|
-
var RETRY_STATUSES2 = [429, 503];
|
|
866
|
-
var RETRY_DELAYS2 = [1e3, 2e3, 4e3];
|
|
867
912
|
var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
868
913
|
timeout;
|
|
869
914
|
_apiKey;
|
|
@@ -885,10 +930,14 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
885
930
|
url(path) {
|
|
886
931
|
return `${this.baseUrl}${path}`;
|
|
887
932
|
}
|
|
888
|
-
/**
|
|
933
|
+
/**
|
|
934
|
+
* fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
|
|
935
|
+
* (mirrors the admin client). See ./retry.ts for why the delay must be able
|
|
936
|
+
* to outlast the backend's 60-second rate-limit window.
|
|
937
|
+
*/
|
|
889
938
|
async fetchJSON(url, init) {
|
|
890
939
|
let lastError;
|
|
891
|
-
for (let attempt = 0; attempt <=
|
|
940
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
892
941
|
const controller = new AbortController();
|
|
893
942
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
894
943
|
let nonRetryable = false;
|
|
@@ -902,12 +951,12 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
902
951
|
if (res.ok) {
|
|
903
952
|
return res.json();
|
|
904
953
|
}
|
|
905
|
-
const shouldRetry = attempt <
|
|
954
|
+
const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
906
955
|
if (!shouldRetry) {
|
|
907
956
|
nonRetryable = true;
|
|
908
957
|
throw await BetterCMSError.from(res);
|
|
909
958
|
}
|
|
910
|
-
await
|
|
959
|
+
await sleep(backoffMs(attempt, res));
|
|
911
960
|
} catch (err) {
|
|
912
961
|
clearTimeout(timeoutId);
|
|
913
962
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -915,8 +964,8 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
915
964
|
}
|
|
916
965
|
if (nonRetryable) throw err;
|
|
917
966
|
lastError = err;
|
|
918
|
-
if (attempt <
|
|
919
|
-
await
|
|
967
|
+
if (attempt < MAX_RETRIES) {
|
|
968
|
+
await sleep(backoffMs(attempt));
|
|
920
969
|
continue;
|
|
921
970
|
}
|
|
922
971
|
throw err;
|
|
@@ -1048,9 +1097,6 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
1048
1097
|
return generateSeoMeta(this, input);
|
|
1049
1098
|
}
|
|
1050
1099
|
};
|
|
1051
|
-
function sleep2(ms) {
|
|
1052
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1053
|
-
}
|
|
1054
1100
|
|
|
1055
1101
|
// src/auth.ts
|
|
1056
1102
|
var AUTH_BASE_URL = "https://api.bettercms.ai/api/v1/auth";
|
|
@@ -1141,7 +1187,8 @@ var BetterCMS = {
|
|
|
1141
1187
|
return new BetterCMSDeliveryClient({
|
|
1142
1188
|
workspace: options.workspace,
|
|
1143
1189
|
apiKey: options.apiKey,
|
|
1144
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3
|
|
1190
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3,
|
|
1191
|
+
timeout: options.timeout
|
|
1145
1192
|
});
|
|
1146
1193
|
},
|
|
1147
1194
|
/**
|
|
@@ -1173,10 +1220,7 @@ var BetterCMS = {
|
|
|
1173
1220
|
};
|
|
1174
1221
|
|
|
1175
1222
|
// src/read-client.ts
|
|
1176
|
-
var RETRY_STATUSES3 = [429, 503];
|
|
1177
|
-
var RETRY_DELAYS3 = [250, 750, 1500];
|
|
1178
1223
|
var DEFAULT_TIMEOUT3 = 1e4;
|
|
1179
|
-
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1180
1224
|
function qs(params2) {
|
|
1181
1225
|
const sp = new URLSearchParams();
|
|
1182
1226
|
for (const [k, v] of Object.entries(params2)) {
|
|
@@ -1193,6 +1237,27 @@ function unwrap(json) {
|
|
|
1193
1237
|
}
|
|
1194
1238
|
return json;
|
|
1195
1239
|
}
|
|
1240
|
+
function createRequestCache() {
|
|
1241
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1242
|
+
const settled = /* @__PURE__ */ new Map();
|
|
1243
|
+
const copy = (value) => typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
1244
|
+
return async function collapse(url, fetcher) {
|
|
1245
|
+
if (settled.has(url)) return copy(settled.get(url));
|
|
1246
|
+
const pending = inflight.get(url);
|
|
1247
|
+
if (pending) return copy(await pending);
|
|
1248
|
+
const run = (async () => {
|
|
1249
|
+
try {
|
|
1250
|
+
const value = await fetcher();
|
|
1251
|
+
settled.set(url, value);
|
|
1252
|
+
return value;
|
|
1253
|
+
} finally {
|
|
1254
|
+
inflight.delete(url);
|
|
1255
|
+
}
|
|
1256
|
+
})();
|
|
1257
|
+
inflight.set(url, run);
|
|
1258
|
+
return copy(await run);
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1196
1261
|
function createClient(options) {
|
|
1197
1262
|
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
1198
1263
|
const { workspace, apiKey, perspective = "published", previewToken } = options;
|
|
@@ -1204,9 +1269,11 @@ function createClient(options) {
|
|
|
1204
1269
|
if (apiKey) h["X-API-Key"] = apiKey;
|
|
1205
1270
|
return h;
|
|
1206
1271
|
};
|
|
1207
|
-
|
|
1272
|
+
const collapse = options.cache && perspective === "published" ? createRequestCache() : void 0;
|
|
1273
|
+
const fetchJSON = (url) => collapse ? collapse(url, () => request(url)) : request(url);
|
|
1274
|
+
async function request(url) {
|
|
1208
1275
|
let lastError;
|
|
1209
|
-
for (let attempt = 0; attempt <=
|
|
1276
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1210
1277
|
const controller = new AbortController();
|
|
1211
1278
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
1212
1279
|
try {
|
|
@@ -1216,9 +1283,9 @@ function createClient(options) {
|
|
|
1216
1283
|
});
|
|
1217
1284
|
clearTimeout(timer);
|
|
1218
1285
|
if (res.ok) return await res.json();
|
|
1219
|
-
const retryable = attempt <
|
|
1286
|
+
const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
1220
1287
|
if (!retryable) throw await BetterCMSError.from(res);
|
|
1221
|
-
await
|
|
1288
|
+
await sleep(backoffMs(attempt, res));
|
|
1222
1289
|
} catch (err) {
|
|
1223
1290
|
clearTimeout(timer);
|
|
1224
1291
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -1226,8 +1293,8 @@ function createClient(options) {
|
|
|
1226
1293
|
}
|
|
1227
1294
|
if (err instanceof BetterCMSError) throw err;
|
|
1228
1295
|
lastError = err;
|
|
1229
|
-
if (attempt <
|
|
1230
|
-
await
|
|
1296
|
+
if (attempt < MAX_RETRIES) {
|
|
1297
|
+
await sleep(backoffMs(attempt));
|
|
1231
1298
|
continue;
|
|
1232
1299
|
}
|
|
1233
1300
|
throw new BetterCMSError(
|