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