@paragraphcms/client 1.0.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/README.md +473 -0
- package/dist/client.d.ts +82 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +506 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/rate-limiter.d.ts +9 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +35 -0
- package/dist/types.d.ts +397 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +45 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { ParagraphApiError, ParagraphClientError, } from "./errors.js";
|
|
2
|
+
import { RequestRateLimiter } from "./rate-limiter.js";
|
|
3
|
+
const DEFAULT_BASE_URL = "https://api.paragraphcms.com/v1";
|
|
4
|
+
const DEFAULT_REQUESTS_PER_SECOND = 5;
|
|
5
|
+
function normalizeBaseUrl(baseUrl) {
|
|
6
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
throw new ParagraphClientError("`baseUrl` cannot be empty.");
|
|
9
|
+
}
|
|
10
|
+
const url = new URL(trimmed);
|
|
11
|
+
const normalizedPath = url.pathname.replace(/\/+$/, "");
|
|
12
|
+
if (normalizedPath === "" || normalizedPath === "/") {
|
|
13
|
+
url.pathname = "/v1";
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
url.pathname = normalizedPath;
|
|
17
|
+
}
|
|
18
|
+
return url.toString().replace(/\/+$/, "");
|
|
19
|
+
}
|
|
20
|
+
function buildUrl(baseUrl, path, query) {
|
|
21
|
+
const normalizedPath = path ? `/${path.replace(/^\/+/, "")}` : "";
|
|
22
|
+
const url = new URL(`${baseUrl}${normalizedPath}`);
|
|
23
|
+
if (query) {
|
|
24
|
+
for (const [key, rawValue] of Object.entries(query)) {
|
|
25
|
+
if (rawValue === undefined || rawValue === null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(rawValue)) {
|
|
29
|
+
if (rawValue.length === 0) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
url.searchParams.set(key, rawValue.map((value) => String(value)).join(","));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
url.searchParams.set(key, String(rawValue));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
function isApiErrorPayload(value) {
|
|
41
|
+
if (typeof value !== "object" || value === null) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const maybeError = value.error;
|
|
45
|
+
if (typeof maybeError !== "object" || maybeError === null) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const code = maybeError.code;
|
|
49
|
+
const message = maybeError.message;
|
|
50
|
+
return typeof code === "string" && typeof message === "string";
|
|
51
|
+
}
|
|
52
|
+
async function parseResponse(response) {
|
|
53
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
54
|
+
if (contentType.includes("application/json")) {
|
|
55
|
+
return response.json();
|
|
56
|
+
}
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
if (!text) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function createRequestDescriptor(method, url) {
|
|
69
|
+
return {
|
|
70
|
+
method,
|
|
71
|
+
url: url.toString(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function createRequestSignal(signal, timeoutMs) {
|
|
75
|
+
if (!signal && (!timeoutMs || timeoutMs <= 0)) {
|
|
76
|
+
return {
|
|
77
|
+
signal: undefined,
|
|
78
|
+
cleanup: () => { },
|
|
79
|
+
didTimeout: () => false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
let timedOut = false;
|
|
84
|
+
let timeoutId;
|
|
85
|
+
const onAbort = () => {
|
|
86
|
+
controller.abort(signal?.reason);
|
|
87
|
+
};
|
|
88
|
+
if (signal) {
|
|
89
|
+
if (signal.aborted) {
|
|
90
|
+
controller.abort(signal.reason);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
97
|
+
timeoutId = setTimeout(() => {
|
|
98
|
+
timedOut = true;
|
|
99
|
+
controller.abort(new Error(`Request timed out after ${timeoutMs}ms.`));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
cleanup: () => {
|
|
105
|
+
if (timeoutId !== undefined) {
|
|
106
|
+
clearTimeout(timeoutId);
|
|
107
|
+
}
|
|
108
|
+
if (signal) {
|
|
109
|
+
signal.removeEventListener("abort", onAbort);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
didTimeout: () => timedOut,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function isAbortError(error) {
|
|
116
|
+
return (error instanceof DOMException && error.name === "AbortError");
|
|
117
|
+
}
|
|
118
|
+
function toBlobPart(file) {
|
|
119
|
+
if (typeof File !== "undefined" && file instanceof File) {
|
|
120
|
+
return {
|
|
121
|
+
value: file,
|
|
122
|
+
fileName: undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (file instanceof Blob) {
|
|
126
|
+
return {
|
|
127
|
+
value: file,
|
|
128
|
+
fileName: undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (ArrayBuffer.isView(file)) {
|
|
132
|
+
const source = new Uint8Array(file.buffer, file.byteOffset, file.byteLength);
|
|
133
|
+
const view = new Uint8Array(source.byteLength);
|
|
134
|
+
view.set(source);
|
|
135
|
+
return {
|
|
136
|
+
value: new Blob([view]),
|
|
137
|
+
fileName: undefined,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
value: new Blob([file]),
|
|
142
|
+
fileName: undefined,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function buildUploadFilePart(input) {
|
|
146
|
+
const source = toBlobPart(input.file);
|
|
147
|
+
const fileName = input.file_name ??
|
|
148
|
+
(typeof File !== "undefined" && input.file instanceof File
|
|
149
|
+
? input.file.name
|
|
150
|
+
: "upload.bin");
|
|
151
|
+
const contentType = input.content_type ??
|
|
152
|
+
("type" in source.value &&
|
|
153
|
+
typeof source.value.type === "string" &&
|
|
154
|
+
source.value.type.length > 0
|
|
155
|
+
? source.value.type
|
|
156
|
+
: "application/octet-stream");
|
|
157
|
+
if (typeof File !== "undefined") {
|
|
158
|
+
if (source.value instanceof File) {
|
|
159
|
+
if (source.value.name === fileName &&
|
|
160
|
+
source.value.type === contentType) {
|
|
161
|
+
return {
|
|
162
|
+
value: source.value,
|
|
163
|
+
fileName: undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
value: new File([source.value], fileName, {
|
|
168
|
+
type: contentType,
|
|
169
|
+
lastModified: source.value.lastModified,
|
|
170
|
+
}),
|
|
171
|
+
fileName: undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
value: new File([source.value], fileName, {
|
|
176
|
+
type: contentType,
|
|
177
|
+
}),
|
|
178
|
+
fileName: undefined,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
value: new Blob([source.value], {
|
|
183
|
+
type: contentType,
|
|
184
|
+
}),
|
|
185
|
+
fileName,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function createUploadFormData(input) {
|
|
189
|
+
const formData = new FormData();
|
|
190
|
+
const filePart = buildUploadFilePart(input);
|
|
191
|
+
if (filePart.fileName) {
|
|
192
|
+
formData.append("file", filePart.value, filePart.fileName);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
formData.append("file", filePart.value);
|
|
196
|
+
}
|
|
197
|
+
formData.append("page_id", input.page_id);
|
|
198
|
+
if (input.alt !== undefined && input.alt !== null) {
|
|
199
|
+
formData.append("alt", input.alt);
|
|
200
|
+
}
|
|
201
|
+
return formData;
|
|
202
|
+
}
|
|
203
|
+
export class Client {
|
|
204
|
+
apiKey;
|
|
205
|
+
baseUrl;
|
|
206
|
+
fetchImpl;
|
|
207
|
+
defaultHeaders;
|
|
208
|
+
timeoutMs;
|
|
209
|
+
limiter;
|
|
210
|
+
pages = {
|
|
211
|
+
list: (query, options) => this.requestList("GET", "/pages", {
|
|
212
|
+
query,
|
|
213
|
+
options,
|
|
214
|
+
}),
|
|
215
|
+
create: (body = {}, options) => this.requestData("POST", "/pages", {
|
|
216
|
+
body,
|
|
217
|
+
options,
|
|
218
|
+
}),
|
|
219
|
+
get: (pageId, query, options) => this.requestData("GET", `/pages/${pageId}`, {
|
|
220
|
+
query,
|
|
221
|
+
options,
|
|
222
|
+
}),
|
|
223
|
+
update: (pageId, body, options) => this.requestData("PATCH", `/pages/${pageId}`, {
|
|
224
|
+
body,
|
|
225
|
+
options,
|
|
226
|
+
}),
|
|
227
|
+
delete: (pageId, options) => this.requestData("DELETE", `/pages/${pageId}`, {
|
|
228
|
+
options,
|
|
229
|
+
}),
|
|
230
|
+
restore: (pageId, options) => this.requestData("POST", `/pages/${pageId}/restore`, {
|
|
231
|
+
options,
|
|
232
|
+
}),
|
|
233
|
+
permanentlyDelete: (pageId, options) => this.requestData("DELETE", `/pages/${pageId}/permanent`, {
|
|
234
|
+
options,
|
|
235
|
+
}),
|
|
236
|
+
duplicate: (pageId, options) => this.requestData("POST", `/pages/${pageId}/duplicate`, {
|
|
237
|
+
options,
|
|
238
|
+
}),
|
|
239
|
+
createTranslation: (pageId, body, options) => this.requestData("POST", `/pages/${pageId}/translations`, {
|
|
240
|
+
body,
|
|
241
|
+
options,
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
collections = {
|
|
245
|
+
list: (query, options) => this.requestList("GET", "/collections", {
|
|
246
|
+
query,
|
|
247
|
+
options,
|
|
248
|
+
}),
|
|
249
|
+
create: (body, options) => this.requestData("POST", "/collections", {
|
|
250
|
+
body,
|
|
251
|
+
options,
|
|
252
|
+
}),
|
|
253
|
+
get: (collectionId, options) => this.requestData("GET", `/collections/${collectionId}`, {
|
|
254
|
+
options,
|
|
255
|
+
}),
|
|
256
|
+
update: (collectionId, body, options) => this.requestData("PATCH", `/collections/${collectionId}`, {
|
|
257
|
+
body,
|
|
258
|
+
options,
|
|
259
|
+
}),
|
|
260
|
+
delete: (collectionId, options) => this.requestData("DELETE", `/collections/${collectionId}`, {
|
|
261
|
+
options,
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
media = {
|
|
265
|
+
list: (query, options) => this.requestList("GET", "/media", {
|
|
266
|
+
query,
|
|
267
|
+
options,
|
|
268
|
+
}),
|
|
269
|
+
upload: (body, options) => this.requestData("POST", "/media", {
|
|
270
|
+
formData: createUploadFormData(body),
|
|
271
|
+
options,
|
|
272
|
+
}),
|
|
273
|
+
get: (mediaId, options) => this.requestData("GET", `/media/${mediaId}`, {
|
|
274
|
+
options,
|
|
275
|
+
}),
|
|
276
|
+
update: (mediaId, body, options) => this.requestData("PATCH", `/media/${mediaId}`, {
|
|
277
|
+
body,
|
|
278
|
+
options,
|
|
279
|
+
}),
|
|
280
|
+
delete: (mediaId, options) => this.requestData("DELETE", `/media/${mediaId}`, {
|
|
281
|
+
options,
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
members = {
|
|
285
|
+
list: (query, options) => this.requestList("GET", "/members", {
|
|
286
|
+
query,
|
|
287
|
+
options,
|
|
288
|
+
}),
|
|
289
|
+
};
|
|
290
|
+
authors = {
|
|
291
|
+
list: (query, options) => this.requestList("GET", "/authors", {
|
|
292
|
+
query,
|
|
293
|
+
options,
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
reviewers = {
|
|
297
|
+
list: (query, options) => this.requestList("GET", "/reviewers", {
|
|
298
|
+
query,
|
|
299
|
+
options,
|
|
300
|
+
}),
|
|
301
|
+
};
|
|
302
|
+
statuses = {
|
|
303
|
+
list: (query, options) => this.requestList("GET", "/statuses", {
|
|
304
|
+
query,
|
|
305
|
+
options,
|
|
306
|
+
}),
|
|
307
|
+
create: (body, options) => this.requestData("POST", "/statuses", {
|
|
308
|
+
body,
|
|
309
|
+
options,
|
|
310
|
+
}),
|
|
311
|
+
get: (statusId, options) => this.requestData("GET", `/statuses/${statusId}`, {
|
|
312
|
+
options,
|
|
313
|
+
}),
|
|
314
|
+
update: (statusId, body, options) => this.requestData("PATCH", `/statuses/${statusId}`, {
|
|
315
|
+
body,
|
|
316
|
+
options,
|
|
317
|
+
}),
|
|
318
|
+
reorder: (body, options) => this.requestData("POST", "/statuses/reorder", {
|
|
319
|
+
body,
|
|
320
|
+
options,
|
|
321
|
+
}),
|
|
322
|
+
delete: (statusId, options) => this.requestData("DELETE", `/statuses/${statusId}`, {
|
|
323
|
+
options,
|
|
324
|
+
}),
|
|
325
|
+
};
|
|
326
|
+
labels = {
|
|
327
|
+
list: (query, options) => this.requestList("GET", "/labels", {
|
|
328
|
+
query,
|
|
329
|
+
options,
|
|
330
|
+
}),
|
|
331
|
+
create: (body, options) => this.requestData("POST", "/labels", {
|
|
332
|
+
body,
|
|
333
|
+
options,
|
|
334
|
+
}),
|
|
335
|
+
get: (labelId, options) => this.requestData("GET", `/labels/${labelId}`, {
|
|
336
|
+
options,
|
|
337
|
+
}),
|
|
338
|
+
update: (labelId, body, options) => this.requestData("PATCH", `/labels/${labelId}`, {
|
|
339
|
+
body,
|
|
340
|
+
options,
|
|
341
|
+
}),
|
|
342
|
+
reorder: (body, options) => this.requestData("POST", "/labels/reorder", {
|
|
343
|
+
body,
|
|
344
|
+
options,
|
|
345
|
+
}),
|
|
346
|
+
delete: (labelId, options) => this.requestData("DELETE", `/labels/${labelId}`, {
|
|
347
|
+
options,
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
dataModels = {
|
|
351
|
+
list: (query, options) => this.requestList("GET", "/data-models", {
|
|
352
|
+
query,
|
|
353
|
+
options,
|
|
354
|
+
}),
|
|
355
|
+
create: (body, options) => this.requestData("POST", "/data-models", {
|
|
356
|
+
body,
|
|
357
|
+
options,
|
|
358
|
+
}),
|
|
359
|
+
get: (dataModelId, options) => this.requestData("GET", `/data-models/${dataModelId}`, {
|
|
360
|
+
options,
|
|
361
|
+
}),
|
|
362
|
+
update: (dataModelId, body, options) => this.requestData("PATCH", `/data-models/${dataModelId}`, {
|
|
363
|
+
body,
|
|
364
|
+
options,
|
|
365
|
+
}),
|
|
366
|
+
delete: (dataModelId, options) => this.requestData("DELETE", `/data-models/${dataModelId}`, {
|
|
367
|
+
options,
|
|
368
|
+
}),
|
|
369
|
+
};
|
|
370
|
+
locales = {
|
|
371
|
+
list: (options) => this.requestData("GET", "/locales", {
|
|
372
|
+
options,
|
|
373
|
+
}),
|
|
374
|
+
create: (body, options) => this.requestData("POST", "/locales", {
|
|
375
|
+
body,
|
|
376
|
+
options,
|
|
377
|
+
}),
|
|
378
|
+
delete: (code, options) => this.requestData("DELETE", `/locales/${code}`, {
|
|
379
|
+
options,
|
|
380
|
+
}),
|
|
381
|
+
};
|
|
382
|
+
ai = {
|
|
383
|
+
generateMetaName: (body, options) => this.requestData("POST", "/ai/meta-name", {
|
|
384
|
+
body,
|
|
385
|
+
options,
|
|
386
|
+
}),
|
|
387
|
+
generateMetaDescription: (body, options) => this.requestData("POST", "/ai/meta-description", {
|
|
388
|
+
body,
|
|
389
|
+
options,
|
|
390
|
+
}),
|
|
391
|
+
generateContent: (body, options) => this.requestData("POST", "/ai/content", {
|
|
392
|
+
body,
|
|
393
|
+
options,
|
|
394
|
+
}),
|
|
395
|
+
};
|
|
396
|
+
constructor(options) {
|
|
397
|
+
if (!options.apiKey.trim()) {
|
|
398
|
+
throw new ParagraphClientError("`apiKey` is required.");
|
|
399
|
+
}
|
|
400
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
401
|
+
if (typeof fetchImpl !== "function") {
|
|
402
|
+
throw new ParagraphClientError("No fetch implementation available. Pass `fetch` in the client options.");
|
|
403
|
+
}
|
|
404
|
+
this.apiKey = options.apiKey.trim();
|
|
405
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
406
|
+
this.fetchImpl = fetchImpl;
|
|
407
|
+
this.defaultHeaders = new Headers(options.headers);
|
|
408
|
+
this.timeoutMs = options.timeoutMs;
|
|
409
|
+
this.limiter = new RequestRateLimiter(options.maxRequestsPerSecond ?? DEFAULT_REQUESTS_PER_SECOND);
|
|
410
|
+
}
|
|
411
|
+
getInfo(options) {
|
|
412
|
+
return this.requestData("GET", "", { options });
|
|
413
|
+
}
|
|
414
|
+
requestData(method, path, config) {
|
|
415
|
+
return this.requestJson(method, path, config).then((response) => response.data);
|
|
416
|
+
}
|
|
417
|
+
requestList(method, path, config) {
|
|
418
|
+
return this.requestJson(method, path, config);
|
|
419
|
+
}
|
|
420
|
+
requestJson(method, path, config) {
|
|
421
|
+
const url = buildUrl(this.baseUrl, path, config?.query);
|
|
422
|
+
const request = createRequestDescriptor(method, url);
|
|
423
|
+
return this.limiter.schedule(async () => {
|
|
424
|
+
const headers = new Headers(this.defaultHeaders);
|
|
425
|
+
const timeoutMs = config?.options?.timeoutMs ?? this.timeoutMs;
|
|
426
|
+
headers.set("accept", "application/json");
|
|
427
|
+
if (!headers.has("x-api-key") &&
|
|
428
|
+
!headers.has("authorization")) {
|
|
429
|
+
headers.set("x-api-key", this.apiKey);
|
|
430
|
+
}
|
|
431
|
+
let body;
|
|
432
|
+
if (config?.formData) {
|
|
433
|
+
headers.delete("content-type");
|
|
434
|
+
body = config.formData;
|
|
435
|
+
}
|
|
436
|
+
else if (config?.body !== undefined) {
|
|
437
|
+
if (!headers.has("content-type")) {
|
|
438
|
+
headers.set("content-type", "application/json");
|
|
439
|
+
}
|
|
440
|
+
body = JSON.stringify(config.body);
|
|
441
|
+
}
|
|
442
|
+
const optionHeaders = new Headers(config?.options?.headers);
|
|
443
|
+
for (const [key, value] of optionHeaders.entries()) {
|
|
444
|
+
headers.set(key, value);
|
|
445
|
+
}
|
|
446
|
+
if (config?.formData) {
|
|
447
|
+
headers.delete("content-type");
|
|
448
|
+
}
|
|
449
|
+
const requestSignal = createRequestSignal(config?.options?.signal, timeoutMs);
|
|
450
|
+
try {
|
|
451
|
+
const response = await this.fetchImpl(url, {
|
|
452
|
+
method,
|
|
453
|
+
headers,
|
|
454
|
+
body,
|
|
455
|
+
signal: requestSignal.signal,
|
|
456
|
+
});
|
|
457
|
+
const payload = await parseResponse(response);
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
if (isApiErrorPayload(payload)) {
|
|
460
|
+
throw new ParagraphApiError({
|
|
461
|
+
status: response.status,
|
|
462
|
+
code: payload.error.code,
|
|
463
|
+
message: payload.error.message,
|
|
464
|
+
details: payload.error.details,
|
|
465
|
+
headers: new Headers(response.headers),
|
|
466
|
+
request,
|
|
467
|
+
body: payload,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
throw new ParagraphApiError({
|
|
471
|
+
status: response.status,
|
|
472
|
+
code: response.status === 401 ? "unauthorized" : "request_failed",
|
|
473
|
+
message: typeof payload === "string" && payload.length > 0
|
|
474
|
+
? payload
|
|
475
|
+
: response.statusText || "Request failed.",
|
|
476
|
+
headers: new Headers(response.headers),
|
|
477
|
+
request,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return payload;
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
if (error instanceof ParagraphApiError ||
|
|
484
|
+
error instanceof ParagraphClientError) {
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
if (requestSignal.didTimeout()) {
|
|
488
|
+
throw new ParagraphClientError(`Request timed out after ${timeoutMs}ms.`, {
|
|
489
|
+
request,
|
|
490
|
+
cause: error,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (isAbortError(error)) {
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
throw new ParagraphClientError("Request failed.", {
|
|
497
|
+
request,
|
|
498
|
+
cause: error,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
requestSignal.cleanup();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApiErrorPayload, RequestDescriptor } from "./types.js";
|
|
2
|
+
export declare class ParagraphApiError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly details: unknown;
|
|
6
|
+
readonly headers: Headers;
|
|
7
|
+
readonly request: RequestDescriptor;
|
|
8
|
+
readonly body?: ApiErrorPayload;
|
|
9
|
+
constructor(options: {
|
|
10
|
+
status: number;
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
details?: unknown;
|
|
14
|
+
headers?: Headers;
|
|
15
|
+
request: RequestDescriptor;
|
|
16
|
+
body?: ApiErrorPayload;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export declare class ParagraphClientError extends Error {
|
|
20
|
+
readonly request?: RequestDescriptor;
|
|
21
|
+
readonly cause?: unknown;
|
|
22
|
+
constructor(message: string, options?: {
|
|
23
|
+
request?: RequestDescriptor;
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;gBAEpB,OAAO,EAAE;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,EAAE,iBAAiB,CAAC;QAC3B,IAAI,CAAC,EAAE,eAAe,CAAC;KACxB;CAUF;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IACrC,SAAkB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAGhC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,iBAAiB,CAAC;QAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAOJ"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class ParagraphApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
headers;
|
|
6
|
+
request;
|
|
7
|
+
body;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
super(options.message);
|
|
10
|
+
this.name = "ParagraphApiError";
|
|
11
|
+
this.status = options.status;
|
|
12
|
+
this.code = options.code;
|
|
13
|
+
this.details = options.details;
|
|
14
|
+
this.headers = options.headers ?? new Headers();
|
|
15
|
+
this.request = options.request;
|
|
16
|
+
this.body = options.body;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class ParagraphClientError extends Error {
|
|
20
|
+
request;
|
|
21
|
+
cause;
|
|
22
|
+
constructor(message, options) {
|
|
23
|
+
super(message, options?.cause ? { cause: options.cause } : undefined);
|
|
24
|
+
this.name = "ParagraphClientError";
|
|
25
|
+
this.request = options?.request;
|
|
26
|
+
this.cause = options?.cause;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,mBAAmB,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class RequestRateLimiter {
|
|
2
|
+
private readonly minIntervalMs;
|
|
3
|
+
private nextAvailableAt;
|
|
4
|
+
private scheduling;
|
|
5
|
+
constructor(requestsPerSecond: number);
|
|
6
|
+
schedule<T>(task: () => Promise<T>): Promise<T>;
|
|
7
|
+
private reserveSlot;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAQA,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,UAAU,CAAoC;gBAE1C,iBAAiB,EAAE,MAAM;IAarC,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;IAKlC,OAAO,CAAC,WAAW;CAiBpB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ParagraphClientError } from "./errors.js";
|
|
2
|
+
function sleep(ms) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
setTimeout(resolve, ms);
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
export class RequestRateLimiter {
|
|
8
|
+
minIntervalMs;
|
|
9
|
+
nextAvailableAt = 0;
|
|
10
|
+
scheduling = Promise.resolve();
|
|
11
|
+
constructor(requestsPerSecond) {
|
|
12
|
+
if (!Number.isFinite(requestsPerSecond) ||
|
|
13
|
+
requestsPerSecond <= 0) {
|
|
14
|
+
throw new ParagraphClientError("`maxRequestsPerSecond` must be a positive number.");
|
|
15
|
+
}
|
|
16
|
+
this.minIntervalMs = Math.ceil(1000 / requestsPerSecond);
|
|
17
|
+
}
|
|
18
|
+
schedule(task) {
|
|
19
|
+
const slot = this.reserveSlot();
|
|
20
|
+
return slot.then(task);
|
|
21
|
+
}
|
|
22
|
+
reserveSlot() {
|
|
23
|
+
const slot = this.scheduling.then(async () => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const scheduledAt = Math.max(now, this.nextAvailableAt);
|
|
26
|
+
const delayMs = scheduledAt - now;
|
|
27
|
+
this.nextAvailableAt = scheduledAt + this.minIntervalMs;
|
|
28
|
+
if (delayMs > 0) {
|
|
29
|
+
await sleep(delayMs);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
this.scheduling = slot.catch(() => { });
|
|
33
|
+
return slot;
|
|
34
|
+
}
|
|
35
|
+
}
|