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