@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.d.cts
CHANGED
|
@@ -51,11 +51,13 @@ declare function listContentAll(client: BetterCMSDeliveryClient, opts?: ListCont
|
|
|
51
51
|
declare class BetterCMSDeliveryClient {
|
|
52
52
|
readonly workspace: string;
|
|
53
53
|
readonly baseUrl: string;
|
|
54
|
+
protected readonly timeout: number;
|
|
54
55
|
private readonly apiKey?;
|
|
55
56
|
constructor(opts: {
|
|
56
57
|
workspace: string;
|
|
57
58
|
apiKey?: string;
|
|
58
59
|
baseUrl: string;
|
|
60
|
+
timeout?: number;
|
|
59
61
|
});
|
|
60
62
|
getContent(slug: string): Promise<_bettercms_ai_types.Content>;
|
|
61
63
|
listContent(opts?: Parameters<typeof listContent>[1]): Promise<_bettercms_ai_types.PaginatedResult<_bettercms_ai_types.Content>>;
|
|
@@ -71,8 +73,13 @@ declare class BetterCMSDeliveryClient {
|
|
|
71
73
|
/** Build request headers, including auth if provided. */
|
|
72
74
|
headers(): Record<string, string>;
|
|
73
75
|
/**
|
|
74
|
-
* Generic fetch helper —
|
|
75
|
-
* and throws a typed BetterCMSError on non-OK responses.
|
|
76
|
+
* Generic fetch helper — retries 429/503 honouring Retry-After, applies a
|
|
77
|
+
* per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
|
|
78
|
+
*
|
|
79
|
+
* This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
|
|
80
|
+
* 429 from the delivery limiter threw straight through and killed the caller. In a
|
|
81
|
+
* static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
|
|
82
|
+
* never going to be enough against a 60-second fixed window.
|
|
76
83
|
*/
|
|
77
84
|
fetchJSON<T>(url: string): Promise<T>;
|
|
78
85
|
}
|
|
@@ -139,7 +146,7 @@ interface WebhookListResponse {
|
|
|
139
146
|
*
|
|
140
147
|
* Extends BetterCMSDeliveryClient but overrides:
|
|
141
148
|
* - `headers()` — uses Bearer token auth instead of API key
|
|
142
|
-
* - `fetchJSON()` — adds retry on 429/503
|
|
149
|
+
* - `fetchJSON()` — adds Retry-After-aware retry on 429/503 (see ./retry.ts)
|
|
143
150
|
*
|
|
144
151
|
* @see openspec/changes/flo-6-backend-sdk-admin/specs/sdk-admin-client/spec.md
|
|
145
152
|
*/
|
|
@@ -158,13 +165,12 @@ interface BetterCMSAdminOptions {
|
|
|
158
165
|
* fetchJSON() for Bearer auth + retry/timeout.
|
|
159
166
|
*/
|
|
160
167
|
declare class BetterCMSAdminClient extends BetterCMSDeliveryClient {
|
|
161
|
-
readonly timeout: number;
|
|
162
168
|
private readonly _token;
|
|
163
169
|
constructor(options: BetterCMSAdminOptions);
|
|
164
170
|
headers(): Record<string, string>;
|
|
165
171
|
/**
|
|
166
|
-
* Admin fetchJSON with retry (429, 503) and per-request
|
|
167
|
-
* Each retry gets a fresh timeout.
|
|
172
|
+
* Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
|
|
173
|
+
* timeout (AbortController). Each retry gets a fresh timeout.
|
|
168
174
|
*/
|
|
169
175
|
fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
|
|
170
176
|
/** Build the admin API URL. */
|
|
@@ -741,7 +747,11 @@ declare class BetterCMSManagementClient extends BetterCMSDeliveryClient {
|
|
|
741
747
|
headers(): Record<string, string>;
|
|
742
748
|
/** Management API URL: baseUrl + path (path is workspace-scoped via the key). */
|
|
743
749
|
url(path: string): string;
|
|
744
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
|
|
752
|
+
* (mirrors the admin client). See ./retry.ts for why the delay must be able
|
|
753
|
+
* to outlast the backend's 60-second rate-limit window.
|
|
754
|
+
*/
|
|
745
755
|
fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
|
|
746
756
|
listModels(): Promise<ManagedContentModel[]>;
|
|
747
757
|
getModel(id: string): Promise<ManagedContentModel>;
|
|
@@ -864,6 +874,12 @@ interface BetterCMSSiteOptions {
|
|
|
864
874
|
apiKey?: string;
|
|
865
875
|
/** Base URL of the Delivery API. Defaults to https://api.bettercms.ai/v1. */
|
|
866
876
|
baseUrl?: string;
|
|
877
|
+
/**
|
|
878
|
+
* Per-attempt request timeout in ms. Defaults to 10000. Each of the 3 retries gets
|
|
879
|
+
* a fresh one. Raise it for a slow delivery origin rather than losing the timeout —
|
|
880
|
+
* without one, a hung socket stalls a build indefinitely.
|
|
881
|
+
*/
|
|
882
|
+
timeout?: number;
|
|
867
883
|
}
|
|
868
884
|
/**
|
|
869
885
|
* BetterCMS factory — returns a typed client for the Delivery API.
|
|
@@ -931,6 +947,21 @@ interface CreateClientOptions {
|
|
|
931
947
|
stega?: boolean;
|
|
932
948
|
/** Per-attempt timeout in ms. Defaults to 10000. */
|
|
933
949
|
timeout?: number;
|
|
950
|
+
/**
|
|
951
|
+
* Collapse identical GETs to one network round-trip for the life of this client.
|
|
952
|
+
* Default false.
|
|
953
|
+
*
|
|
954
|
+
* Only correct where the content is a POINT-IN-TIME SNAPSHOT — i.e. a static build,
|
|
955
|
+
* where every route renders from the same published dataset. `@bettercms-ai/astro`
|
|
956
|
+
* turns this on automatically when Astro's `command === "build"` and never for `dev`,
|
|
957
|
+
* SSR or the drafts perspective, where a repeat read must see the current row.
|
|
958
|
+
*
|
|
959
|
+
* A static build without it re-fetches the same nav/settings/collection once PER ROUTE.
|
|
960
|
+
* That is what made a large site exhaust the delivery rate limit: hundreds of identical
|
|
961
|
+
* requests, and `getPage()` re-scanning every page of the paginated list each time it is
|
|
962
|
+
* called.
|
|
963
|
+
*/
|
|
964
|
+
cache?: boolean;
|
|
934
965
|
}
|
|
935
966
|
interface ListEntriesOptions {
|
|
936
967
|
/** Filter by content-model slug. */
|
package/dist/index.d.ts
CHANGED
|
@@ -51,11 +51,13 @@ declare function listContentAll(client: BetterCMSDeliveryClient, opts?: ListCont
|
|
|
51
51
|
declare class BetterCMSDeliveryClient {
|
|
52
52
|
readonly workspace: string;
|
|
53
53
|
readonly baseUrl: string;
|
|
54
|
+
protected readonly timeout: number;
|
|
54
55
|
private readonly apiKey?;
|
|
55
56
|
constructor(opts: {
|
|
56
57
|
workspace: string;
|
|
57
58
|
apiKey?: string;
|
|
58
59
|
baseUrl: string;
|
|
60
|
+
timeout?: number;
|
|
59
61
|
});
|
|
60
62
|
getContent(slug: string): Promise<_bettercms_ai_types.Content>;
|
|
61
63
|
listContent(opts?: Parameters<typeof listContent>[1]): Promise<_bettercms_ai_types.PaginatedResult<_bettercms_ai_types.Content>>;
|
|
@@ -71,8 +73,13 @@ declare class BetterCMSDeliveryClient {
|
|
|
71
73
|
/** Build request headers, including auth if provided. */
|
|
72
74
|
headers(): Record<string, string>;
|
|
73
75
|
/**
|
|
74
|
-
* Generic fetch helper —
|
|
75
|
-
* and throws a typed BetterCMSError on non-OK responses.
|
|
76
|
+
* Generic fetch helper — retries 429/503 honouring Retry-After, applies a
|
|
77
|
+
* per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
|
|
78
|
+
*
|
|
79
|
+
* This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
|
|
80
|
+
* 429 from the delivery limiter threw straight through and killed the caller. In a
|
|
81
|
+
* static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
|
|
82
|
+
* never going to be enough against a 60-second fixed window.
|
|
76
83
|
*/
|
|
77
84
|
fetchJSON<T>(url: string): Promise<T>;
|
|
78
85
|
}
|
|
@@ -139,7 +146,7 @@ interface WebhookListResponse {
|
|
|
139
146
|
*
|
|
140
147
|
* Extends BetterCMSDeliveryClient but overrides:
|
|
141
148
|
* - `headers()` — uses Bearer token auth instead of API key
|
|
142
|
-
* - `fetchJSON()` — adds retry on 429/503
|
|
149
|
+
* - `fetchJSON()` — adds Retry-After-aware retry on 429/503 (see ./retry.ts)
|
|
143
150
|
*
|
|
144
151
|
* @see openspec/changes/flo-6-backend-sdk-admin/specs/sdk-admin-client/spec.md
|
|
145
152
|
*/
|
|
@@ -158,13 +165,12 @@ interface BetterCMSAdminOptions {
|
|
|
158
165
|
* fetchJSON() for Bearer auth + retry/timeout.
|
|
159
166
|
*/
|
|
160
167
|
declare class BetterCMSAdminClient extends BetterCMSDeliveryClient {
|
|
161
|
-
readonly timeout: number;
|
|
162
168
|
private readonly _token;
|
|
163
169
|
constructor(options: BetterCMSAdminOptions);
|
|
164
170
|
headers(): Record<string, string>;
|
|
165
171
|
/**
|
|
166
|
-
* Admin fetchJSON with retry (429, 503) and per-request
|
|
167
|
-
* Each retry gets a fresh timeout.
|
|
172
|
+
* Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
|
|
173
|
+
* timeout (AbortController). Each retry gets a fresh timeout.
|
|
168
174
|
*/
|
|
169
175
|
fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
|
|
170
176
|
/** Build the admin API URL. */
|
|
@@ -741,7 +747,11 @@ declare class BetterCMSManagementClient extends BetterCMSDeliveryClient {
|
|
|
741
747
|
headers(): Record<string, string>;
|
|
742
748
|
/** Management API URL: baseUrl + path (path is workspace-scoped via the key). */
|
|
743
749
|
url(path: string): string;
|
|
744
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
|
|
752
|
+
* (mirrors the admin client). See ./retry.ts for why the delay must be able
|
|
753
|
+
* to outlast the backend's 60-second rate-limit window.
|
|
754
|
+
*/
|
|
745
755
|
fetchJSON<T>(url: string, init?: RequestInit): Promise<T>;
|
|
746
756
|
listModels(): Promise<ManagedContentModel[]>;
|
|
747
757
|
getModel(id: string): Promise<ManagedContentModel>;
|
|
@@ -864,6 +874,12 @@ interface BetterCMSSiteOptions {
|
|
|
864
874
|
apiKey?: string;
|
|
865
875
|
/** Base URL of the Delivery API. Defaults to https://api.bettercms.ai/v1. */
|
|
866
876
|
baseUrl?: string;
|
|
877
|
+
/**
|
|
878
|
+
* Per-attempt request timeout in ms. Defaults to 10000. Each of the 3 retries gets
|
|
879
|
+
* a fresh one. Raise it for a slow delivery origin rather than losing the timeout —
|
|
880
|
+
* without one, a hung socket stalls a build indefinitely.
|
|
881
|
+
*/
|
|
882
|
+
timeout?: number;
|
|
867
883
|
}
|
|
868
884
|
/**
|
|
869
885
|
* BetterCMS factory — returns a typed client for the Delivery API.
|
|
@@ -931,6 +947,21 @@ interface CreateClientOptions {
|
|
|
931
947
|
stega?: boolean;
|
|
932
948
|
/** Per-attempt timeout in ms. Defaults to 10000. */
|
|
933
949
|
timeout?: number;
|
|
950
|
+
/**
|
|
951
|
+
* Collapse identical GETs to one network round-trip for the life of this client.
|
|
952
|
+
* Default false.
|
|
953
|
+
*
|
|
954
|
+
* Only correct where the content is a POINT-IN-TIME SNAPSHOT — i.e. a static build,
|
|
955
|
+
* where every route renders from the same published dataset. `@bettercms-ai/astro`
|
|
956
|
+
* turns this on automatically when Astro's `command === "build"` and never for `dev`,
|
|
957
|
+
* SSR or the drafts perspective, where a repeat read must see the current row.
|
|
958
|
+
*
|
|
959
|
+
* A static build without it re-fetches the same nav/settings/collection once PER ROUTE.
|
|
960
|
+
* That is what made a large site exhaust the delivery rate limit: hundreds of identical
|
|
961
|
+
* requests, and `getPage()` re-scanning every page of the paginated list each time it is
|
|
962
|
+
* called.
|
|
963
|
+
*/
|
|
964
|
+
cache?: boolean;
|
|
934
965
|
}
|
|
935
966
|
interface ListEntriesOptions {
|
|
936
967
|
/** Filter by content-model slug. */
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,35 @@ var BetterCMSError = class _BetterCMSError extends Error {
|
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
// src/retry.ts
|
|
93
|
+
var RETRY_STATUSES = [429, 503];
|
|
94
|
+
var MAX_RETRIES = 3;
|
|
95
|
+
var MAX_DELAY_MS = 65e3;
|
|
96
|
+
var BASE_DELAY_MS = 1e3;
|
|
97
|
+
var JITTER_MS = 250;
|
|
98
|
+
function isRetryableStatus(status) {
|
|
99
|
+
return RETRY_STATUSES.includes(status);
|
|
100
|
+
}
|
|
101
|
+
function retryAfterMs(res) {
|
|
102
|
+
const raw = res.headers?.get?.("retry-after");
|
|
103
|
+
if (!raw) return null;
|
|
104
|
+
const seconds = Number(raw);
|
|
105
|
+
if (Number.isFinite(seconds)) {
|
|
106
|
+
return Math.min(Math.max(seconds, 0) * 1e3, MAX_DELAY_MS);
|
|
107
|
+
}
|
|
108
|
+
const at = Date.parse(raw);
|
|
109
|
+
if (!Number.isNaN(at)) {
|
|
110
|
+
return Math.min(Math.max(0, at - Date.now()), MAX_DELAY_MS);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function backoffMs(attempt, res) {
|
|
115
|
+
const fromServer = res ? retryAfterMs(res) : null;
|
|
116
|
+
const base = Math.min(fromServer ?? BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS - JITTER_MS);
|
|
117
|
+
return base + Math.random() * JITTER_MS;
|
|
118
|
+
}
|
|
119
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
|
|
92
121
|
// src/methods/getContent.ts
|
|
93
122
|
async function getContent(client, slug) {
|
|
94
123
|
const data = await client.fetchJSON(client.url(slug));
|
|
@@ -153,14 +182,17 @@ function listContentAll(client, opts) {
|
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
// src/delivery-client.ts
|
|
185
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
156
186
|
var BetterCMSDeliveryClient = class {
|
|
157
187
|
workspace;
|
|
158
188
|
baseUrl;
|
|
189
|
+
timeout;
|
|
159
190
|
apiKey;
|
|
160
191
|
constructor(opts) {
|
|
161
192
|
this.workspace = opts.workspace;
|
|
162
193
|
this.baseUrl = opts.baseUrl;
|
|
163
194
|
this.apiKey = opts.apiKey;
|
|
195
|
+
this.timeout = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
164
196
|
}
|
|
165
197
|
getContent(slug) {
|
|
166
198
|
return getContent(this, slug);
|
|
@@ -193,24 +225,46 @@ var BetterCMSDeliveryClient = class {
|
|
|
193
225
|
return headers;
|
|
194
226
|
}
|
|
195
227
|
/**
|
|
196
|
-
* Generic fetch helper —
|
|
197
|
-
* and throws a typed BetterCMSError on non-OK responses.
|
|
228
|
+
* Generic fetch helper — retries 429/503 honouring Retry-After, applies a
|
|
229
|
+
* per-attempt timeout, and throws a typed BetterCMSError on non-OK responses.
|
|
230
|
+
*
|
|
231
|
+
* This path (BetterCMS.site()) previously had NO retry and NO timeout: the first
|
|
232
|
+
* 429 from the delivery limiter threw straight through and killed the caller. In a
|
|
233
|
+
* static build that is a failed deploy. See ./retry.ts for why a fixed ladder was
|
|
234
|
+
* never going to be enough against a 60-second fixed window.
|
|
198
235
|
*/
|
|
199
236
|
async fetchJSON(url) {
|
|
200
|
-
let
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
for (let attempt = 0; ; attempt++) {
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
240
|
+
let res;
|
|
241
|
+
try {
|
|
242
|
+
res = await globalThis.fetch(url, {
|
|
243
|
+
headers: this.headers(),
|
|
244
|
+
signal: controller.signal
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const timedOut = err instanceof Error && err.name === "AbortError";
|
|
248
|
+
if (attempt < MAX_RETRIES) {
|
|
249
|
+
await sleep(backoffMs(attempt));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (timedOut) throw new BetterCMSError("Request timeout", 408, "NETWORK_ERROR");
|
|
253
|
+
throw new BetterCMSError(
|
|
254
|
+
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
255
|
+
0,
|
|
256
|
+
"NETWORK_ERROR"
|
|
257
|
+
);
|
|
258
|
+
} finally {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
}
|
|
261
|
+
if (res.ok) return await res.json();
|
|
262
|
+
if (attempt < MAX_RETRIES && isRetryableStatus(res.status)) {
|
|
263
|
+
await sleep(backoffMs(attempt, res));
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
211
266
|
throw await BetterCMSError.from(res);
|
|
212
267
|
}
|
|
213
|
-
return await res.json();
|
|
214
268
|
}
|
|
215
269
|
};
|
|
216
270
|
|
|
@@ -291,19 +345,15 @@ function addWebhookMethods(client) {
|
|
|
291
345
|
|
|
292
346
|
// src/admin-client.ts
|
|
293
347
|
var DEFAULT_BASE_URL = "https://api.bettercms.ai/v1";
|
|
294
|
-
var DEFAULT_TIMEOUT = 1e4;
|
|
295
|
-
var RETRY_STATUSES = [429, 503];
|
|
296
|
-
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
297
348
|
var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
298
|
-
timeout;
|
|
299
349
|
_token;
|
|
300
350
|
constructor(options) {
|
|
301
351
|
super({
|
|
302
352
|
workspace: "",
|
|
303
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL
|
|
353
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
354
|
+
timeout: options.timeout
|
|
304
355
|
});
|
|
305
356
|
this._token = options.token;
|
|
306
|
-
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
307
357
|
}
|
|
308
358
|
headers() {
|
|
309
359
|
return {
|
|
@@ -313,12 +363,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
313
363
|
};
|
|
314
364
|
}
|
|
315
365
|
/**
|
|
316
|
-
* Admin fetchJSON with retry (429, 503) and per-request
|
|
317
|
-
* Each retry gets a fresh timeout.
|
|
366
|
+
* Admin fetchJSON with Retry-After-aware retry (429, 503) and a per-request
|
|
367
|
+
* timeout (AbortController). Each retry gets a fresh timeout.
|
|
318
368
|
*/
|
|
319
369
|
async fetchJSON(url, init) {
|
|
320
370
|
let lastError;
|
|
321
|
-
for (let attempt = 0; attempt <=
|
|
371
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
322
372
|
const controller = new AbortController();
|
|
323
373
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
324
374
|
let nonRetryable = false;
|
|
@@ -332,12 +382,12 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
332
382
|
if (res.ok) {
|
|
333
383
|
return res.json();
|
|
334
384
|
}
|
|
335
|
-
const shouldRetry = attempt <
|
|
385
|
+
const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
336
386
|
if (!shouldRetry) {
|
|
337
387
|
nonRetryable = true;
|
|
338
388
|
throw await BetterCMSError.from(res);
|
|
339
389
|
}
|
|
340
|
-
await sleep(
|
|
390
|
+
await sleep(backoffMs(attempt, res));
|
|
341
391
|
} catch (err) {
|
|
342
392
|
clearTimeout(timeoutId);
|
|
343
393
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -347,8 +397,8 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
347
397
|
throw err;
|
|
348
398
|
}
|
|
349
399
|
lastError = err;
|
|
350
|
-
if (attempt <
|
|
351
|
-
await sleep(
|
|
400
|
+
if (attempt < MAX_RETRIES) {
|
|
401
|
+
await sleep(backoffMs(attempt));
|
|
352
402
|
continue;
|
|
353
403
|
}
|
|
354
404
|
throw err;
|
|
@@ -365,9 +415,6 @@ var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
|
365
415
|
return addWebhookMethods(this);
|
|
366
416
|
}
|
|
367
417
|
};
|
|
368
|
-
function sleep(ms) {
|
|
369
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
370
|
-
}
|
|
371
418
|
|
|
372
419
|
// src/methods/management/content.ts
|
|
373
420
|
async function listModels(client) {
|
|
@@ -744,8 +791,6 @@ async function commandManagedLayout(client, input) {
|
|
|
744
791
|
// src/management-client.ts
|
|
745
792
|
var DEFAULT_BASE_URL2 = "https://api.bettercms.ai/v1";
|
|
746
793
|
var DEFAULT_TIMEOUT2 = 1e4;
|
|
747
|
-
var RETRY_STATUSES2 = [429, 503];
|
|
748
|
-
var RETRY_DELAYS2 = [1e3, 2e3, 4e3];
|
|
749
794
|
var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
750
795
|
timeout;
|
|
751
796
|
_apiKey;
|
|
@@ -767,10 +812,14 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
767
812
|
url(path) {
|
|
768
813
|
return `${this.baseUrl}${path}`;
|
|
769
814
|
}
|
|
770
|
-
/**
|
|
815
|
+
/**
|
|
816
|
+
* fetchJSON with Retry-After-aware retry (429, 503) + per-request timeout
|
|
817
|
+
* (mirrors the admin client). See ./retry.ts for why the delay must be able
|
|
818
|
+
* to outlast the backend's 60-second rate-limit window.
|
|
819
|
+
*/
|
|
771
820
|
async fetchJSON(url, init) {
|
|
772
821
|
let lastError;
|
|
773
|
-
for (let attempt = 0; attempt <=
|
|
822
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
774
823
|
const controller = new AbortController();
|
|
775
824
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
776
825
|
let nonRetryable = false;
|
|
@@ -784,12 +833,12 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
784
833
|
if (res.ok) {
|
|
785
834
|
return res.json();
|
|
786
835
|
}
|
|
787
|
-
const shouldRetry = attempt <
|
|
836
|
+
const shouldRetry = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
788
837
|
if (!shouldRetry) {
|
|
789
838
|
nonRetryable = true;
|
|
790
839
|
throw await BetterCMSError.from(res);
|
|
791
840
|
}
|
|
792
|
-
await
|
|
841
|
+
await sleep(backoffMs(attempt, res));
|
|
793
842
|
} catch (err) {
|
|
794
843
|
clearTimeout(timeoutId);
|
|
795
844
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -797,8 +846,8 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
797
846
|
}
|
|
798
847
|
if (nonRetryable) throw err;
|
|
799
848
|
lastError = err;
|
|
800
|
-
if (attempt <
|
|
801
|
-
await
|
|
849
|
+
if (attempt < MAX_RETRIES) {
|
|
850
|
+
await sleep(backoffMs(attempt));
|
|
802
851
|
continue;
|
|
803
852
|
}
|
|
804
853
|
throw err;
|
|
@@ -930,9 +979,6 @@ var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
|
930
979
|
return generateSeoMeta(this, input);
|
|
931
980
|
}
|
|
932
981
|
};
|
|
933
|
-
function sleep2(ms) {
|
|
934
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
935
|
-
}
|
|
936
982
|
|
|
937
983
|
// src/auth.ts
|
|
938
984
|
var AUTH_BASE_URL = "https://api.bettercms.ai/api/v1/auth";
|
|
@@ -1023,7 +1069,8 @@ var BetterCMS = {
|
|
|
1023
1069
|
return new BetterCMSDeliveryClient({
|
|
1024
1070
|
workspace: options.workspace,
|
|
1025
1071
|
apiKey: options.apiKey,
|
|
1026
|
-
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3
|
|
1072
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL3,
|
|
1073
|
+
timeout: options.timeout
|
|
1027
1074
|
});
|
|
1028
1075
|
},
|
|
1029
1076
|
/**
|
|
@@ -1055,10 +1102,7 @@ var BetterCMS = {
|
|
|
1055
1102
|
};
|
|
1056
1103
|
|
|
1057
1104
|
// src/read-client.ts
|
|
1058
|
-
var RETRY_STATUSES3 = [429, 503];
|
|
1059
|
-
var RETRY_DELAYS3 = [250, 750, 1500];
|
|
1060
1105
|
var DEFAULT_TIMEOUT3 = 1e4;
|
|
1061
|
-
var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1062
1106
|
function qs(params2) {
|
|
1063
1107
|
const sp = new URLSearchParams();
|
|
1064
1108
|
for (const [k, v] of Object.entries(params2)) {
|
|
@@ -1075,6 +1119,27 @@ function unwrap(json) {
|
|
|
1075
1119
|
}
|
|
1076
1120
|
return json;
|
|
1077
1121
|
}
|
|
1122
|
+
function createRequestCache() {
|
|
1123
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1124
|
+
const settled = /* @__PURE__ */ new Map();
|
|
1125
|
+
const copy = (value) => typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
1126
|
+
return async function collapse(url, fetcher) {
|
|
1127
|
+
if (settled.has(url)) return copy(settled.get(url));
|
|
1128
|
+
const pending = inflight.get(url);
|
|
1129
|
+
if (pending) return copy(await pending);
|
|
1130
|
+
const run = (async () => {
|
|
1131
|
+
try {
|
|
1132
|
+
const value = await fetcher();
|
|
1133
|
+
settled.set(url, value);
|
|
1134
|
+
return value;
|
|
1135
|
+
} finally {
|
|
1136
|
+
inflight.delete(url);
|
|
1137
|
+
}
|
|
1138
|
+
})();
|
|
1139
|
+
inflight.set(url, run);
|
|
1140
|
+
return copy(await run);
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1078
1143
|
function createClient(options) {
|
|
1079
1144
|
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
1080
1145
|
const { workspace, apiKey, perspective = "published", previewToken } = options;
|
|
@@ -1086,9 +1151,11 @@ function createClient(options) {
|
|
|
1086
1151
|
if (apiKey) h["X-API-Key"] = apiKey;
|
|
1087
1152
|
return h;
|
|
1088
1153
|
};
|
|
1089
|
-
|
|
1154
|
+
const collapse = options.cache && perspective === "published" ? createRequestCache() : void 0;
|
|
1155
|
+
const fetchJSON = (url) => collapse ? collapse(url, () => request(url)) : request(url);
|
|
1156
|
+
async function request(url) {
|
|
1090
1157
|
let lastError;
|
|
1091
|
-
for (let attempt = 0; attempt <=
|
|
1158
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
1092
1159
|
const controller = new AbortController();
|
|
1093
1160
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
1094
1161
|
try {
|
|
@@ -1098,9 +1165,9 @@ function createClient(options) {
|
|
|
1098
1165
|
});
|
|
1099
1166
|
clearTimeout(timer);
|
|
1100
1167
|
if (res.ok) return await res.json();
|
|
1101
|
-
const retryable = attempt <
|
|
1168
|
+
const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
1102
1169
|
if (!retryable) throw await BetterCMSError.from(res);
|
|
1103
|
-
await
|
|
1170
|
+
await sleep(backoffMs(attempt, res));
|
|
1104
1171
|
} catch (err) {
|
|
1105
1172
|
clearTimeout(timer);
|
|
1106
1173
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -1108,8 +1175,8 @@ function createClient(options) {
|
|
|
1108
1175
|
}
|
|
1109
1176
|
if (err instanceof BetterCMSError) throw err;
|
|
1110
1177
|
lastError = err;
|
|
1111
|
-
if (attempt <
|
|
1112
|
-
await
|
|
1178
|
+
if (attempt < MAX_RETRIES) {
|
|
1179
|
+
await sleep(backoffMs(attempt));
|
|
1113
1180
|
continue;
|
|
1114
1181
|
}
|
|
1115
1182
|
throw new BetterCMSError(
|