wontopos 2.2.40 → 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 +335 -171
- package/dist/cjs/wontopos.js +569 -254
- package/dist/wontopos.d.ts +335 -171
- package/dist/wontopos.js +569 -254
- 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, "_");
|
|
@@ -73,6 +76,9 @@ export function _resetStoreIdWarnings() {
|
|
|
73
76
|
}
|
|
74
77
|
const NO_BODY_STATUS = new Set([204, 205, 304]);
|
|
75
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
|
+
}
|
|
76
82
|
export const SEARCH_LIMIT_MIN = 5;
|
|
77
83
|
export const SEARCH_LIMIT_MAX = 20;
|
|
78
84
|
export const CONTEXT_LIMIT_MIN = 0;
|
|
@@ -89,6 +95,26 @@ function checkContextLimit(n) {
|
|
|
89
95
|
throw new Error(`context_limit must be an integer between ${CONTEXT_LIMIT_MIN} and ${CONTEXT_LIMIT_MAX}, got ${n}.`);
|
|
90
96
|
}
|
|
91
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
|
+
}
|
|
92
118
|
function normalizeImage(image) {
|
|
93
119
|
if (!image || typeof image !== "object") {
|
|
94
120
|
throw new Error("image must be an object — { data: '<base64>' }.");
|
|
@@ -118,14 +144,18 @@ const CONNECT_FAIL_CODES = new Set([
|
|
|
118
144
|
"EADDRNOTAVAIL",
|
|
119
145
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
120
146
|
]);
|
|
147
|
+
const CONNECT_SYSCALLS = new Set(["connect", "getaddrinfo"]);
|
|
148
|
+
const CONNECT_ONLY_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
121
149
|
function isConnectFailure(e) {
|
|
122
150
|
const stack = [e];
|
|
123
151
|
for (let steps = 0; stack.length && steps < 24; steps++) {
|
|
124
152
|
const cur = stack.pop();
|
|
125
153
|
if (!cur || typeof cur !== "object")
|
|
126
154
|
continue;
|
|
127
|
-
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)))) {
|
|
128
157
|
return true;
|
|
158
|
+
}
|
|
129
159
|
if (cur.cause)
|
|
130
160
|
stack.push(cur.cause);
|
|
131
161
|
if (Array.isArray(cur.errors))
|
|
@@ -133,6 +163,191 @@ function isConnectFailure(e) {
|
|
|
133
163
|
}
|
|
134
164
|
return false;
|
|
135
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
|
+
}
|
|
136
351
|
function maskKey(key) {
|
|
137
352
|
return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
|
|
138
353
|
}
|
|
@@ -183,11 +398,15 @@ function withForm(body, form, tz) {
|
|
|
183
398
|
export class WosError extends Error {
|
|
184
399
|
status;
|
|
185
400
|
requestId;
|
|
186
|
-
|
|
187
|
-
|
|
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 });
|
|
188
405
|
this.name = new.target.name;
|
|
189
406
|
this.status = status;
|
|
190
407
|
this.requestId = requestId;
|
|
408
|
+
this.type = init?.type;
|
|
409
|
+
this.details = init?.details;
|
|
191
410
|
}
|
|
192
411
|
}
|
|
193
412
|
export class APIConnectionError extends WosError {
|
|
@@ -203,8 +422,20 @@ export class PermissionDeniedError extends WosError {
|
|
|
203
422
|
export class NotFoundError extends WosError {
|
|
204
423
|
}
|
|
205
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
|
+
}
|
|
206
431
|
}
|
|
207
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
|
+
}
|
|
208
439
|
}
|
|
209
440
|
export class ServerError extends WosError {
|
|
210
441
|
}
|
|
@@ -215,13 +446,15 @@ const STATUS_ERRORS = {
|
|
|
215
446
|
403: PermissionDeniedError,
|
|
216
447
|
404: NotFoundError,
|
|
217
448
|
409: ConflictError,
|
|
449
|
+
413: BadRequestError,
|
|
450
|
+
422: BadRequestError,
|
|
218
451
|
429: RateLimitError,
|
|
219
452
|
};
|
|
220
|
-
function errorFor(status, message, requestId) {
|
|
453
|
+
function errorFor(status, message, requestId, init) {
|
|
221
454
|
const Cls = status === 0
|
|
222
455
|
? APIConnectionError
|
|
223
456
|
: STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
|
|
224
|
-
return new Cls(status, message, requestId);
|
|
457
|
+
return new Cls(status, message, requestId, init);
|
|
225
458
|
}
|
|
226
459
|
const KNOWN_FILTER_KEYS = new Set([
|
|
227
460
|
"categories",
|
|
@@ -240,6 +473,8 @@ const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
|
|
|
240
473
|
const KNOWN_ENGRAM_KEYS = new Set(["form", "tz"]);
|
|
241
474
|
function checkSearchOpts(opts) {
|
|
242
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);
|
|
243
478
|
}
|
|
244
479
|
function checkOpts(opts, known, what) {
|
|
245
480
|
for (const k of Object.keys(opts)) {
|
|
@@ -273,24 +508,33 @@ function editWithin(a, b, max) {
|
|
|
273
508
|
}
|
|
274
509
|
return prev[b.length] <= max;
|
|
275
510
|
}
|
|
276
|
-
function
|
|
277
|
-
if (!
|
|
511
|
+
function warnOnceUnknownKeys(obj, known, warned, message) {
|
|
512
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj))
|
|
278
513
|
return;
|
|
279
|
-
for (const k of Object.keys(
|
|
280
|
-
if (
|
|
514
|
+
for (const k of Object.keys(obj)) {
|
|
515
|
+
if (known.has(k) || warned.has(k))
|
|
281
516
|
continue;
|
|
282
|
-
|
|
283
|
-
while (
|
|
284
|
-
const oldest =
|
|
517
|
+
warned.add(k);
|
|
518
|
+
while (warned.size > WARNED_FILTER_KEYS_MAX) {
|
|
519
|
+
const oldest = warned.values().next().value;
|
|
285
520
|
if (oldest === undefined)
|
|
286
521
|
break;
|
|
287
|
-
|
|
522
|
+
warned.delete(oldest);
|
|
288
523
|
}
|
|
289
|
-
console.warn(
|
|
290
|
-
`so this filter has NO effect and the search is wider than you think. ` +
|
|
291
|
-
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
524
|
+
console.warn(message(k));
|
|
292
525
|
}
|
|
293
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
|
+
}
|
|
294
538
|
export function _resetFilterWarnings() {
|
|
295
539
|
warnedFilterKeys.clear();
|
|
296
540
|
}
|
|
@@ -305,6 +549,9 @@ export class Client {
|
|
|
305
549
|
deadlineMs;
|
|
306
550
|
signal;
|
|
307
551
|
fetchImpl;
|
|
552
|
+
userinfo;
|
|
553
|
+
pageRelative;
|
|
554
|
+
customFetch;
|
|
308
555
|
defaultUser;
|
|
309
556
|
_rateLimit = null;
|
|
310
557
|
get rateLimit() {
|
|
@@ -324,25 +571,41 @@ export class Client {
|
|
|
324
571
|
"en dash; copy the key from a plain-text field");
|
|
325
572
|
}
|
|
326
573
|
this.apiKey = key;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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;
|
|
330
589
|
this.model = opts.model ?? DEFAULT_MODEL;
|
|
331
590
|
if (this.model && !MODEL_RE.test(this.model)) {
|
|
332
591
|
throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
|
|
333
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
|
+
}
|
|
334
597
|
assertUsableStoreId(opts.userId);
|
|
335
598
|
this.defaultUser = opts.userId ?? DEFAULT_USER;
|
|
336
599
|
const mr = opts.maxRetries ?? opts.retries;
|
|
337
600
|
this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
|
|
338
|
-
|
|
339
|
-
this.deadlineMs = typeof dl === "number" && Number.isFinite(dl) && dl > 0 ? dl : undefined;
|
|
601
|
+
this.deadlineMs = checkMs(opts.deadlineMs);
|
|
340
602
|
this.signal = opts.signal;
|
|
341
603
|
const f = opts.fetch;
|
|
342
604
|
if (f !== undefined && typeof f !== "function") {
|
|
343
605
|
throw new Error("fetch must be a function (a fetch-compatible transport)");
|
|
344
606
|
}
|
|
345
|
-
this.
|
|
607
|
+
this.customFetch = f !== undefined && !DEFAULT_TRANSPORTS.has(f);
|
|
608
|
+
this.fetchImpl = (f ?? defaultTransport());
|
|
346
609
|
if (typeof this.fetchImpl !== "function") {
|
|
347
610
|
throw new Error("no fetch available in this runtime — pass one as `fetch` (Node 18+ has it built in; older Node needs undici)");
|
|
348
611
|
}
|
|
@@ -353,14 +616,8 @@ export class Client {
|
|
|
353
616
|
};
|
|
354
617
|
if (this.model)
|
|
355
618
|
this.headers["X-WOS-Model"] = this.model;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
host = new URL(this.base).hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
359
|
-
}
|
|
360
|
-
catch {
|
|
361
|
-
host = "";
|
|
362
|
-
}
|
|
363
|
-
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)) {
|
|
364
621
|
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
365
622
|
}
|
|
366
623
|
Object.defineProperty(this, "apiKey", { enumerable: false });
|
|
@@ -374,10 +631,10 @@ export class Client {
|
|
|
374
631
|
return new Client({ ...opts, apiKey: key });
|
|
375
632
|
}
|
|
376
633
|
toJSON() {
|
|
377
|
-
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) };
|
|
378
635
|
}
|
|
379
636
|
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
380
|
-
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)})`;
|
|
381
638
|
}
|
|
382
639
|
uid(userId) {
|
|
383
640
|
assertUsableStoreId(userId);
|
|
@@ -426,6 +683,7 @@ export class Client {
|
|
|
426
683
|
const body = { user_id: this.uid(userId), content, metadata };
|
|
427
684
|
if (opts.image !== undefined)
|
|
428
685
|
body.image = normalizeImage(opts.image);
|
|
686
|
+
warnOnUnknownMetadata(metadata);
|
|
429
687
|
return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
|
|
430
688
|
}
|
|
431
689
|
async store(content, userId, metadata = {}, opts = {}) {
|
|
@@ -446,6 +704,7 @@ export class Client {
|
|
|
446
704
|
async search(query, userId, limit = 10, opts = {}) {
|
|
447
705
|
checkSearchOpts(opts);
|
|
448
706
|
warnOnUnknownFilters(opts.filters);
|
|
707
|
+
limit ??= 10;
|
|
449
708
|
checkCount(limit, "limit");
|
|
450
709
|
const { extra, ...known } = opts;
|
|
451
710
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -454,6 +713,7 @@ export class Client {
|
|
|
454
713
|
async searchSelf(query, userId, limit = 10, opts = {}) {
|
|
455
714
|
checkSearchOpts(opts);
|
|
456
715
|
warnOnUnknownFilters(opts.filters);
|
|
716
|
+
limit ??= 10;
|
|
457
717
|
checkCount(limit, "limit");
|
|
458
718
|
const { extra, ...known } = opts;
|
|
459
719
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -462,6 +722,7 @@ export class Client {
|
|
|
462
722
|
async searchFull(query, userId, limit = 10, opts = {}) {
|
|
463
723
|
checkSearchOpts(opts);
|
|
464
724
|
warnOnUnknownFilters(opts.filters);
|
|
725
|
+
limit ??= 10;
|
|
465
726
|
checkCount(limit, "limit");
|
|
466
727
|
const { extra, ...known } = opts;
|
|
467
728
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -475,11 +736,11 @@ export class Client {
|
|
|
475
736
|
async recall(query, userId, opts = {}) {
|
|
476
737
|
checkOpts(opts, KNOWN_RECALL_KEYS, "recall");
|
|
477
738
|
const body = { user_id: this.uid(userId), query };
|
|
478
|
-
if (opts.limit
|
|
739
|
+
if (opts.limit != null) {
|
|
479
740
|
checkCount(opts.limit, "limit");
|
|
480
741
|
body.limit = opts.limit;
|
|
481
742
|
}
|
|
482
|
-
if (opts.context_limit
|
|
743
|
+
if (opts.context_limit != null) {
|
|
483
744
|
checkContextLimit(opts.context_limit);
|
|
484
745
|
body.context_limit = opts.context_limit;
|
|
485
746
|
}
|
|
@@ -509,7 +770,8 @@ export class Client {
|
|
|
509
770
|
return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then(memoryFromGet);
|
|
510
771
|
}
|
|
511
772
|
async listMemories(userId, opts = {}) {
|
|
512
|
-
const
|
|
773
|
+
const limit = optRange(opts.limit, "limit", 1, LIST_LIMIT_MAX);
|
|
774
|
+
const body = { user_id: this.uid(userId), limit: limit ?? 100 };
|
|
513
775
|
if (opts.cursor)
|
|
514
776
|
body.cursor = opts.cursor;
|
|
515
777
|
return this.post("/api/v1/memory/list", body);
|
|
@@ -519,16 +781,21 @@ export class Client {
|
|
|
519
781
|
const seen = new Set();
|
|
520
782
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
521
783
|
const p = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
|
|
522
|
-
|
|
784
|
+
const rows = asRecords(p.memories);
|
|
785
|
+
for (const m of rows)
|
|
523
786
|
yield m;
|
|
524
787
|
const next = p.next_cursor ?? undefined;
|
|
525
|
-
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.`);
|
|
526
793
|
return;
|
|
794
|
+
}
|
|
527
795
|
seen.add(next);
|
|
528
796
|
cursor = next;
|
|
529
797
|
}
|
|
530
|
-
throw
|
|
531
|
-
`answer, not the whole store.`);
|
|
798
|
+
throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
|
|
532
799
|
}
|
|
533
800
|
async exportMemories(userId) {
|
|
534
801
|
const out = [];
|
|
@@ -553,14 +820,16 @@ export class Client {
|
|
|
553
820
|
throw new Error("memoryId is required — the id of the memory whose image you want removed.");
|
|
554
821
|
}
|
|
555
822
|
const body = { user_id: this.uid(userId), memory_id: memoryId.trim() };
|
|
556
|
-
if (opts.preview)
|
|
557
|
-
|
|
823
|
+
if (!opts.preview)
|
|
824
|
+
return this.remove("/api/v1/memory/image", body);
|
|
825
|
+
body.preview = true;
|
|
558
826
|
return this.request("DELETE", "/api/v1/memory/image", body);
|
|
559
827
|
}
|
|
560
828
|
async listImages(userId, opts = {}) {
|
|
829
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
561
830
|
const body = { user_id: this.uid(userId) };
|
|
562
|
-
if (
|
|
563
|
-
body.limit =
|
|
831
|
+
if (limit !== undefined)
|
|
832
|
+
body.limit = limit;
|
|
564
833
|
if (opts.before !== undefined)
|
|
565
834
|
body.before = opts.before;
|
|
566
835
|
if (opts.skipIds !== undefined)
|
|
@@ -573,26 +842,30 @@ export class Client {
|
|
|
573
842
|
const seen = new Set();
|
|
574
843
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
575
844
|
const p = await this.listImages(userId, { limit: opts.pageSize, before, skipIds });
|
|
576
|
-
|
|
845
|
+
const rows = asRecords(p.images);
|
|
846
|
+
for (const m of rows)
|
|
577
847
|
yield m;
|
|
578
848
|
if (!p.has_more || !p.next_before)
|
|
579
849
|
return;
|
|
580
850
|
const key = `${p.next_before}|${(Array.isArray(p.next_skip_ids) ? p.next_skip_ids : []).join(",")}`;
|
|
581
|
-
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.`);
|
|
582
854
|
return;
|
|
855
|
+
}
|
|
583
856
|
seen.add(key);
|
|
584
857
|
before = p.next_before;
|
|
585
858
|
skipIds = Array.isArray(p.next_skip_ids) ? p.next_skip_ids : undefined;
|
|
586
859
|
}
|
|
587
|
-
throw
|
|
588
|
-
`answer, not the whole store.`);
|
|
860
|
+
throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
|
|
589
861
|
}
|
|
590
862
|
async revisions(userId, opts = {}) {
|
|
863
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
591
864
|
const body = {};
|
|
592
865
|
if (opts.include !== undefined)
|
|
593
866
|
body.include = opts.include;
|
|
594
|
-
if (
|
|
595
|
-
body.limit =
|
|
867
|
+
if (limit !== undefined)
|
|
868
|
+
body.limit = limit;
|
|
596
869
|
if (opts.before !== undefined)
|
|
597
870
|
body.before = opts.before;
|
|
598
871
|
if (opts.skipIds !== undefined)
|
|
@@ -610,9 +883,10 @@ export class Client {
|
|
|
610
883
|
if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
|
|
611
884
|
throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
|
|
612
885
|
}
|
|
886
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
613
887
|
const body = { user_id: this.uid(userId), speaker: speaker.trim() };
|
|
614
|
-
if (
|
|
615
|
-
body.limit =
|
|
888
|
+
if (limit !== undefined)
|
|
889
|
+
body.limit = limit;
|
|
616
890
|
if (opts.before !== undefined)
|
|
617
891
|
body.before = opts.before;
|
|
618
892
|
if (opts.skipIds !== undefined)
|
|
@@ -647,7 +921,7 @@ export class Client {
|
|
|
647
921
|
if (typeof userId !== "string" || !userId.trim())
|
|
648
922
|
throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
|
|
649
923
|
warnIfStoreIdCollapses(userId);
|
|
650
|
-
return this.
|
|
924
|
+
return this.remove("/api/v1/memory/collection", { user_id: userId });
|
|
651
925
|
}
|
|
652
926
|
async addSpeaker(speaker, userId) {
|
|
653
927
|
return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
@@ -657,7 +931,7 @@ export class Client {
|
|
|
657
931
|
return { ...r, speakers: asRecords(r?.speakers) };
|
|
658
932
|
}
|
|
659
933
|
async removeSpeaker(speaker, userId) {
|
|
660
|
-
return this.
|
|
934
|
+
return this.remove("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
661
935
|
}
|
|
662
936
|
async delete(userId, memoryId) {
|
|
663
937
|
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
@@ -674,22 +948,26 @@ export class Client {
|
|
|
674
948
|
post(path, body, idempotencyKey) {
|
|
675
949
|
return this.request("POST", path, body, idempotencyKey);
|
|
676
950
|
}
|
|
951
|
+
remove(path, body) {
|
|
952
|
+
return this.request("DELETE", path, body, undefined, true);
|
|
953
|
+
}
|
|
677
954
|
deadlineAt() {
|
|
678
955
|
return this.deadlineMs === undefined ? undefined : Date.now() + this.deadlineMs;
|
|
679
956
|
}
|
|
680
|
-
beginAttempt(deadlineAt) {
|
|
957
|
+
beginAttempt(deadlineAt, answer) {
|
|
681
958
|
let why = "timeout";
|
|
682
959
|
let budget = this.timeoutMs;
|
|
683
960
|
if (deadlineAt !== undefined) {
|
|
684
961
|
const left = deadlineAt - Date.now();
|
|
685
962
|
if (left <= 0)
|
|
686
|
-
throw new APIConnectionError(0, this.abortMessage("deadline"));
|
|
963
|
+
throw answer ?? new APIConnectionError(0, this.abortMessage("deadline"));
|
|
687
964
|
if (left < budget) {
|
|
688
965
|
budget = left;
|
|
689
966
|
why = "deadline";
|
|
690
967
|
}
|
|
691
968
|
}
|
|
692
969
|
const ctrl = new AbortController();
|
|
970
|
+
const endsAt = Date.now() + budget;
|
|
693
971
|
const timer = setTimeout(() => ctrl.abort(), budget);
|
|
694
972
|
const outer = this.signal;
|
|
695
973
|
const onCaller = () => {
|
|
@@ -709,6 +987,7 @@ export class Client {
|
|
|
709
987
|
outer?.removeEventListener("abort", onCaller);
|
|
710
988
|
},
|
|
711
989
|
why: () => why,
|
|
990
|
+
endsAt,
|
|
712
991
|
};
|
|
713
992
|
}
|
|
714
993
|
abortMessage(why) {
|
|
@@ -718,10 +997,10 @@ export class Client {
|
|
|
718
997
|
return `deadline of ${this.deadlineMs}ms exhausted`;
|
|
719
998
|
return `request timed out after ${this.timeoutMs}ms`;
|
|
720
999
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1000
|
+
fits(ms, deadlineAt) {
|
|
1001
|
+
return deadlineAt === undefined || ms <= deadlineAt - Date.now();
|
|
1002
|
+
}
|
|
1003
|
+
async pause(ms) {
|
|
725
1004
|
const outer = this.signal;
|
|
726
1005
|
if (!outer) {
|
|
727
1006
|
await new Promise((r) => setTimeout(r, ms));
|
|
@@ -739,17 +1018,82 @@ export class Client {
|
|
|
739
1018
|
outer.addEventListener("abort", done);
|
|
740
1019
|
});
|
|
741
1020
|
}
|
|
742
|
-
|
|
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
|
+
}
|
|
743
1063
|
const attempts = this.maxRetries + 1;
|
|
1064
|
+
const upper = method.toUpperCase();
|
|
1065
|
+
const idempotent = IDEMPOTENT_METHODS.has(upper);
|
|
1066
|
+
const logPath = path.split("?")[0];
|
|
744
1067
|
const deadlineAt = this.deadlineAt();
|
|
1068
|
+
const headers = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
|
|
1069
|
+
let ambiguous = false;
|
|
1070
|
+
let refused;
|
|
745
1071
|
for (let attempt = 0;; attempt++) {
|
|
746
|
-
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
|
+
};
|
|
747
1091
|
let res;
|
|
748
1092
|
try {
|
|
749
1093
|
res = await this.fetchImpl(`${this.base}${path}`, {
|
|
750
|
-
method
|
|
751
|
-
headers
|
|
752
|
-
body:
|
|
1094
|
+
method,
|
|
1095
|
+
headers,
|
|
1096
|
+
body: payload,
|
|
753
1097
|
signal: att.signal,
|
|
754
1098
|
redirect: "manual",
|
|
755
1099
|
});
|
|
@@ -757,63 +1101,109 @@ export class Client {
|
|
|
757
1101
|
catch (e) {
|
|
758
1102
|
att.clear();
|
|
759
1103
|
const timedOut = e?.name === "AbortError" || att.signal.aborted;
|
|
760
|
-
if (
|
|
761
|
-
|
|
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");
|
|
762
1111
|
continue;
|
|
763
1112
|
}
|
|
764
|
-
throw
|
|
1113
|
+
throw this.networkError(e, path);
|
|
765
1114
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
void res.body?.cancel();
|
|
770
|
-
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);
|
|
771
1118
|
}
|
|
772
|
-
|
|
773
|
-
if (
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
let msg = await this.readCapped(res).catch(() => "");
|
|
781
|
-
try {
|
|
782
|
-
const data = JSON.parse(msg);
|
|
783
|
-
const err = data?.error;
|
|
784
|
-
if (err && typeof err === "object")
|
|
785
|
-
msg = err.message ?? err.type ?? msg;
|
|
786
|
-
else if (typeof err === "string")
|
|
787
|
-
msg = err;
|
|
788
|
-
}
|
|
789
|
-
catch {
|
|
790
|
-
}
|
|
791
|
-
if (msg.length > MAX_ERR_MSG)
|
|
792
|
-
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
793
|
-
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);
|
|
794
1127
|
}
|
|
795
|
-
|
|
1128
|
+
};
|
|
1129
|
+
const readErrorBody = async (withinMs) => {
|
|
796
1130
|
try {
|
|
797
|
-
|
|
1131
|
+
return { body: await this.readCappedBytes(res, withinMs) };
|
|
798
1132
|
}
|
|
799
1133
|
catch (e) {
|
|
800
|
-
|
|
801
|
-
|
|
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
|
+
}
|
|
802
1164
|
}
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
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);
|
|
806
1184
|
}
|
|
807
|
-
|
|
808
|
-
|
|
1185
|
+
const body = await readBody();
|
|
1186
|
+
if (body === null) {
|
|
1187
|
+
ambiguous = true;
|
|
1188
|
+
att.clear();
|
|
1189
|
+
await pauseWithoutAnswer("body dropped mid-stream");
|
|
1190
|
+
continue;
|
|
809
1191
|
}
|
|
810
|
-
|
|
1192
|
+
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms ${tag}`);
|
|
1193
|
+
return { res, body };
|
|
811
1194
|
}
|
|
812
1195
|
finally {
|
|
813
1196
|
att.clear();
|
|
814
1197
|
}
|
|
815
1198
|
}
|
|
816
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
|
+
}
|
|
817
1207
|
idemHeader(key) {
|
|
818
1208
|
if (key === undefined || key === null)
|
|
819
1209
|
return undefined;
|
|
@@ -823,58 +1213,72 @@ export class Client {
|
|
|
823
1213
|
return { "Idempotency-Key": key };
|
|
824
1214
|
}
|
|
825
1215
|
backoffMs(attempt, retryAfter) {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
return Math.min(30_000, s * 1000);
|
|
830
|
-
const t = Date.parse(retryAfter);
|
|
831
|
-
if (Number.isFinite(t))
|
|
832
|
-
return Math.min(30_000, Math.max(0, t - Date.now()));
|
|
833
|
-
}
|
|
1216
|
+
const ra = retryAfterMs(retryAfter ?? null);
|
|
1217
|
+
if (ra !== undefined)
|
|
1218
|
+
return ra;
|
|
834
1219
|
return Math.min(8_000, 500 * 2 ** attempt) + Math.random() * 250;
|
|
835
1220
|
}
|
|
836
|
-
async
|
|
837
|
-
return new TextDecoder().decode(await this.readCappedBytes(res));
|
|
838
|
-
}
|
|
839
|
-
async readCappedBytes(res) {
|
|
1221
|
+
async readCappedBytes(res, withinMs) {
|
|
840
1222
|
const cl = res.headers.get("content-length");
|
|
841
1223
|
if (cl && Number(cl) > MAX_RESPONSE_BYTES) {
|
|
1224
|
+
discard(res);
|
|
842
1225
|
throw new WosError(res.status, `response too large (${cl} bytes) — refusing to buffer it`);
|
|
843
1226
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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;
|
|
848
1270
|
}
|
|
849
1271
|
return buf;
|
|
850
1272
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
let size = 0;
|
|
854
|
-
for (;;) {
|
|
855
|
-
const { done, value } = await reader.read();
|
|
856
|
-
if (done)
|
|
857
|
-
break;
|
|
858
|
-
size += value.byteLength;
|
|
859
|
-
if (size > MAX_RESPONSE_BYTES) {
|
|
860
|
-
void reader.cancel();
|
|
861
|
-
throw new WosError(res.status, "response too large — refusing to buffer it");
|
|
862
|
-
}
|
|
863
|
-
parts.push(value);
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
throw late && !(e instanceof SlowBody) ? slow() : e;
|
|
864
1275
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
for (const p of parts) {
|
|
868
|
-
buf.set(p, off);
|
|
869
|
-
off += p.byteLength;
|
|
1276
|
+
finally {
|
|
1277
|
+
clearTimeout(timer);
|
|
870
1278
|
}
|
|
871
|
-
return buf;
|
|
872
1279
|
}
|
|
873
|
-
async request(method, path, body, idempotencyKey) {
|
|
1280
|
+
async request(method, path, body, idempotencyKey, removes = false) {
|
|
874
1281
|
const extraHeaders = this.idemHeader(idempotencyKey);
|
|
875
|
-
const attempts = this.maxRetries + 1;
|
|
876
|
-
const logPath = path.split("?")[0];
|
|
877
|
-
const deadlineAt = this.deadlineAt();
|
|
878
1282
|
let payload;
|
|
879
1283
|
try {
|
|
880
1284
|
payload = body === undefined ? undefined : JSON.stringify(body);
|
|
@@ -883,117 +1287,28 @@ export class Client {
|
|
|
883
1287
|
throw new Error(`request body could not be serialized: ${e instanceof Error ? e.message : String(e)}. ` +
|
|
884
1288
|
"Pass plain JSON values — an ORM entity, a BigInt or a circular reference cannot be sent.");
|
|
885
1289
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
await this.backoffSleep(delay, deadlineAt);
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
throw new APIConnectionError(0, timedOut ? this.abortMessage(att.why()) : `network error: ${e?.message ?? e}`);
|
|
910
|
-
}
|
|
911
|
-
const retryable = RETRY_ALWAYS.has(res.status) ||
|
|
912
|
-
(RETRY_IF_IDEMPOTENT.has(res.status) && IDEMPOTENT_METHODS.has(method.toUpperCase()));
|
|
913
|
-
if (retryable && attempt + 1 < attempts) {
|
|
914
|
-
att.clear();
|
|
915
|
-
const retryAfter = res.headers.get("Retry-After");
|
|
916
|
-
void res.body?.cancel();
|
|
917
|
-
const delay = this.backoffMs(attempt, retryAfter);
|
|
918
|
-
logDebug(`${method} ${logPath} -> ${res.status} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
919
|
-
await this.backoffSleep(delay, deadlineAt);
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
923
|
-
att.clear();
|
|
924
|
-
void res.body?.cancel();
|
|
925
|
-
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
926
|
-
}
|
|
927
|
-
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
928
|
-
let text;
|
|
929
|
-
try {
|
|
930
|
-
text = await this.readCapped(res);
|
|
931
|
-
}
|
|
932
|
-
catch (e) {
|
|
933
|
-
if (e?.name === "AbortError" || att.signal.aborted) {
|
|
934
|
-
throw new APIConnectionError(0, this.abortMessage(att.why()));
|
|
935
|
-
}
|
|
936
|
-
if (e instanceof WosError)
|
|
937
|
-
throw e;
|
|
938
|
-
if (!(IDEMPOTENT_METHODS.has(method.toUpperCase()) && attempt + 1 < attempts)) {
|
|
939
|
-
throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
|
|
940
|
-
}
|
|
941
|
-
att.clear();
|
|
942
|
-
const delay = this.backoffMs(attempt);
|
|
943
|
-
logDebug(`${method} ${logPath} — body dropped mid-stream, retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
944
|
-
await this.backoffSleep(delay, deadlineAt);
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
finally {
|
|
948
|
-
att.clear();
|
|
949
|
-
}
|
|
950
|
-
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms (attempt ${attempt + 1}/${attempts})`);
|
|
951
|
-
if (!res.ok) {
|
|
952
|
-
let msg = text;
|
|
953
|
-
let requestId;
|
|
954
|
-
try {
|
|
955
|
-
const data = JSON.parse(text);
|
|
956
|
-
if (data && typeof data === "object") {
|
|
957
|
-
const err = data.error;
|
|
958
|
-
if (err && typeof err === "object") {
|
|
959
|
-
msg = err.message ?? err.type ?? msg;
|
|
960
|
-
if (typeof err.request_id === "string")
|
|
961
|
-
requestId = err.request_id;
|
|
962
|
-
}
|
|
963
|
-
else if (typeof err === "string")
|
|
964
|
-
msg = err;
|
|
965
|
-
else if (typeof data.message === "string")
|
|
966
|
-
msg = data.message;
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
catch {
|
|
970
|
-
}
|
|
971
|
-
if (typeof msg === "string" && msg.length > MAX_ERR_MSG)
|
|
972
|
-
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
973
|
-
throw errorFor(res.status, msg, requestId);
|
|
974
|
-
}
|
|
975
|
-
if (!text) {
|
|
976
|
-
if (NO_BODY_STATUS.has(res.status))
|
|
977
|
-
return {};
|
|
978
|
-
throw new WosError(res.status, "empty response body — expected a JSON object");
|
|
979
|
-
}
|
|
980
|
-
let data;
|
|
981
|
-
try {
|
|
982
|
-
data = JSON.parse(text);
|
|
983
|
-
}
|
|
984
|
-
catch (e) {
|
|
985
|
-
throw new WosError(res.status, `invalid JSON in response: ${e?.message ?? e}`);
|
|
986
|
-
}
|
|
987
|
-
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
988
|
-
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
989
|
-
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
990
|
-
}
|
|
991
|
-
if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
992
|
-
data.replayed = true;
|
|
993
|
-
}
|
|
994
|
-
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;
|
|
995
1310
|
}
|
|
996
|
-
|
|
1311
|
+
return data;
|
|
997
1312
|
}
|
|
998
1313
|
}
|
|
999
1314
|
export const WME = Client;
|