wontopos 2.2.39 → 2.2.41
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 +9 -9
- package/dist/cjs/wontopos.d.cts +360 -189
- package/dist/cjs/wontopos.js +612 -267
- package/dist/wontopos.d.ts +360 -189
- package/dist/wontopos.js +612 -267
- package/package.json +1 -1
package/dist/cjs/wontopos.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WME = exports.Client = exports.ServerError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.AuthenticationError = exports.BadRequestError = exports.APIConnectionError = exports.WosError = exports.CONTEXT_LIMIT_MAX = exports.CONTEXT_LIMIT_MIN = exports.SEARCH_LIMIT_MAX = exports.SEARCH_LIMIT_MIN = void 0;
|
|
4
4
|
exports._resetStoreIdWarnings = _resetStoreIdWarnings;
|
|
5
5
|
exports._resetFilterWarnings = _resetFilterWarnings;
|
|
6
|
-
const VERSION = "2.2.
|
|
6
|
+
const VERSION = "2.2.41";
|
|
7
7
|
const RUNTIME = (() => {
|
|
8
8
|
const p = globalThis?.process;
|
|
9
9
|
return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
|
|
@@ -18,12 +18,15 @@ function logDebug(msg) {
|
|
|
18
18
|
const DEFAULT_BASE_URL = "https://api.wontopos.com";
|
|
19
19
|
const DEFAULT_MODEL = "tablet-2";
|
|
20
20
|
const RETRY_ALWAYS = new Set([429]);
|
|
21
|
+
const MAX_RETRY_WAIT_MS = 30_000;
|
|
22
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
21
23
|
const RETRY_IF_IDEMPOTENT = new Set([408, 502, 503, 504]);
|
|
22
24
|
const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
23
25
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
24
26
|
const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
25
27
|
const MODEL_RE = /^[A-Za-z0-9._-]+$/;
|
|
26
28
|
const MAX_ERR_MSG = 4096;
|
|
29
|
+
const MAX_ERR_DETAILS = 8192;
|
|
27
30
|
const IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9._:\-]{1,128}$/;
|
|
28
31
|
function normalizeStoreId(id) {
|
|
29
32
|
return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
@@ -40,8 +43,9 @@ const warnedStoreIds = new Set();
|
|
|
40
43
|
function warnIfStoreIdCollapses(id) {
|
|
41
44
|
if (!id || warnedStoreIds.has(id))
|
|
42
45
|
return;
|
|
46
|
+
const valid = STORE_ID_FORMAT.test(id);
|
|
43
47
|
const normalized = normalizeStoreId(id);
|
|
44
|
-
if (normalized === id)
|
|
48
|
+
if (valid && normalized === id)
|
|
45
49
|
return;
|
|
46
50
|
warnedStoreIds.add(id);
|
|
47
51
|
while (warnedStoreIds.size > WARNED_STORE_IDS_MAX) {
|
|
@@ -50,16 +54,36 @@ function warnIfStoreIdCollapses(id) {
|
|
|
50
54
|
break;
|
|
51
55
|
warnedStoreIds.delete(oldest);
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
if (!valid) {
|
|
58
|
+
console.warn(`wontopos: store id ${JSON.stringify(id)} is not a valid store id: use 1-64 ASCII letters, ` +
|
|
59
|
+
`digits, ".", "_" and "-", starting with a letter or digit. Creating it is refused (400).`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.warn(`wontopos: store id ${JSON.stringify(id)} normalizes to ${JSON.stringify(normalized)}. ` +
|
|
63
|
+
`Ids that differ only by case name this same store; one that differs only by punctuation ` +
|
|
64
|
+
`cannot be created beside it (409) and is not found when used (404). If these ids come from ` +
|
|
65
|
+
`your end users, normalize them yourself first so two people never compete for one name.`);
|
|
66
|
+
}
|
|
67
|
+
const STORE_ID_FORMAT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
68
|
+
const NOT_METADATA = new Set(["userid", "storeid", "idempotencykey"]);
|
|
69
|
+
function checkMetadataKeys(metadata) {
|
|
70
|
+
if (metadata == null || typeof metadata !== "object")
|
|
71
|
+
return;
|
|
72
|
+
for (const k of Object.keys(metadata)) {
|
|
73
|
+
if (NOT_METADATA.has(k.toLowerCase().replace(/[\s_.-]/g, ""))) {
|
|
74
|
+
throw new Error(`${JSON.stringify(k)} is not a metadata field: pass the store as userId and an ` +
|
|
75
|
+
`idempotency key as opts.idempotencyKey. Nothing was sent.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
57
78
|
}
|
|
58
79
|
function _resetStoreIdWarnings() {
|
|
59
80
|
warnedStoreIds.clear();
|
|
60
81
|
}
|
|
61
82
|
const NO_BODY_STATUS = new Set([204, 205, 304]);
|
|
62
83
|
const MAX_PAGES = 20_000;
|
|
84
|
+
function truncatedWalk(why) {
|
|
85
|
+
return new Error(`${why} This is a truncated answer, not the whole store.`);
|
|
86
|
+
}
|
|
63
87
|
exports.SEARCH_LIMIT_MIN = 5;
|
|
64
88
|
exports.SEARCH_LIMIT_MAX = 20;
|
|
65
89
|
exports.CONTEXT_LIMIT_MIN = 0;
|
|
@@ -76,6 +100,26 @@ function checkContextLimit(n) {
|
|
|
76
100
|
throw new Error(`context_limit must be an integer between ${exports.CONTEXT_LIMIT_MIN} and ${exports.CONTEXT_LIMIT_MAX}, got ${n}.`);
|
|
77
101
|
}
|
|
78
102
|
}
|
|
103
|
+
const PAGE_LIMIT_MIN = 5;
|
|
104
|
+
const PAGE_LIMIT_MAX = 20;
|
|
105
|
+
const LIST_LIMIT_MAX = 500;
|
|
106
|
+
const MAX_IMAGES_MAX = 5;
|
|
107
|
+
function checkRange(n, name, min, max) {
|
|
108
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < min || n > max) {
|
|
109
|
+
throw new Error(`${name} must be an integer between ${min} and ${max}, got ${String(n)}.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function optRange(n, name, min, max) {
|
|
113
|
+
if (n === undefined || n === null)
|
|
114
|
+
return undefined;
|
|
115
|
+
checkRange(n, name, min, max);
|
|
116
|
+
return n;
|
|
117
|
+
}
|
|
118
|
+
function checkMs(v) {
|
|
119
|
+
if (typeof v !== "number" || Number.isNaN(v) || v <= 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return Math.min(v, MAX_TIMER_MS);
|
|
122
|
+
}
|
|
79
123
|
function normalizeImage(image) {
|
|
80
124
|
if (!image || typeof image !== "object") {
|
|
81
125
|
throw new Error("image must be an object — { data: '<base64>' }.");
|
|
@@ -105,14 +149,18 @@ const CONNECT_FAIL_CODES = new Set([
|
|
|
105
149
|
"EADDRNOTAVAIL",
|
|
106
150
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
107
151
|
]);
|
|
152
|
+
const CONNECT_SYSCALLS = new Set(["connect", "getaddrinfo"]);
|
|
153
|
+
const CONNECT_ONLY_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
108
154
|
function isConnectFailure(e) {
|
|
109
155
|
const stack = [e];
|
|
110
156
|
for (let steps = 0; stack.length && steps < 24; steps++) {
|
|
111
157
|
const cur = stack.pop();
|
|
112
158
|
if (!cur || typeof cur !== "object")
|
|
113
159
|
continue;
|
|
114
|
-
if (typeof cur.code === "string" &&
|
|
160
|
+
if (typeof cur.code === "string" &&
|
|
161
|
+
(CONNECT_ONLY_CODES.has(cur.code) || (CONNECT_FAIL_CODES.has(cur.code) && CONNECT_SYSCALLS.has(cur.syscall)))) {
|
|
115
162
|
return true;
|
|
163
|
+
}
|
|
116
164
|
if (cur.cause)
|
|
117
165
|
stack.push(cur.cause);
|
|
118
166
|
if (Array.isArray(cur.errors))
|
|
@@ -120,6 +168,191 @@ function isConnectFailure(e) {
|
|
|
120
168
|
}
|
|
121
169
|
return false;
|
|
122
170
|
}
|
|
171
|
+
function causeCode(e) {
|
|
172
|
+
const queue = [e];
|
|
173
|
+
for (let steps = 0; queue.length && steps < 24; steps++) {
|
|
174
|
+
const cur = queue.shift();
|
|
175
|
+
if (!cur || typeof cur !== "object")
|
|
176
|
+
continue;
|
|
177
|
+
if (typeof cur.code === "string" && /^[A-Z0-9_]{1,64}$/.test(cur.code))
|
|
178
|
+
return cur.code;
|
|
179
|
+
if (cur.cause)
|
|
180
|
+
queue.push(cur.cause);
|
|
181
|
+
if (Array.isArray(cur.errors))
|
|
182
|
+
queue.push(...cur.errors.slice(0, 8));
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
function cleanServerText(s, max = MAX_ERR_MSG) {
|
|
187
|
+
const clean = s.replace(/[\u0000-\u001f\u007f]/g, "");
|
|
188
|
+
return clean.length > max ? clean.slice(0, max) + "…(truncated)" : clean;
|
|
189
|
+
}
|
|
190
|
+
function cleanDetails(d) {
|
|
191
|
+
if (!d)
|
|
192
|
+
return undefined;
|
|
193
|
+
const clean = (v) => {
|
|
194
|
+
if (typeof v === "string")
|
|
195
|
+
return cleanServerText(v);
|
|
196
|
+
if (Array.isArray(v))
|
|
197
|
+
return v.map(clean);
|
|
198
|
+
if (v && typeof v === "object") {
|
|
199
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [cleanServerText(k), clean(x)]));
|
|
200
|
+
}
|
|
201
|
+
return v;
|
|
202
|
+
};
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
JSON.stringify(d);
|
|
206
|
+
entries = Object.entries(clean(d)).map(([k, v]) => [
|
|
207
|
+
k,
|
|
208
|
+
v,
|
|
209
|
+
JSON.stringify(k).length + JSON.stringify(v).length + 2,
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
const keep = new Set();
|
|
216
|
+
let size = 2;
|
|
217
|
+
for (const [k, , n] of [...entries].sort((a, b) => a[2] - b[2])) {
|
|
218
|
+
if (size + n > MAX_ERR_DETAILS)
|
|
219
|
+
break;
|
|
220
|
+
keep.add(k);
|
|
221
|
+
size += n;
|
|
222
|
+
}
|
|
223
|
+
if (!keep.size)
|
|
224
|
+
return undefined;
|
|
225
|
+
return Object.fromEntries(entries.filter(([k]) => keep.has(k)).map(([k, v]) => [k, v]));
|
|
226
|
+
}
|
|
227
|
+
function parseErrorText(text) {
|
|
228
|
+
let data;
|
|
229
|
+
try {
|
|
230
|
+
data = JSON.parse(text);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return { message: text };
|
|
234
|
+
}
|
|
235
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
236
|
+
return { message: text };
|
|
237
|
+
const err = data.error;
|
|
238
|
+
if (err && typeof err === "object" && !Array.isArray(err)) {
|
|
239
|
+
const { message, type, request_id, ...rest } = err;
|
|
240
|
+
return {
|
|
241
|
+
message: typeof message === "string" ? message : typeof type === "string" ? type : text,
|
|
242
|
+
requestId: typeof request_id === "string" ? request_id : undefined,
|
|
243
|
+
type: typeof type === "string" ? type : undefined,
|
|
244
|
+
details: Object.keys(rest).length ? rest : undefined,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (typeof err === "string")
|
|
248
|
+
return { message: err };
|
|
249
|
+
if (typeof data.message === "string")
|
|
250
|
+
return { message: data.message };
|
|
251
|
+
return { message: text };
|
|
252
|
+
}
|
|
253
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
254
|
+
const IMF_FIXDATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) ([A-Z][a-z]{2}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
255
|
+
const RFC850_DATE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-([A-Z][a-z]{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/;
|
|
256
|
+
const ASCTIME_DATE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
|
|
257
|
+
function retryAfterMs(v, now = Date.now()) {
|
|
258
|
+
if (v == null)
|
|
259
|
+
return undefined;
|
|
260
|
+
const s = v.trim();
|
|
261
|
+
if (/^\d+$/.test(s))
|
|
262
|
+
return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
|
|
263
|
+
let parts;
|
|
264
|
+
let m;
|
|
265
|
+
if ((m = IMF_FIXDATE.exec(s)))
|
|
266
|
+
parts = [m[1], m[2], Number(m[3]), m[4], m[5], m[6]];
|
|
267
|
+
else if ((m = RFC850_DATE.exec(s))) {
|
|
268
|
+
let year = 2000 + Number(m[3]);
|
|
269
|
+
if (year > new Date(now).getUTCFullYear() + 50)
|
|
270
|
+
year -= 100;
|
|
271
|
+
parts = [m[1], m[2], year, m[4], m[5], m[6]];
|
|
272
|
+
}
|
|
273
|
+
else if ((m = ASCTIME_DATE.exec(s)))
|
|
274
|
+
parts = [m[2].trim(), m[1], Number(m[6]), m[3], m[4], m[5]];
|
|
275
|
+
if (!parts)
|
|
276
|
+
return undefined;
|
|
277
|
+
const [day, mon, year, hh, mm, ss] = parts;
|
|
278
|
+
const month = MONTHS.indexOf(mon);
|
|
279
|
+
const d = Number(day), h = Number(hh), mi = Number(mm), se = Number(ss);
|
|
280
|
+
if (month < 0 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60)
|
|
281
|
+
return undefined;
|
|
282
|
+
return Math.max(0, Date.UTC(year, month, d, h, mi, se) - now);
|
|
283
|
+
}
|
|
284
|
+
const BRIEF_BODY_MS = 1_000;
|
|
285
|
+
class SlowBody extends Error {
|
|
286
|
+
}
|
|
287
|
+
const DEFAULT_TRANSPORTS = new WeakSet();
|
|
288
|
+
function defaultTransport() {
|
|
289
|
+
const g = globalThis.fetch;
|
|
290
|
+
if (typeof g !== "function")
|
|
291
|
+
return undefined;
|
|
292
|
+
const bound = g.bind(globalThis);
|
|
293
|
+
DEFAULT_TRANSPORTS.add(bound);
|
|
294
|
+
return bound;
|
|
295
|
+
}
|
|
296
|
+
function discard(res) {
|
|
297
|
+
res.body?.cancel().catch(() => { });
|
|
298
|
+
}
|
|
299
|
+
function tryUrl(s, base) {
|
|
300
|
+
try {
|
|
301
|
+
return base === undefined ? new URL(s) : new URL(s, base);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function pageHref() {
|
|
308
|
+
try {
|
|
309
|
+
const href = globalThis.location?.href;
|
|
310
|
+
return typeof href === "string" ? href : undefined;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function parseBase(base) {
|
|
317
|
+
let url = tryUrl(base);
|
|
318
|
+
if (!url) {
|
|
319
|
+
const page = pageHref();
|
|
320
|
+
if (page !== undefined)
|
|
321
|
+
url = tryUrl(base, page);
|
|
322
|
+
}
|
|
323
|
+
return url && (url.protocol === "http:" || url.protocol === "https:") ? url : undefined;
|
|
324
|
+
}
|
|
325
|
+
function asPageRelative(base) {
|
|
326
|
+
return tryUrl(base) ? undefined : tryUrl(base, "http://page.invalid/");
|
|
327
|
+
}
|
|
328
|
+
const BASE_URL_ENDS = /^\s+|\s+$/g;
|
|
329
|
+
function notAUrl(base) {
|
|
330
|
+
return `baseUrl is not a URL: ${JSON.stringify(maskBase(base))} (expected e.g. ${DEFAULT_BASE_URL})`;
|
|
331
|
+
}
|
|
332
|
+
function maskBase(raw) {
|
|
333
|
+
const at = raw.lastIndexOf("@");
|
|
334
|
+
if (at === -1)
|
|
335
|
+
return raw;
|
|
336
|
+
const from = /^(?:[a-z][a-z0-9+.-]*:)?[\\/]*/i.exec(raw)[0].length;
|
|
337
|
+
return `${raw.slice(0, from)}***@${raw.slice(at + 1)}`;
|
|
338
|
+
}
|
|
339
|
+
function maskUserinfo(text) {
|
|
340
|
+
let out = "";
|
|
341
|
+
let done = 0;
|
|
342
|
+
for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", at + 1)) {
|
|
343
|
+
let start = at;
|
|
344
|
+
while (start > 0 && !/[\s\\/@]/.test(text[start - 1]))
|
|
345
|
+
start--;
|
|
346
|
+
const run = text.slice(start, at);
|
|
347
|
+
const afterSlash = start > 0 && (text[start - 1] === "/" || text[start - 1] === "\\");
|
|
348
|
+
if (!afterSlash && !run.includes(":"))
|
|
349
|
+
continue;
|
|
350
|
+
const scheme = afterSlash ? "" : (/^[a-z][a-z0-9+.-]*:/i.exec(run)?.[0] ?? "");
|
|
351
|
+
out += `${text.slice(done, start + scheme.length)}***`;
|
|
352
|
+
done = at;
|
|
353
|
+
}
|
|
354
|
+
return out + text.slice(done);
|
|
355
|
+
}
|
|
123
356
|
function maskKey(key) {
|
|
124
357
|
return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
|
|
125
358
|
}
|
|
@@ -141,13 +374,24 @@ function parseRateLimit(h) {
|
|
|
141
374
|
const asList = (x) => (Array.isArray(x) ? x : []);
|
|
142
375
|
const asObj = (x) => (x != null && typeof x === "object" && !Array.isArray(x) ? x : {});
|
|
143
376
|
const asRecords = (x) => asList(x).filter((m) => m != null && typeof m === "object" && !Array.isArray(m));
|
|
377
|
+
const memoryFromGet = (r) => {
|
|
378
|
+
const o = asObj(r);
|
|
379
|
+
if (o.memory != null && typeof o.memory === "object" && !Array.isArray(o.memory))
|
|
380
|
+
return o.memory;
|
|
381
|
+
return typeof o.id === "string" ? o : {};
|
|
382
|
+
};
|
|
144
383
|
function mergeResults(r) {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
384
|
+
const out = asRecords(r?.memories);
|
|
385
|
+
const seen = new Set(out.map((m) => m.id).filter((id) => typeof id === "string"));
|
|
386
|
+
for (const m of [...asRecords(r?.self_memories), ...asRecords(r?.images)]) {
|
|
387
|
+
if (typeof m.id === "string") {
|
|
388
|
+
if (seen.has(m.id))
|
|
389
|
+
continue;
|
|
390
|
+
seen.add(m.id);
|
|
391
|
+
}
|
|
392
|
+
out.push(m);
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
151
395
|
}
|
|
152
396
|
function withForm(body, form, tz) {
|
|
153
397
|
if (form !== undefined)
|
|
@@ -159,11 +403,15 @@ function withForm(body, form, tz) {
|
|
|
159
403
|
class WosError extends Error {
|
|
160
404
|
status;
|
|
161
405
|
requestId;
|
|
162
|
-
|
|
163
|
-
|
|
406
|
+
type;
|
|
407
|
+
details;
|
|
408
|
+
constructor(status, message, requestId, init) {
|
|
409
|
+
super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`, init?.cause === undefined ? undefined : { cause: init.cause });
|
|
164
410
|
this.name = new.target.name;
|
|
165
411
|
this.status = status;
|
|
166
412
|
this.requestId = requestId;
|
|
413
|
+
this.type = init?.type;
|
|
414
|
+
this.details = init?.details;
|
|
167
415
|
}
|
|
168
416
|
}
|
|
169
417
|
exports.WosError = WosError;
|
|
@@ -186,9 +434,21 @@ class NotFoundError extends WosError {
|
|
|
186
434
|
}
|
|
187
435
|
exports.NotFoundError = NotFoundError;
|
|
188
436
|
class ConflictError extends WosError {
|
|
437
|
+
conflictsWith;
|
|
438
|
+
constructor(status, message, requestId, init) {
|
|
439
|
+
super(status, message, requestId, init);
|
|
440
|
+
const c = init?.details?.conflicts_with;
|
|
441
|
+
this.conflictsWith = typeof c === "string" ? c : undefined;
|
|
442
|
+
}
|
|
189
443
|
}
|
|
190
444
|
exports.ConflictError = ConflictError;
|
|
191
445
|
class RateLimitError extends WosError {
|
|
446
|
+
retryAfter;
|
|
447
|
+
constructor(status, message, requestId, init) {
|
|
448
|
+
super(status, message, requestId, init);
|
|
449
|
+
const r = init?.retryAfter;
|
|
450
|
+
this.retryAfter = typeof r === "number" && Number.isFinite(r) && r >= 0 ? r : undefined;
|
|
451
|
+
}
|
|
192
452
|
}
|
|
193
453
|
exports.RateLimitError = RateLimitError;
|
|
194
454
|
class ServerError extends WosError {
|
|
@@ -201,13 +461,15 @@ const STATUS_ERRORS = {
|
|
|
201
461
|
403: PermissionDeniedError,
|
|
202
462
|
404: NotFoundError,
|
|
203
463
|
409: ConflictError,
|
|
464
|
+
413: BadRequestError,
|
|
465
|
+
422: BadRequestError,
|
|
204
466
|
429: RateLimitError,
|
|
205
467
|
};
|
|
206
|
-
function errorFor(status, message, requestId) {
|
|
468
|
+
function errorFor(status, message, requestId, init) {
|
|
207
469
|
const Cls = status === 0
|
|
208
470
|
? APIConnectionError
|
|
209
471
|
: STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
|
|
210
|
-
return new Cls(status, message, requestId);
|
|
472
|
+
return new Cls(status, message, requestId, init);
|
|
211
473
|
}
|
|
212
474
|
const KNOWN_FILTER_KEYS = new Set([
|
|
213
475
|
"categories",
|
|
@@ -219,13 +481,15 @@ const KNOWN_FILTER_KEYS = new Set([
|
|
|
219
481
|
]);
|
|
220
482
|
const warnedFilterKeys = new Set();
|
|
221
483
|
const KNOWN_SEARCH_KEYS = new Set([
|
|
222
|
-
"cache_control", "speaker", "filters", "verify", "max_images", "extra",
|
|
484
|
+
"cache_control", "speaker", "filters", "verify", "max_images", "form", "tz", "extra",
|
|
223
485
|
]);
|
|
224
486
|
const RESERVED_SEARCH_KEYS = new Set(["user_id", "query", "max_results"]);
|
|
225
487
|
const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
|
|
226
488
|
const KNOWN_ENGRAM_KEYS = new Set(["form", "tz"]);
|
|
227
489
|
function checkSearchOpts(opts) {
|
|
228
490
|
checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
|
|
491
|
+
const o = opts;
|
|
492
|
+
optRange(o.max_images !== undefined ? o.max_images : o.extra?.max_images, "max_images", 0, MAX_IMAGES_MAX);
|
|
229
493
|
}
|
|
230
494
|
function checkOpts(opts, known, what) {
|
|
231
495
|
for (const k of Object.keys(opts)) {
|
|
@@ -259,24 +523,33 @@ function editWithin(a, b, max) {
|
|
|
259
523
|
}
|
|
260
524
|
return prev[b.length] <= max;
|
|
261
525
|
}
|
|
262
|
-
function
|
|
263
|
-
if (!
|
|
526
|
+
function warnOnceUnknownKeys(obj, known, warned, message) {
|
|
527
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj))
|
|
264
528
|
return;
|
|
265
|
-
for (const k of Object.keys(
|
|
266
|
-
if (
|
|
529
|
+
for (const k of Object.keys(obj)) {
|
|
530
|
+
if (known.has(k) || warned.has(k))
|
|
267
531
|
continue;
|
|
268
|
-
|
|
269
|
-
while (
|
|
270
|
-
const oldest =
|
|
532
|
+
warned.add(k);
|
|
533
|
+
while (warned.size > WARNED_FILTER_KEYS_MAX) {
|
|
534
|
+
const oldest = warned.values().next().value;
|
|
271
535
|
if (oldest === undefined)
|
|
272
536
|
break;
|
|
273
|
-
|
|
537
|
+
warned.delete(oldest);
|
|
274
538
|
}
|
|
275
|
-
console.warn(
|
|
276
|
-
`so this filter has NO effect and the search is wider than you think. ` +
|
|
277
|
-
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
539
|
+
console.warn(message(k));
|
|
278
540
|
}
|
|
279
541
|
}
|
|
542
|
+
function warnOnUnknownFilters(filters) {
|
|
543
|
+
warnOnceUnknownKeys(filters, KNOWN_FILTER_KEYS, warnedFilterKeys, (k) => `wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
544
|
+
`so this filter has NO effect and the search is wider than you think. ` +
|
|
545
|
+
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
546
|
+
}
|
|
547
|
+
const KNOWN_METADATA_KEYS = new Set(["speaker", "event_date", "category", "conversation_id"]);
|
|
548
|
+
const warnedMetadataKeys = new Set();
|
|
549
|
+
function warnOnUnknownMetadata(metadata) {
|
|
550
|
+
warnOnceUnknownKeys(metadata, KNOWN_METADATA_KEYS, warnedMetadataKeys, (k) => `wontopos: metadata key ${JSON.stringify(k)} is not kept: the service keeps only ` +
|
|
551
|
+
`${[...KNOWN_METADATA_KEYS].join(", ")} and drops the rest, so this value is not stored.`);
|
|
552
|
+
}
|
|
280
553
|
function _resetFilterWarnings() {
|
|
281
554
|
warnedFilterKeys.clear();
|
|
282
555
|
}
|
|
@@ -291,6 +564,9 @@ class Client {
|
|
|
291
564
|
deadlineMs;
|
|
292
565
|
signal;
|
|
293
566
|
fetchImpl;
|
|
567
|
+
userinfo;
|
|
568
|
+
pageRelative;
|
|
569
|
+
customFetch;
|
|
294
570
|
defaultUser;
|
|
295
571
|
_rateLimit = null;
|
|
296
572
|
get rateLimit() {
|
|
@@ -310,25 +586,41 @@ class Client {
|
|
|
310
586
|
"en dash; copy the key from a plain-text field");
|
|
311
587
|
}
|
|
312
588
|
this.apiKey = key;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
589
|
+
const rawBase = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
590
|
+
if (typeof rawBase !== "string")
|
|
591
|
+
throw new Error("baseUrl must be a string");
|
|
592
|
+
const trimmed = rawBase.replace(BASE_URL_ENDS, "");
|
|
593
|
+
if (/[\s\\\u0000-\u001f\u007f]/.test(trimmed)) {
|
|
594
|
+
throw new Error(`baseUrl contains whitespace, a backslash or a control character: ${JSON.stringify(maskBase(trimmed))}`);
|
|
595
|
+
}
|
|
596
|
+
this.base = trimmed.replace(/\/+$/, "");
|
|
597
|
+
const url = parseBase(this.base);
|
|
598
|
+
const parsed = url ?? (pageHref() === undefined ? asPageRelative(this.base) : undefined);
|
|
599
|
+
if (!parsed)
|
|
600
|
+
throw new Error(notAUrl(trimmed));
|
|
601
|
+
this.pageRelative = !url;
|
|
602
|
+
this.userinfo = Boolean(parsed.username || parsed.password);
|
|
603
|
+
this.timeoutMs = checkMs(opts.timeoutMs) ?? 30_000;
|
|
316
604
|
this.model = opts.model ?? DEFAULT_MODEL;
|
|
317
605
|
if (this.model && !MODEL_RE.test(this.model)) {
|
|
318
606
|
throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
|
|
319
607
|
}
|
|
608
|
+
if (opts && "userId" in opts && opts.userId === undefined) {
|
|
609
|
+
throw new Error("new Client() needs a store id in userId, or no userId option at all. It was given " +
|
|
610
|
+
"undefined. That is usually a lookup that found nothing. Leave userId out to use the default store.");
|
|
611
|
+
}
|
|
320
612
|
assertUsableStoreId(opts.userId);
|
|
321
613
|
this.defaultUser = opts.userId ?? DEFAULT_USER;
|
|
322
614
|
const mr = opts.maxRetries ?? opts.retries;
|
|
323
615
|
this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
|
|
324
|
-
|
|
325
|
-
this.deadlineMs = typeof dl === "number" && Number.isFinite(dl) && dl > 0 ? dl : undefined;
|
|
616
|
+
this.deadlineMs = checkMs(opts.deadlineMs);
|
|
326
617
|
this.signal = opts.signal;
|
|
327
618
|
const f = opts.fetch;
|
|
328
619
|
if (f !== undefined && typeof f !== "function") {
|
|
329
620
|
throw new Error("fetch must be a function (a fetch-compatible transport)");
|
|
330
621
|
}
|
|
331
|
-
this.
|
|
622
|
+
this.customFetch = f !== undefined && !DEFAULT_TRANSPORTS.has(f);
|
|
623
|
+
this.fetchImpl = (f ?? defaultTransport());
|
|
332
624
|
if (typeof this.fetchImpl !== "function") {
|
|
333
625
|
throw new Error("no fetch available in this runtime — pass one as `fetch` (Node 18+ has it built in; older Node needs undici)");
|
|
334
626
|
}
|
|
@@ -339,14 +631,8 @@ class Client {
|
|
|
339
631
|
};
|
|
340
632
|
if (this.model)
|
|
341
633
|
this.headers["X-WOS-Model"] = this.model;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
host = new URL(this.base).hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
host = "";
|
|
348
|
-
}
|
|
349
|
-
if (/^http:\/\//i.test(this.base) && !LOOPBACK_HOSTS.has(host)) {
|
|
634
|
+
const host = url?.hostname.toLowerCase().replace(/^\[|\]$/g, "") ?? "";
|
|
635
|
+
if (url?.protocol === "http:" && !LOOPBACK_HOSTS.has(host)) {
|
|
350
636
|
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
351
637
|
}
|
|
352
638
|
Object.defineProperty(this, "apiKey", { enumerable: false });
|
|
@@ -360,10 +646,10 @@ class Client {
|
|
|
360
646
|
return new Client({ ...opts, apiKey: key });
|
|
361
647
|
}
|
|
362
648
|
toJSON() {
|
|
363
|
-
return { baseUrl: this.base, model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
|
|
649
|
+
return { baseUrl: maskBase(this.base), model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
|
|
364
650
|
}
|
|
365
651
|
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
366
|
-
return `Client(${this.base}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
|
|
652
|
+
return `Client(${maskBase(this.base)}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
|
|
367
653
|
}
|
|
368
654
|
uid(userId) {
|
|
369
655
|
assertUsableStoreId(userId);
|
|
@@ -408,9 +694,11 @@ class Client {
|
|
|
408
694
|
return this.clone({ maxRetries });
|
|
409
695
|
}
|
|
410
696
|
async add(content, userId, metadata = {}, opts = {}) {
|
|
697
|
+
checkMetadataKeys(metadata);
|
|
411
698
|
const body = { user_id: this.uid(userId), content, metadata };
|
|
412
699
|
if (opts.image !== undefined)
|
|
413
700
|
body.image = normalizeImage(opts.image);
|
|
701
|
+
warnOnUnknownMetadata(metadata);
|
|
414
702
|
return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
|
|
415
703
|
}
|
|
416
704
|
async store(content, userId, metadata = {}, opts = {}) {
|
|
@@ -431,6 +719,7 @@ class Client {
|
|
|
431
719
|
async search(query, userId, limit = 10, opts = {}) {
|
|
432
720
|
checkSearchOpts(opts);
|
|
433
721
|
warnOnUnknownFilters(opts.filters);
|
|
722
|
+
limit ??= 10;
|
|
434
723
|
checkCount(limit, "limit");
|
|
435
724
|
const { extra, ...known } = opts;
|
|
436
725
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -439,6 +728,7 @@ class Client {
|
|
|
439
728
|
async searchSelf(query, userId, limit = 10, opts = {}) {
|
|
440
729
|
checkSearchOpts(opts);
|
|
441
730
|
warnOnUnknownFilters(opts.filters);
|
|
731
|
+
limit ??= 10;
|
|
442
732
|
checkCount(limit, "limit");
|
|
443
733
|
const { extra, ...known } = opts;
|
|
444
734
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -447,6 +737,7 @@ class Client {
|
|
|
447
737
|
async searchFull(query, userId, limit = 10, opts = {}) {
|
|
448
738
|
checkSearchOpts(opts);
|
|
449
739
|
warnOnUnknownFilters(opts.filters);
|
|
740
|
+
limit ??= 10;
|
|
450
741
|
checkCount(limit, "limit");
|
|
451
742
|
const { extra, ...known } = opts;
|
|
452
743
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -460,11 +751,11 @@ class Client {
|
|
|
460
751
|
async recall(query, userId, opts = {}) {
|
|
461
752
|
checkOpts(opts, KNOWN_RECALL_KEYS, "recall");
|
|
462
753
|
const body = { user_id: this.uid(userId), query };
|
|
463
|
-
if (opts.limit
|
|
754
|
+
if (opts.limit != null) {
|
|
464
755
|
checkCount(opts.limit, "limit");
|
|
465
756
|
body.limit = opts.limit;
|
|
466
757
|
}
|
|
467
|
-
if (opts.context_limit
|
|
758
|
+
if (opts.context_limit != null) {
|
|
468
759
|
checkContextLimit(opts.context_limit);
|
|
469
760
|
body.context_limit = opts.context_limit;
|
|
470
761
|
}
|
|
@@ -491,10 +782,11 @@ class Client {
|
|
|
491
782
|
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
492
783
|
throw new Error("memoryId is required — the id that add/store or listMemories returned.");
|
|
493
784
|
}
|
|
494
|
-
return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then(
|
|
785
|
+
return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then(memoryFromGet);
|
|
495
786
|
}
|
|
496
787
|
async listMemories(userId, opts = {}) {
|
|
497
|
-
const
|
|
788
|
+
const limit = optRange(opts.limit, "limit", 1, LIST_LIMIT_MAX);
|
|
789
|
+
const body = { user_id: this.uid(userId), limit: limit ?? 100 };
|
|
498
790
|
if (opts.cursor)
|
|
499
791
|
body.cursor = opts.cursor;
|
|
500
792
|
return this.post("/api/v1/memory/list", body);
|
|
@@ -504,16 +796,21 @@ class Client {
|
|
|
504
796
|
const seen = new Set();
|
|
505
797
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
506
798
|
const p = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
|
|
507
|
-
|
|
799
|
+
const rows = asRecords(p.memories);
|
|
800
|
+
for (const m of rows)
|
|
508
801
|
yield m;
|
|
509
802
|
const next = p.next_cursor ?? undefined;
|
|
510
|
-
if (!next
|
|
803
|
+
if (!next)
|
|
804
|
+
return;
|
|
805
|
+
if (seen.has(next)) {
|
|
806
|
+
if (rows.length)
|
|
807
|
+
throw truncatedWalk(`stopped at page ${page + 1}: the service repeated a cursor, so the store did not end.`);
|
|
511
808
|
return;
|
|
809
|
+
}
|
|
512
810
|
seen.add(next);
|
|
513
811
|
cursor = next;
|
|
514
812
|
}
|
|
515
|
-
throw
|
|
516
|
-
`answer, not the whole store.`);
|
|
813
|
+
throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
|
|
517
814
|
}
|
|
518
815
|
async exportMemories(userId) {
|
|
519
816
|
const out = [];
|
|
@@ -538,14 +835,16 @@ class Client {
|
|
|
538
835
|
throw new Error("memoryId is required — the id of the memory whose image you want removed.");
|
|
539
836
|
}
|
|
540
837
|
const body = { user_id: this.uid(userId), memory_id: memoryId.trim() };
|
|
541
|
-
if (opts.preview)
|
|
542
|
-
|
|
838
|
+
if (!opts.preview)
|
|
839
|
+
return this.remove("/api/v1/memory/image", body);
|
|
840
|
+
body.preview = true;
|
|
543
841
|
return this.request("DELETE", "/api/v1/memory/image", body);
|
|
544
842
|
}
|
|
545
843
|
async listImages(userId, opts = {}) {
|
|
844
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
546
845
|
const body = { user_id: this.uid(userId) };
|
|
547
|
-
if (
|
|
548
|
-
body.limit =
|
|
846
|
+
if (limit !== undefined)
|
|
847
|
+
body.limit = limit;
|
|
549
848
|
if (opts.before !== undefined)
|
|
550
849
|
body.before = opts.before;
|
|
551
850
|
if (opts.skipIds !== undefined)
|
|
@@ -558,26 +857,30 @@ class Client {
|
|
|
558
857
|
const seen = new Set();
|
|
559
858
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
560
859
|
const p = await this.listImages(userId, { limit: opts.pageSize, before, skipIds });
|
|
561
|
-
|
|
860
|
+
const rows = asRecords(p.images);
|
|
861
|
+
for (const m of rows)
|
|
562
862
|
yield m;
|
|
563
863
|
if (!p.has_more || !p.next_before)
|
|
564
864
|
return;
|
|
565
865
|
const key = `${p.next_before}|${(Array.isArray(p.next_skip_ids) ? p.next_skip_ids : []).join(",")}`;
|
|
566
|
-
if (seen.has(key))
|
|
866
|
+
if (seen.has(key)) {
|
|
867
|
+
if (rows.length)
|
|
868
|
+
throw truncatedWalk(`stopped at page ${page + 1}: the service repeated a cursor, so the store did not end.`);
|
|
567
869
|
return;
|
|
870
|
+
}
|
|
568
871
|
seen.add(key);
|
|
569
872
|
before = p.next_before;
|
|
570
873
|
skipIds = Array.isArray(p.next_skip_ids) ? p.next_skip_ids : undefined;
|
|
571
874
|
}
|
|
572
|
-
throw
|
|
573
|
-
`answer, not the whole store.`);
|
|
875
|
+
throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
|
|
574
876
|
}
|
|
575
877
|
async revisions(userId, opts = {}) {
|
|
878
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
576
879
|
const body = {};
|
|
577
880
|
if (opts.include !== undefined)
|
|
578
881
|
body.include = opts.include;
|
|
579
|
-
if (
|
|
580
|
-
body.limit =
|
|
882
|
+
if (limit !== undefined)
|
|
883
|
+
body.limit = limit;
|
|
581
884
|
if (opts.before !== undefined)
|
|
582
885
|
body.before = opts.before;
|
|
583
886
|
if (opts.skipIds !== undefined)
|
|
@@ -595,9 +898,10 @@ class Client {
|
|
|
595
898
|
if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
|
|
596
899
|
throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
|
|
597
900
|
}
|
|
901
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
598
902
|
const body = { user_id: this.uid(userId), speaker: speaker.trim() };
|
|
599
|
-
if (
|
|
600
|
-
body.limit =
|
|
903
|
+
if (limit !== undefined)
|
|
904
|
+
body.limit = limit;
|
|
601
905
|
if (opts.before !== undefined)
|
|
602
906
|
body.before = opts.before;
|
|
603
907
|
if (opts.skipIds !== undefined)
|
|
@@ -632,7 +936,7 @@ class Client {
|
|
|
632
936
|
if (typeof userId !== "string" || !userId.trim())
|
|
633
937
|
throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
|
|
634
938
|
warnIfStoreIdCollapses(userId);
|
|
635
|
-
return this.
|
|
939
|
+
return this.remove("/api/v1/memory/collection", { user_id: userId });
|
|
636
940
|
}
|
|
637
941
|
async addSpeaker(speaker, userId) {
|
|
638
942
|
return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
@@ -642,7 +946,7 @@ class Client {
|
|
|
642
946
|
return { ...r, speakers: asRecords(r?.speakers) };
|
|
643
947
|
}
|
|
644
948
|
async removeSpeaker(speaker, userId) {
|
|
645
|
-
return this.
|
|
949
|
+
return this.remove("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
646
950
|
}
|
|
647
951
|
async delete(userId, memoryId) {
|
|
648
952
|
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
@@ -659,22 +963,26 @@ class Client {
|
|
|
659
963
|
post(path, body, idempotencyKey) {
|
|
660
964
|
return this.request("POST", path, body, idempotencyKey);
|
|
661
965
|
}
|
|
966
|
+
remove(path, body) {
|
|
967
|
+
return this.request("DELETE", path, body, undefined, true);
|
|
968
|
+
}
|
|
662
969
|
deadlineAt() {
|
|
663
970
|
return this.deadlineMs === undefined ? undefined : Date.now() + this.deadlineMs;
|
|
664
971
|
}
|
|
665
|
-
beginAttempt(deadlineAt) {
|
|
972
|
+
beginAttempt(deadlineAt, answer) {
|
|
666
973
|
let why = "timeout";
|
|
667
974
|
let budget = this.timeoutMs;
|
|
668
975
|
if (deadlineAt !== undefined) {
|
|
669
976
|
const left = deadlineAt - Date.now();
|
|
670
977
|
if (left <= 0)
|
|
671
|
-
throw new APIConnectionError(0, this.abortMessage("deadline"));
|
|
978
|
+
throw answer ?? new APIConnectionError(0, this.abortMessage("deadline"));
|
|
672
979
|
if (left < budget) {
|
|
673
980
|
budget = left;
|
|
674
981
|
why = "deadline";
|
|
675
982
|
}
|
|
676
983
|
}
|
|
677
984
|
const ctrl = new AbortController();
|
|
985
|
+
const endsAt = Date.now() + budget;
|
|
678
986
|
const timer = setTimeout(() => ctrl.abort(), budget);
|
|
679
987
|
const outer = this.signal;
|
|
680
988
|
const onCaller = () => {
|
|
@@ -694,6 +1002,7 @@ class Client {
|
|
|
694
1002
|
outer?.removeEventListener("abort", onCaller);
|
|
695
1003
|
},
|
|
696
1004
|
why: () => why,
|
|
1005
|
+
endsAt,
|
|
697
1006
|
};
|
|
698
1007
|
}
|
|
699
1008
|
abortMessage(why) {
|
|
@@ -703,10 +1012,10 @@ class Client {
|
|
|
703
1012
|
return `deadline of ${this.deadlineMs}ms exhausted`;
|
|
704
1013
|
return `request timed out after ${this.timeoutMs}ms`;
|
|
705
1014
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1015
|
+
fits(ms, deadlineAt) {
|
|
1016
|
+
return deadlineAt === undefined || ms <= deadlineAt - Date.now();
|
|
1017
|
+
}
|
|
1018
|
+
async pause(ms) {
|
|
710
1019
|
const outer = this.signal;
|
|
711
1020
|
if (!outer) {
|
|
712
1021
|
await new Promise((r) => setTimeout(r, ms));
|
|
@@ -724,17 +1033,82 @@ class Client {
|
|
|
724
1033
|
outer.addEventListener("abort", done);
|
|
725
1034
|
});
|
|
726
1035
|
}
|
|
727
|
-
|
|
1036
|
+
networkError(e, path) {
|
|
1037
|
+
let text = typeof e?.message === "string" && e.message ? e.message : String(e);
|
|
1038
|
+
const q = path.indexOf("?");
|
|
1039
|
+
if (q !== -1) {
|
|
1040
|
+
const query = path.slice(q);
|
|
1041
|
+
text = text.split(query).join("");
|
|
1042
|
+
try {
|
|
1043
|
+
text = text.split(decodeURIComponent(query)).join("");
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
text = cleanServerText(maskUserinfo(text), 512);
|
|
1049
|
+
const code = causeCode(e);
|
|
1050
|
+
if (code && !text.includes(code))
|
|
1051
|
+
text += ` (${code})`;
|
|
1052
|
+
const quotable = this.userinfo || q !== -1;
|
|
1053
|
+
return new APIConnectionError(0, `network error: ${text}`, undefined, quotable ? undefined : { cause: e });
|
|
1054
|
+
}
|
|
1055
|
+
isRedirected(res) {
|
|
1056
|
+
return (res.status >= 300 && res.status < 400) || res.type === "opaqueredirect" || res.redirected === true;
|
|
1057
|
+
}
|
|
1058
|
+
errorFromAnswer(res, body, retryAfter, mayBeDeleted = false, unread) {
|
|
1059
|
+
const e = parseErrorText(new TextDecoder().decode(body));
|
|
1060
|
+
let msg = cleanServerText(e.message) || `HTTP ${res.status}`;
|
|
1061
|
+
if (unread)
|
|
1062
|
+
msg += ` (the error body could not be read: ${cleanServerText(unread, 512)})`;
|
|
1063
|
+
const requestId = e.requestId === undefined ? undefined : cleanServerText(e.requestId, 256) || undefined;
|
|
1064
|
+
const err = errorFor(res.status, msg, requestId, {
|
|
1065
|
+
type: e.type === undefined ? undefined : cleanServerText(e.type, 256) || undefined,
|
|
1066
|
+
details: cleanDetails(e.details),
|
|
1067
|
+
retryAfter: retryAfter === undefined ? undefined : Math.ceil(retryAfter / 1000),
|
|
1068
|
+
});
|
|
1069
|
+
if (mayBeDeleted && err instanceof NotFoundError) {
|
|
1070
|
+
err.message += " (an earlier attempt may already have deleted it)";
|
|
1071
|
+
}
|
|
1072
|
+
return err;
|
|
1073
|
+
}
|
|
1074
|
+
async send(method, path, payload, extraHeaders, removes = false, reads = false) {
|
|
1075
|
+
if (this.pageRelative && !this.customFetch && !parseBase(this.base)) {
|
|
1076
|
+
throw new Error(`${notAUrl(this.base)}. A relative baseUrl needs a page to resolve against, and this runtime has none.`);
|
|
1077
|
+
}
|
|
728
1078
|
const attempts = this.maxRetries + 1;
|
|
1079
|
+
const upper = method.toUpperCase();
|
|
1080
|
+
const idempotent = IDEMPOTENT_METHODS.has(upper);
|
|
1081
|
+
const logPath = path.split("?")[0];
|
|
729
1082
|
const deadlineAt = this.deadlineAt();
|
|
1083
|
+
const headers = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
|
|
1084
|
+
let ambiguous = false;
|
|
1085
|
+
let refused;
|
|
730
1086
|
for (let attempt = 0;; attempt++) {
|
|
731
|
-
const
|
|
1087
|
+
const last = attempt + 1 >= attempts;
|
|
1088
|
+
const tag = `(attempt ${attempt + 1}/${attempts})`;
|
|
1089
|
+
const start = Date.now();
|
|
1090
|
+
const previous = refused;
|
|
1091
|
+
refused = undefined;
|
|
1092
|
+
const att = this.beginAttempt(deadlineAt, previous);
|
|
1093
|
+
const pauseWithoutAnswer = async (why) => {
|
|
1094
|
+
const delay = this.backoffMs(attempt);
|
|
1095
|
+
if (!this.fits(delay, deadlineAt))
|
|
1096
|
+
throw previous ?? new APIConnectionError(0, this.abortMessage("deadline"));
|
|
1097
|
+
logDebug(`${method} ${logPath}: ${why}. Retrying in ${Math.round(delay)}ms ${tag}`);
|
|
1098
|
+
await this.pause(delay);
|
|
1099
|
+
};
|
|
1100
|
+
const cutShort = () => {
|
|
1101
|
+
const why = att.why();
|
|
1102
|
+
if (why === "deadline" && (idempotent || reads) && previous)
|
|
1103
|
+
return previous;
|
|
1104
|
+
return new APIConnectionError(0, this.abortMessage(why));
|
|
1105
|
+
};
|
|
732
1106
|
let res;
|
|
733
1107
|
try {
|
|
734
1108
|
res = await this.fetchImpl(`${this.base}${path}`, {
|
|
735
|
-
method
|
|
736
|
-
headers
|
|
737
|
-
body:
|
|
1109
|
+
method,
|
|
1110
|
+
headers,
|
|
1111
|
+
body: payload,
|
|
738
1112
|
signal: att.signal,
|
|
739
1113
|
redirect: "manual",
|
|
740
1114
|
});
|
|
@@ -742,63 +1116,109 @@ class Client {
|
|
|
742
1116
|
catch (e) {
|
|
743
1117
|
att.clear();
|
|
744
1118
|
const timedOut = e?.name === "AbortError" || att.signal.aborted;
|
|
745
|
-
if (
|
|
746
|
-
|
|
1119
|
+
if (timedOut)
|
|
1120
|
+
throw cutShort();
|
|
1121
|
+
const connect = isConnectFailure(e);
|
|
1122
|
+
if ((idempotent || connect) && !last) {
|
|
1123
|
+
if (!connect)
|
|
1124
|
+
ambiguous = true;
|
|
1125
|
+
await pauseWithoutAnswer(e?.code ?? e?.name ?? "network error");
|
|
747
1126
|
continue;
|
|
748
1127
|
}
|
|
749
|
-
throw
|
|
1128
|
+
throw this.networkError(e, path);
|
|
750
1129
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
void res.body?.cancel();
|
|
755
|
-
throw errorFor(res.status, "the API answered with a redirect; refusing to follow it");
|
|
1130
|
+
const readBody = async () => {
|
|
1131
|
+
try {
|
|
1132
|
+
return await this.readCappedBytes(res);
|
|
756
1133
|
}
|
|
757
|
-
|
|
758
|
-
if (
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
let msg = await this.readCapped(res).catch(() => "");
|
|
766
|
-
try {
|
|
767
|
-
const data = JSON.parse(msg);
|
|
768
|
-
const err = data?.error;
|
|
769
|
-
if (err && typeof err === "object")
|
|
770
|
-
msg = err.message ?? err.type ?? msg;
|
|
771
|
-
else if (typeof err === "string")
|
|
772
|
-
msg = err;
|
|
773
|
-
}
|
|
774
|
-
catch {
|
|
775
|
-
}
|
|
776
|
-
if (msg.length > MAX_ERR_MSG)
|
|
777
|
-
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
778
|
-
throw errorFor(res.status, msg || `HTTP ${res.status}`);
|
|
1134
|
+
catch (e) {
|
|
1135
|
+
if (e?.name === "AbortError" || att.signal.aborted)
|
|
1136
|
+
throw cutShort();
|
|
1137
|
+
if (e instanceof WosError)
|
|
1138
|
+
throw e;
|
|
1139
|
+
if (idempotent && !last)
|
|
1140
|
+
return null;
|
|
1141
|
+
throw this.networkError(e, path);
|
|
779
1142
|
}
|
|
780
|
-
|
|
1143
|
+
};
|
|
1144
|
+
const readErrorBody = async (withinMs) => {
|
|
781
1145
|
try {
|
|
782
|
-
|
|
1146
|
+
return { body: await this.readCappedBytes(res, withinMs) };
|
|
783
1147
|
}
|
|
784
1148
|
catch (e) {
|
|
785
|
-
|
|
786
|
-
|
|
1149
|
+
const aborted = e?.name === "AbortError" || att.signal.aborted;
|
|
1150
|
+
if (aborted && att.why() === "caller")
|
|
1151
|
+
throw new APIConnectionError(0, this.abortMessage("caller"));
|
|
1152
|
+
const unread = aborted
|
|
1153
|
+
? this.abortMessage(att.why())
|
|
1154
|
+
: e instanceof SlowBody
|
|
1155
|
+
? e.message
|
|
1156
|
+
: (e instanceof WosError ? e : this.networkError(e, path)).message.replace(/^\[\d+\] /, "");
|
|
1157
|
+
return { body: new Uint8Array(0), unread };
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
try {
|
|
1161
|
+
if (this.isRedirected(res)) {
|
|
1162
|
+
discard(res);
|
|
1163
|
+
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
1164
|
+
}
|
|
1165
|
+
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
1166
|
+
if (!res.ok) {
|
|
1167
|
+
const ra = retryAfterMs(res.headers.get("Retry-After"));
|
|
1168
|
+
let wait = RETRY_ALWAYS.has(res.status) || (RETRY_IF_IDEMPOTENT.has(res.status) && idempotent)
|
|
1169
|
+
? ra ?? this.backoffMs(attempt)
|
|
1170
|
+
: undefined;
|
|
1171
|
+
const retrying = wait !== undefined && !last && wait <= MAX_RETRY_WAIT_MS && this.fits(wait, deadlineAt);
|
|
1172
|
+
const { body, unread } = await readErrorBody(retrying ? Math.max(0, Math.min(BRIEF_BODY_MS, att.endsAt - Date.now())) : undefined);
|
|
1173
|
+
if (res.status === 409) {
|
|
1174
|
+
const d = parseErrorText(new TextDecoder().decode(body)).details;
|
|
1175
|
+
const ms = d?.retry_after_ms;
|
|
1176
|
+
if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0 && d?.conflicts_with === undefined) {
|
|
1177
|
+
wait = Math.max(ms, this.backoffMs(attempt));
|
|
1178
|
+
}
|
|
787
1179
|
}
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
1180
|
+
if (wait !== undefined && !last) {
|
|
1181
|
+
if (wait > MAX_RETRY_WAIT_MS) {
|
|
1182
|
+
logDebug(`${method} ${logPath} -> ${res.status}: asked to wait ${Math.round(wait)}ms, over the cap. Not retrying ${tag}`);
|
|
1183
|
+
}
|
|
1184
|
+
else if (!this.fits(wait, deadlineAt)) {
|
|
1185
|
+
logDebug(`${method} ${logPath} -> ${res.status}: a ${Math.round(wait)}ms wait does not fit the deadline. Not retrying ${tag}`);
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
if (RETRY_IF_IDEMPOTENT.has(res.status))
|
|
1189
|
+
ambiguous = true;
|
|
1190
|
+
refused = this.errorFromAnswer(res, body, ra, ambiguous && removes, unread);
|
|
1191
|
+
att.clear();
|
|
1192
|
+
logDebug(`${method} ${logPath} -> ${res.status}. Retrying in ${Math.round(wait)}ms ${tag}`);
|
|
1193
|
+
await this.pause(wait);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms ${tag}`);
|
|
1198
|
+
throw this.errorFromAnswer(res, body, ra, ambiguous && removes, unread);
|
|
791
1199
|
}
|
|
792
|
-
|
|
793
|
-
|
|
1200
|
+
const body = await readBody();
|
|
1201
|
+
if (body === null) {
|
|
1202
|
+
ambiguous = true;
|
|
1203
|
+
att.clear();
|
|
1204
|
+
await pauseWithoutAnswer("body dropped mid-stream");
|
|
1205
|
+
continue;
|
|
794
1206
|
}
|
|
795
|
-
|
|
1207
|
+
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms ${tag}`);
|
|
1208
|
+
return { res, body };
|
|
796
1209
|
}
|
|
797
1210
|
finally {
|
|
798
1211
|
att.clear();
|
|
799
1212
|
}
|
|
800
1213
|
}
|
|
801
1214
|
}
|
|
1215
|
+
async requestBytes(path, body) {
|
|
1216
|
+
const { res, body: buf } = await this.send("POST", path, JSON.stringify(body), undefined, false, true);
|
|
1217
|
+
if (buf.byteLength === 0) {
|
|
1218
|
+
throw new WosError(res.status, "empty image body — the service returned no bytes");
|
|
1219
|
+
}
|
|
1220
|
+
return { bytes: buf, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
|
|
1221
|
+
}
|
|
802
1222
|
idemHeader(key) {
|
|
803
1223
|
if (key === undefined || key === null)
|
|
804
1224
|
return undefined;
|
|
@@ -808,58 +1228,72 @@ class Client {
|
|
|
808
1228
|
return { "Idempotency-Key": key };
|
|
809
1229
|
}
|
|
810
1230
|
backoffMs(attempt, retryAfter) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return Math.min(30_000, s * 1000);
|
|
815
|
-
const t = Date.parse(retryAfter);
|
|
816
|
-
if (Number.isFinite(t))
|
|
817
|
-
return Math.min(30_000, Math.max(0, t - Date.now()));
|
|
818
|
-
}
|
|
1231
|
+
const ra = retryAfterMs(retryAfter ?? null);
|
|
1232
|
+
if (ra !== undefined)
|
|
1233
|
+
return ra;
|
|
819
1234
|
return Math.min(8_000, 500 * 2 ** attempt) + Math.random() * 250;
|
|
820
1235
|
}
|
|
821
|
-
async
|
|
822
|
-
return new TextDecoder().decode(await this.readCappedBytes(res));
|
|
823
|
-
}
|
|
824
|
-
async readCappedBytes(res) {
|
|
1236
|
+
async readCappedBytes(res, withinMs) {
|
|
825
1237
|
const cl = res.headers.get("content-length");
|
|
826
1238
|
if (cl && Number(cl) > MAX_RESPONSE_BYTES) {
|
|
1239
|
+
discard(res);
|
|
827
1240
|
throw new WosError(res.status, `response too large (${cl} bytes) — refusing to buffer it`);
|
|
828
1241
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1242
|
+
const slow = () => new SlowBody(`not received within ${withinMs}ms`);
|
|
1243
|
+
let late = false;
|
|
1244
|
+
let reader;
|
|
1245
|
+
let stop = () => { };
|
|
1246
|
+
const stopped = new Promise((_, reject) => (stop = () => reject(slow())));
|
|
1247
|
+
stopped.catch(() => { });
|
|
1248
|
+
const timer = withinMs === undefined
|
|
1249
|
+
? undefined
|
|
1250
|
+
: setTimeout(() => {
|
|
1251
|
+
late = true;
|
|
1252
|
+
stop();
|
|
1253
|
+
reader?.cancel().catch(() => { });
|
|
1254
|
+
}, withinMs);
|
|
1255
|
+
const within = (p) => (timer === undefined ? p : Promise.race([p, stopped]));
|
|
1256
|
+
try {
|
|
1257
|
+
if (!res.body) {
|
|
1258
|
+
const buf = new Uint8Array(await within(res.arrayBuffer()));
|
|
1259
|
+
if (buf.byteLength > MAX_RESPONSE_BYTES) {
|
|
1260
|
+
throw new WosError(res.status, `response too large (${buf.byteLength} bytes) — refusing it`);
|
|
1261
|
+
}
|
|
1262
|
+
return buf;
|
|
1263
|
+
}
|
|
1264
|
+
reader = res.body.getReader();
|
|
1265
|
+
const parts = [];
|
|
1266
|
+
let size = 0;
|
|
1267
|
+
for (;;) {
|
|
1268
|
+
const { done, value } = await within(reader.read());
|
|
1269
|
+
if (late)
|
|
1270
|
+
throw slow();
|
|
1271
|
+
if (done)
|
|
1272
|
+
break;
|
|
1273
|
+
size += value.byteLength;
|
|
1274
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
1275
|
+
reader.cancel().catch(() => { });
|
|
1276
|
+
throw new WosError(res.status, "response too large — refusing to buffer it");
|
|
1277
|
+
}
|
|
1278
|
+
parts.push(value);
|
|
1279
|
+
}
|
|
1280
|
+
const buf = new Uint8Array(size);
|
|
1281
|
+
let off = 0;
|
|
1282
|
+
for (const p of parts) {
|
|
1283
|
+
buf.set(p, off);
|
|
1284
|
+
off += p.byteLength;
|
|
833
1285
|
}
|
|
834
1286
|
return buf;
|
|
835
1287
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
let size = 0;
|
|
839
|
-
for (;;) {
|
|
840
|
-
const { done, value } = await reader.read();
|
|
841
|
-
if (done)
|
|
842
|
-
break;
|
|
843
|
-
size += value.byteLength;
|
|
844
|
-
if (size > MAX_RESPONSE_BYTES) {
|
|
845
|
-
void reader.cancel();
|
|
846
|
-
throw new WosError(res.status, "response too large — refusing to buffer it");
|
|
847
|
-
}
|
|
848
|
-
parts.push(value);
|
|
1288
|
+
catch (e) {
|
|
1289
|
+
throw late && !(e instanceof SlowBody) ? slow() : e;
|
|
849
1290
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
for (const p of parts) {
|
|
853
|
-
buf.set(p, off);
|
|
854
|
-
off += p.byteLength;
|
|
1291
|
+
finally {
|
|
1292
|
+
clearTimeout(timer);
|
|
855
1293
|
}
|
|
856
|
-
return buf;
|
|
857
1294
|
}
|
|
858
|
-
async request(method, path, body, idempotencyKey) {
|
|
1295
|
+
async request(method, path, body, idempotencyKey, removes = false) {
|
|
859
1296
|
const extraHeaders = this.idemHeader(idempotencyKey);
|
|
860
|
-
const attempts = this.maxRetries + 1;
|
|
861
|
-
const logPath = path.split("?")[0];
|
|
862
|
-
const deadlineAt = this.deadlineAt();
|
|
863
1297
|
let payload;
|
|
864
1298
|
try {
|
|
865
1299
|
payload = body === undefined ? undefined : JSON.stringify(body);
|
|
@@ -868,117 +1302,28 @@ class Client {
|
|
|
868
1302
|
throw new Error(`request body could not be serialized: ${e instanceof Error ? e.message : String(e)}. ` +
|
|
869
1303
|
"Pass plain JSON values — an ORM entity, a BigInt or a circular reference cannot be sent.");
|
|
870
1304
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
await this.backoffSleep(delay, deadlineAt);
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
throw new APIConnectionError(0, timedOut ? this.abortMessage(att.why()) : `network error: ${e?.message ?? e}`);
|
|
895
|
-
}
|
|
896
|
-
const retryable = RETRY_ALWAYS.has(res.status) ||
|
|
897
|
-
(RETRY_IF_IDEMPOTENT.has(res.status) && IDEMPOTENT_METHODS.has(method.toUpperCase()));
|
|
898
|
-
if (retryable && attempt + 1 < attempts) {
|
|
899
|
-
att.clear();
|
|
900
|
-
const retryAfter = res.headers.get("Retry-After");
|
|
901
|
-
void res.body?.cancel();
|
|
902
|
-
const delay = this.backoffMs(attempt, retryAfter);
|
|
903
|
-
logDebug(`${method} ${logPath} -> ${res.status} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
904
|
-
await this.backoffSleep(delay, deadlineAt);
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
908
|
-
att.clear();
|
|
909
|
-
void res.body?.cancel();
|
|
910
|
-
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
911
|
-
}
|
|
912
|
-
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
913
|
-
let text;
|
|
914
|
-
try {
|
|
915
|
-
text = await this.readCapped(res);
|
|
916
|
-
}
|
|
917
|
-
catch (e) {
|
|
918
|
-
if (e?.name === "AbortError" || att.signal.aborted) {
|
|
919
|
-
throw new APIConnectionError(0, this.abortMessage(att.why()));
|
|
920
|
-
}
|
|
921
|
-
if (e instanceof WosError)
|
|
922
|
-
throw e;
|
|
923
|
-
if (!(IDEMPOTENT_METHODS.has(method.toUpperCase()) && attempt + 1 < attempts)) {
|
|
924
|
-
throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
|
|
925
|
-
}
|
|
926
|
-
att.clear();
|
|
927
|
-
const delay = this.backoffMs(attempt);
|
|
928
|
-
logDebug(`${method} ${logPath} — body dropped mid-stream, retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
929
|
-
await this.backoffSleep(delay, deadlineAt);
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
finally {
|
|
933
|
-
att.clear();
|
|
934
|
-
}
|
|
935
|
-
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms (attempt ${attempt + 1}/${attempts})`);
|
|
936
|
-
if (!res.ok) {
|
|
937
|
-
let msg = text;
|
|
938
|
-
let requestId;
|
|
939
|
-
try {
|
|
940
|
-
const data = JSON.parse(text);
|
|
941
|
-
if (data && typeof data === "object") {
|
|
942
|
-
const err = data.error;
|
|
943
|
-
if (err && typeof err === "object") {
|
|
944
|
-
msg = err.message ?? err.type ?? msg;
|
|
945
|
-
if (typeof err.request_id === "string")
|
|
946
|
-
requestId = err.request_id;
|
|
947
|
-
}
|
|
948
|
-
else if (typeof err === "string")
|
|
949
|
-
msg = err;
|
|
950
|
-
else if (typeof data.message === "string")
|
|
951
|
-
msg = data.message;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
catch {
|
|
955
|
-
}
|
|
956
|
-
if (typeof msg === "string" && msg.length > MAX_ERR_MSG)
|
|
957
|
-
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
958
|
-
throw errorFor(res.status, msg, requestId);
|
|
959
|
-
}
|
|
960
|
-
if (!text) {
|
|
961
|
-
if (NO_BODY_STATUS.has(res.status))
|
|
962
|
-
return {};
|
|
963
|
-
throw new WosError(res.status, "empty response body — expected a JSON object");
|
|
964
|
-
}
|
|
965
|
-
let data;
|
|
966
|
-
try {
|
|
967
|
-
data = JSON.parse(text);
|
|
968
|
-
}
|
|
969
|
-
catch (e) {
|
|
970
|
-
throw new WosError(res.status, `invalid JSON in response: ${e?.message ?? e}`);
|
|
971
|
-
}
|
|
972
|
-
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
973
|
-
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
974
|
-
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
975
|
-
}
|
|
976
|
-
if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
977
|
-
data.replayed = true;
|
|
978
|
-
}
|
|
979
|
-
return data;
|
|
1305
|
+
const { res, body: buf } = await this.send(method, path, payload, extraHeaders, removes);
|
|
1306
|
+
const text = new TextDecoder().decode(buf);
|
|
1307
|
+
if (!text) {
|
|
1308
|
+
if (NO_BODY_STATUS.has(res.status))
|
|
1309
|
+
return {};
|
|
1310
|
+
throw new WosError(res.status, "empty response body — expected a JSON object");
|
|
1311
|
+
}
|
|
1312
|
+
let data;
|
|
1313
|
+
try {
|
|
1314
|
+
data = JSON.parse(text);
|
|
1315
|
+
}
|
|
1316
|
+
catch (e) {
|
|
1317
|
+
throw new WosError(res.status, `invalid JSON in response: ${cleanServerText(String(e?.message ?? e), 512)}`);
|
|
1318
|
+
}
|
|
1319
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
1320
|
+
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
1321
|
+
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
1322
|
+
}
|
|
1323
|
+
if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
1324
|
+
data.replayed = true;
|
|
980
1325
|
}
|
|
981
|
-
|
|
1326
|
+
return data;
|
|
982
1327
|
}
|
|
983
1328
|
}
|
|
984
1329
|
exports.Client = Client;
|