wontopos 2.2.40 → 2.2.42
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 +327 -166
- package/dist/cjs/wontopos.js +584 -254
- package/dist/wontopos.d.ts +327 -166
- package/dist/wontopos.js +584 -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.42";
|
|
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,29 @@ 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"]);
|
|
20
|
+
const READ_POSTS = new Set([
|
|
21
|
+
"/api/v1/memory/search",
|
|
22
|
+
"/api/v1/memory/recall",
|
|
23
|
+
"/api/v1/memory/get",
|
|
24
|
+
"/api/v1/memory/list",
|
|
25
|
+
"/api/v1/memory/stats",
|
|
26
|
+
"/api/v1/memory/history",
|
|
27
|
+
"/api/v1/memory/lineage",
|
|
28
|
+
"/api/v1/memory/by-speaker",
|
|
29
|
+
"/api/v1/memory/images",
|
|
30
|
+
"/api/v1/memory/image",
|
|
31
|
+
"/api/v1/engram/run",
|
|
32
|
+
"/api/v1/won/revisions",
|
|
33
|
+
]);
|
|
18
34
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
19
35
|
const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
20
36
|
const MODEL_RE = /^[A-Za-z0-9._-]+$/;
|
|
21
37
|
const MAX_ERR_MSG = 4096;
|
|
38
|
+
const MAX_ERR_DETAILS = 8192;
|
|
22
39
|
const IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9._:\-]{1,128}$/;
|
|
23
40
|
function normalizeStoreId(id) {
|
|
24
41
|
return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
@@ -73,6 +90,9 @@ export function _resetStoreIdWarnings() {
|
|
|
73
90
|
}
|
|
74
91
|
const NO_BODY_STATUS = new Set([204, 205, 304]);
|
|
75
92
|
const MAX_PAGES = 20_000;
|
|
93
|
+
function truncatedWalk(why) {
|
|
94
|
+
return new Error(`${why} This is a truncated answer, not the whole store.`);
|
|
95
|
+
}
|
|
76
96
|
export const SEARCH_LIMIT_MIN = 5;
|
|
77
97
|
export const SEARCH_LIMIT_MAX = 20;
|
|
78
98
|
export const CONTEXT_LIMIT_MIN = 0;
|
|
@@ -89,6 +109,26 @@ function checkContextLimit(n) {
|
|
|
89
109
|
throw new Error(`context_limit must be an integer between ${CONTEXT_LIMIT_MIN} and ${CONTEXT_LIMIT_MAX}, got ${n}.`);
|
|
90
110
|
}
|
|
91
111
|
}
|
|
112
|
+
const PAGE_LIMIT_MIN = 5;
|
|
113
|
+
const PAGE_LIMIT_MAX = 20;
|
|
114
|
+
const LIST_LIMIT_MAX = 500;
|
|
115
|
+
const MAX_IMAGES_MAX = 5;
|
|
116
|
+
function checkRange(n, name, min, max) {
|
|
117
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < min || n > max) {
|
|
118
|
+
throw new Error(`${name} must be an integer between ${min} and ${max}, got ${String(n)}.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function optRange(n, name, min, max) {
|
|
122
|
+
if (n === undefined || n === null)
|
|
123
|
+
return undefined;
|
|
124
|
+
checkRange(n, name, min, max);
|
|
125
|
+
return n;
|
|
126
|
+
}
|
|
127
|
+
function checkMs(v) {
|
|
128
|
+
if (typeof v !== "number" || Number.isNaN(v) || v <= 0)
|
|
129
|
+
return undefined;
|
|
130
|
+
return Math.min(v, MAX_TIMER_MS);
|
|
131
|
+
}
|
|
92
132
|
function normalizeImage(image) {
|
|
93
133
|
if (!image || typeof image !== "object") {
|
|
94
134
|
throw new Error("image must be an object — { data: '<base64>' }.");
|
|
@@ -118,14 +158,18 @@ const CONNECT_FAIL_CODES = new Set([
|
|
|
118
158
|
"EADDRNOTAVAIL",
|
|
119
159
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
120
160
|
]);
|
|
161
|
+
const CONNECT_SYSCALLS = new Set(["connect", "getaddrinfo"]);
|
|
162
|
+
const CONNECT_ONLY_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
121
163
|
function isConnectFailure(e) {
|
|
122
164
|
const stack = [e];
|
|
123
165
|
for (let steps = 0; stack.length && steps < 24; steps++) {
|
|
124
166
|
const cur = stack.pop();
|
|
125
167
|
if (!cur || typeof cur !== "object")
|
|
126
168
|
continue;
|
|
127
|
-
if (typeof cur.code === "string" &&
|
|
169
|
+
if (typeof cur.code === "string" &&
|
|
170
|
+
(CONNECT_ONLY_CODES.has(cur.code) || (CONNECT_FAIL_CODES.has(cur.code) && CONNECT_SYSCALLS.has(cur.syscall)))) {
|
|
128
171
|
return true;
|
|
172
|
+
}
|
|
129
173
|
if (cur.cause)
|
|
130
174
|
stack.push(cur.cause);
|
|
131
175
|
if (Array.isArray(cur.errors))
|
|
@@ -133,6 +177,191 @@ function isConnectFailure(e) {
|
|
|
133
177
|
}
|
|
134
178
|
return false;
|
|
135
179
|
}
|
|
180
|
+
function causeCode(e) {
|
|
181
|
+
const queue = [e];
|
|
182
|
+
for (let steps = 0; queue.length && steps < 24; steps++) {
|
|
183
|
+
const cur = queue.shift();
|
|
184
|
+
if (!cur || typeof cur !== "object")
|
|
185
|
+
continue;
|
|
186
|
+
if (typeof cur.code === "string" && /^[A-Z0-9_]{1,64}$/.test(cur.code))
|
|
187
|
+
return cur.code;
|
|
188
|
+
if (cur.cause)
|
|
189
|
+
queue.push(cur.cause);
|
|
190
|
+
if (Array.isArray(cur.errors))
|
|
191
|
+
queue.push(...cur.errors.slice(0, 8));
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
function cleanServerText(s, max = MAX_ERR_MSG) {
|
|
196
|
+
const clean = s.replace(/[\u0000-\u001f\u007f]/g, "");
|
|
197
|
+
return clean.length > max ? clean.slice(0, max) + "…(truncated)" : clean;
|
|
198
|
+
}
|
|
199
|
+
function cleanDetails(d) {
|
|
200
|
+
if (!d)
|
|
201
|
+
return undefined;
|
|
202
|
+
const clean = (v) => {
|
|
203
|
+
if (typeof v === "string")
|
|
204
|
+
return cleanServerText(v);
|
|
205
|
+
if (Array.isArray(v))
|
|
206
|
+
return v.map(clean);
|
|
207
|
+
if (v && typeof v === "object") {
|
|
208
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [cleanServerText(k), clean(x)]));
|
|
209
|
+
}
|
|
210
|
+
return v;
|
|
211
|
+
};
|
|
212
|
+
let entries;
|
|
213
|
+
try {
|
|
214
|
+
JSON.stringify(d);
|
|
215
|
+
entries = Object.entries(clean(d)).map(([k, v]) => [
|
|
216
|
+
k,
|
|
217
|
+
v,
|
|
218
|
+
JSON.stringify(k).length + JSON.stringify(v).length + 2,
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const keep = new Set();
|
|
225
|
+
let size = 2;
|
|
226
|
+
for (const [k, , n] of [...entries].sort((a, b) => a[2] - b[2])) {
|
|
227
|
+
if (size + n > MAX_ERR_DETAILS)
|
|
228
|
+
break;
|
|
229
|
+
keep.add(k);
|
|
230
|
+
size += n;
|
|
231
|
+
}
|
|
232
|
+
if (!keep.size)
|
|
233
|
+
return undefined;
|
|
234
|
+
return Object.fromEntries(entries.filter(([k]) => keep.has(k)).map(([k, v]) => [k, v]));
|
|
235
|
+
}
|
|
236
|
+
function parseErrorText(text) {
|
|
237
|
+
let data;
|
|
238
|
+
try {
|
|
239
|
+
data = JSON.parse(text);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return { message: text };
|
|
243
|
+
}
|
|
244
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
245
|
+
return { message: text };
|
|
246
|
+
const err = data.error;
|
|
247
|
+
if (err && typeof err === "object" && !Array.isArray(err)) {
|
|
248
|
+
const { message, type, request_id, ...rest } = err;
|
|
249
|
+
return {
|
|
250
|
+
message: typeof message === "string" ? message : typeof type === "string" ? type : text,
|
|
251
|
+
requestId: typeof request_id === "string" ? request_id : undefined,
|
|
252
|
+
type: typeof type === "string" ? type : undefined,
|
|
253
|
+
details: Object.keys(rest).length ? rest : undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (typeof err === "string")
|
|
257
|
+
return { message: err };
|
|
258
|
+
if (typeof data.message === "string")
|
|
259
|
+
return { message: data.message };
|
|
260
|
+
return { message: text };
|
|
261
|
+
}
|
|
262
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
263
|
+
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$/;
|
|
264
|
+
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$/;
|
|
265
|
+
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})$/;
|
|
266
|
+
function retryAfterMs(v, now = Date.now()) {
|
|
267
|
+
if (v == null)
|
|
268
|
+
return undefined;
|
|
269
|
+
const s = v.trim();
|
|
270
|
+
if (/^\d+$/.test(s))
|
|
271
|
+
return Math.min(Number(s), Number.MAX_SAFE_INTEGER / 1000) * 1000;
|
|
272
|
+
let parts;
|
|
273
|
+
let m;
|
|
274
|
+
if ((m = IMF_FIXDATE.exec(s)))
|
|
275
|
+
parts = [m[1], m[2], Number(m[3]), m[4], m[5], m[6]];
|
|
276
|
+
else if ((m = RFC850_DATE.exec(s))) {
|
|
277
|
+
let year = 2000 + Number(m[3]);
|
|
278
|
+
if (year > new Date(now).getUTCFullYear() + 50)
|
|
279
|
+
year -= 100;
|
|
280
|
+
parts = [m[1], m[2], year, m[4], m[5], m[6]];
|
|
281
|
+
}
|
|
282
|
+
else if ((m = ASCTIME_DATE.exec(s)))
|
|
283
|
+
parts = [m[2].trim(), m[1], Number(m[6]), m[3], m[4], m[5]];
|
|
284
|
+
if (!parts)
|
|
285
|
+
return undefined;
|
|
286
|
+
const [day, mon, year, hh, mm, ss] = parts;
|
|
287
|
+
const month = MONTHS.indexOf(mon);
|
|
288
|
+
const d = Number(day), h = Number(hh), mi = Number(mm), se = Number(ss);
|
|
289
|
+
if (month < 0 || d < 1 || d > 31 || h > 23 || mi > 59 || se > 60)
|
|
290
|
+
return undefined;
|
|
291
|
+
return Math.max(0, Date.UTC(year, month, d, h, mi, se) - now);
|
|
292
|
+
}
|
|
293
|
+
const BRIEF_BODY_MS = 1_000;
|
|
294
|
+
class SlowBody extends Error {
|
|
295
|
+
}
|
|
296
|
+
const DEFAULT_TRANSPORTS = new WeakSet();
|
|
297
|
+
function defaultTransport() {
|
|
298
|
+
const g = globalThis.fetch;
|
|
299
|
+
if (typeof g !== "function")
|
|
300
|
+
return undefined;
|
|
301
|
+
const bound = g.bind(globalThis);
|
|
302
|
+
DEFAULT_TRANSPORTS.add(bound);
|
|
303
|
+
return bound;
|
|
304
|
+
}
|
|
305
|
+
function discard(res) {
|
|
306
|
+
res.body?.cancel().catch(() => { });
|
|
307
|
+
}
|
|
308
|
+
function tryUrl(s, base) {
|
|
309
|
+
try {
|
|
310
|
+
return base === undefined ? new URL(s) : new URL(s, base);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function pageHref() {
|
|
317
|
+
try {
|
|
318
|
+
const href = globalThis.location?.href;
|
|
319
|
+
return typeof href === "string" ? href : undefined;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function parseBase(base) {
|
|
326
|
+
let url = tryUrl(base);
|
|
327
|
+
if (!url) {
|
|
328
|
+
const page = pageHref();
|
|
329
|
+
if (page !== undefined)
|
|
330
|
+
url = tryUrl(base, page);
|
|
331
|
+
}
|
|
332
|
+
return url && (url.protocol === "http:" || url.protocol === "https:") ? url : undefined;
|
|
333
|
+
}
|
|
334
|
+
function asPageRelative(base) {
|
|
335
|
+
return tryUrl(base) ? undefined : tryUrl(base, "http://page.invalid/");
|
|
336
|
+
}
|
|
337
|
+
const BASE_URL_ENDS = /^\s+|\s+$/g;
|
|
338
|
+
function notAUrl(base) {
|
|
339
|
+
return `baseUrl is not a URL: ${JSON.stringify(maskBase(base))} (expected e.g. ${DEFAULT_BASE_URL})`;
|
|
340
|
+
}
|
|
341
|
+
function maskBase(raw) {
|
|
342
|
+
const at = raw.lastIndexOf("@");
|
|
343
|
+
if (at === -1)
|
|
344
|
+
return raw;
|
|
345
|
+
const from = /^(?:[a-z][a-z0-9+.-]*:)?[\\/]*/i.exec(raw)[0].length;
|
|
346
|
+
return `${raw.slice(0, from)}***@${raw.slice(at + 1)}`;
|
|
347
|
+
}
|
|
348
|
+
function maskUserinfo(text) {
|
|
349
|
+
let out = "";
|
|
350
|
+
let done = 0;
|
|
351
|
+
for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", at + 1)) {
|
|
352
|
+
let start = at;
|
|
353
|
+
while (start > 0 && !/[\s\\/@]/.test(text[start - 1]))
|
|
354
|
+
start--;
|
|
355
|
+
const run = text.slice(start, at);
|
|
356
|
+
const afterSlash = start > 0 && (text[start - 1] === "/" || text[start - 1] === "\\");
|
|
357
|
+
if (!afterSlash && !run.includes(":"))
|
|
358
|
+
continue;
|
|
359
|
+
const scheme = afterSlash ? "" : (/^[a-z][a-z0-9+.-]*:/i.exec(run)?.[0] ?? "");
|
|
360
|
+
out += `${text.slice(done, start + scheme.length)}***`;
|
|
361
|
+
done = at;
|
|
362
|
+
}
|
|
363
|
+
return out + text.slice(done);
|
|
364
|
+
}
|
|
136
365
|
function maskKey(key) {
|
|
137
366
|
return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
|
|
138
367
|
}
|
|
@@ -183,11 +412,15 @@ function withForm(body, form, tz) {
|
|
|
183
412
|
export class WosError extends Error {
|
|
184
413
|
status;
|
|
185
414
|
requestId;
|
|
186
|
-
|
|
187
|
-
|
|
415
|
+
type;
|
|
416
|
+
details;
|
|
417
|
+
constructor(status, message, requestId, init) {
|
|
418
|
+
super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`, init?.cause === undefined ? undefined : { cause: init.cause });
|
|
188
419
|
this.name = new.target.name;
|
|
189
420
|
this.status = status;
|
|
190
421
|
this.requestId = requestId;
|
|
422
|
+
this.type = init?.type;
|
|
423
|
+
this.details = init?.details;
|
|
191
424
|
}
|
|
192
425
|
}
|
|
193
426
|
export class APIConnectionError extends WosError {
|
|
@@ -203,8 +436,20 @@ export class PermissionDeniedError extends WosError {
|
|
|
203
436
|
export class NotFoundError extends WosError {
|
|
204
437
|
}
|
|
205
438
|
export class ConflictError extends WosError {
|
|
439
|
+
conflictsWith;
|
|
440
|
+
constructor(status, message, requestId, init) {
|
|
441
|
+
super(status, message, requestId, init);
|
|
442
|
+
const c = init?.details?.conflicts_with;
|
|
443
|
+
this.conflictsWith = typeof c === "string" ? c : undefined;
|
|
444
|
+
}
|
|
206
445
|
}
|
|
207
446
|
export class RateLimitError extends WosError {
|
|
447
|
+
retryAfter;
|
|
448
|
+
constructor(status, message, requestId, init) {
|
|
449
|
+
super(status, message, requestId, init);
|
|
450
|
+
const r = init?.retryAfter;
|
|
451
|
+
this.retryAfter = typeof r === "number" && Number.isFinite(r) && r >= 0 ? r : undefined;
|
|
452
|
+
}
|
|
208
453
|
}
|
|
209
454
|
export class ServerError extends WosError {
|
|
210
455
|
}
|
|
@@ -215,13 +460,15 @@ const STATUS_ERRORS = {
|
|
|
215
460
|
403: PermissionDeniedError,
|
|
216
461
|
404: NotFoundError,
|
|
217
462
|
409: ConflictError,
|
|
463
|
+
413: BadRequestError,
|
|
464
|
+
422: BadRequestError,
|
|
218
465
|
429: RateLimitError,
|
|
219
466
|
};
|
|
220
|
-
function errorFor(status, message, requestId) {
|
|
467
|
+
function errorFor(status, message, requestId, init) {
|
|
221
468
|
const Cls = status === 0
|
|
222
469
|
? APIConnectionError
|
|
223
470
|
: STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
|
|
224
|
-
return new Cls(status, message, requestId);
|
|
471
|
+
return new Cls(status, message, requestId, init);
|
|
225
472
|
}
|
|
226
473
|
const KNOWN_FILTER_KEYS = new Set([
|
|
227
474
|
"categories",
|
|
@@ -240,6 +487,8 @@ const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
|
|
|
240
487
|
const KNOWN_ENGRAM_KEYS = new Set(["form", "tz"]);
|
|
241
488
|
function checkSearchOpts(opts) {
|
|
242
489
|
checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
|
|
490
|
+
const o = opts;
|
|
491
|
+
optRange(o.max_images !== undefined ? o.max_images : o.extra?.max_images, "max_images", 0, MAX_IMAGES_MAX);
|
|
243
492
|
}
|
|
244
493
|
function checkOpts(opts, known, what) {
|
|
245
494
|
for (const k of Object.keys(opts)) {
|
|
@@ -273,24 +522,33 @@ function editWithin(a, b, max) {
|
|
|
273
522
|
}
|
|
274
523
|
return prev[b.length] <= max;
|
|
275
524
|
}
|
|
276
|
-
function
|
|
277
|
-
if (!
|
|
525
|
+
function warnOnceUnknownKeys(obj, known, warned, message) {
|
|
526
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj))
|
|
278
527
|
return;
|
|
279
|
-
for (const k of Object.keys(
|
|
280
|
-
if (
|
|
528
|
+
for (const k of Object.keys(obj)) {
|
|
529
|
+
if (known.has(k) || warned.has(k))
|
|
281
530
|
continue;
|
|
282
|
-
|
|
283
|
-
while (
|
|
284
|
-
const oldest =
|
|
531
|
+
warned.add(k);
|
|
532
|
+
while (warned.size > WARNED_FILTER_KEYS_MAX) {
|
|
533
|
+
const oldest = warned.values().next().value;
|
|
285
534
|
if (oldest === undefined)
|
|
286
535
|
break;
|
|
287
|
-
|
|
536
|
+
warned.delete(oldest);
|
|
288
537
|
}
|
|
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(", ")}.`);
|
|
538
|
+
console.warn(message(k));
|
|
292
539
|
}
|
|
293
540
|
}
|
|
541
|
+
function warnOnUnknownFilters(filters) {
|
|
542
|
+
warnOnceUnknownKeys(filters, KNOWN_FILTER_KEYS, warnedFilterKeys, (k) => `wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
543
|
+
`so this filter has NO effect and the search is wider than you think. ` +
|
|
544
|
+
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
545
|
+
}
|
|
546
|
+
const KNOWN_METADATA_KEYS = new Set(["speaker", "event_date", "category", "conversation_id"]);
|
|
547
|
+
const warnedMetadataKeys = new Set();
|
|
548
|
+
function warnOnUnknownMetadata(metadata) {
|
|
549
|
+
warnOnceUnknownKeys(metadata, KNOWN_METADATA_KEYS, warnedMetadataKeys, (k) => `wontopos: metadata key ${JSON.stringify(k)} is not kept: the service keeps only ` +
|
|
550
|
+
`${[...KNOWN_METADATA_KEYS].join(", ")} and drops the rest, so this value is not stored.`);
|
|
551
|
+
}
|
|
294
552
|
export function _resetFilterWarnings() {
|
|
295
553
|
warnedFilterKeys.clear();
|
|
296
554
|
}
|
|
@@ -305,6 +563,9 @@ export class Client {
|
|
|
305
563
|
deadlineMs;
|
|
306
564
|
signal;
|
|
307
565
|
fetchImpl;
|
|
566
|
+
userinfo;
|
|
567
|
+
pageRelative;
|
|
568
|
+
customFetch;
|
|
308
569
|
defaultUser;
|
|
309
570
|
_rateLimit = null;
|
|
310
571
|
get rateLimit() {
|
|
@@ -324,25 +585,41 @@ export class Client {
|
|
|
324
585
|
"en dash; copy the key from a plain-text field");
|
|
325
586
|
}
|
|
326
587
|
this.apiKey = key;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
588
|
+
const rawBase = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
589
|
+
if (typeof rawBase !== "string")
|
|
590
|
+
throw new Error("baseUrl must be a string");
|
|
591
|
+
const trimmed = rawBase.replace(BASE_URL_ENDS, "");
|
|
592
|
+
if (/[\s\\\u0000-\u001f\u007f]/.test(trimmed)) {
|
|
593
|
+
throw new Error(`baseUrl contains whitespace, a backslash or a control character: ${JSON.stringify(maskBase(trimmed))}`);
|
|
594
|
+
}
|
|
595
|
+
this.base = trimmed.replace(/\/+$/, "");
|
|
596
|
+
const url = parseBase(this.base);
|
|
597
|
+
const parsed = url ?? (pageHref() === undefined ? asPageRelative(this.base) : undefined);
|
|
598
|
+
if (!parsed)
|
|
599
|
+
throw new Error(notAUrl(trimmed));
|
|
600
|
+
this.pageRelative = !url;
|
|
601
|
+
this.userinfo = Boolean(parsed.username || parsed.password);
|
|
602
|
+
this.timeoutMs = checkMs(opts.timeoutMs) ?? 30_000;
|
|
330
603
|
this.model = opts.model ?? DEFAULT_MODEL;
|
|
331
604
|
if (this.model && !MODEL_RE.test(this.model)) {
|
|
332
605
|
throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
|
|
333
606
|
}
|
|
607
|
+
if (opts && "userId" in opts && opts.userId === undefined) {
|
|
608
|
+
throw new Error("new Client() needs a store id in userId, or no userId option at all. It was given " +
|
|
609
|
+
"undefined. That is usually a lookup that found nothing. Leave userId out to use the default store.");
|
|
610
|
+
}
|
|
334
611
|
assertUsableStoreId(opts.userId);
|
|
335
612
|
this.defaultUser = opts.userId ?? DEFAULT_USER;
|
|
336
613
|
const mr = opts.maxRetries ?? opts.retries;
|
|
337
614
|
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;
|
|
615
|
+
this.deadlineMs = checkMs(opts.deadlineMs);
|
|
340
616
|
this.signal = opts.signal;
|
|
341
617
|
const f = opts.fetch;
|
|
342
618
|
if (f !== undefined && typeof f !== "function") {
|
|
343
619
|
throw new Error("fetch must be a function (a fetch-compatible transport)");
|
|
344
620
|
}
|
|
345
|
-
this.
|
|
621
|
+
this.customFetch = f !== undefined && !DEFAULT_TRANSPORTS.has(f);
|
|
622
|
+
this.fetchImpl = (f ?? defaultTransport());
|
|
346
623
|
if (typeof this.fetchImpl !== "function") {
|
|
347
624
|
throw new Error("no fetch available in this runtime — pass one as `fetch` (Node 18+ has it built in; older Node needs undici)");
|
|
348
625
|
}
|
|
@@ -353,14 +630,8 @@ export class Client {
|
|
|
353
630
|
};
|
|
354
631
|
if (this.model)
|
|
355
632
|
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)) {
|
|
633
|
+
const host = url?.hostname.toLowerCase().replace(/^\[|\]$/g, "") ?? "";
|
|
634
|
+
if (url?.protocol === "http:" && !LOOPBACK_HOSTS.has(host)) {
|
|
364
635
|
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
365
636
|
}
|
|
366
637
|
Object.defineProperty(this, "apiKey", { enumerable: false });
|
|
@@ -374,10 +645,10 @@ export class Client {
|
|
|
374
645
|
return new Client({ ...opts, apiKey: key });
|
|
375
646
|
}
|
|
376
647
|
toJSON() {
|
|
377
|
-
return { baseUrl: this.base, model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
|
|
648
|
+
return { baseUrl: maskBase(this.base), model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
|
|
378
649
|
}
|
|
379
650
|
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
380
|
-
return `Client(${this.base}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
|
|
651
|
+
return `Client(${maskBase(this.base)}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
|
|
381
652
|
}
|
|
382
653
|
uid(userId) {
|
|
383
654
|
assertUsableStoreId(userId);
|
|
@@ -426,6 +697,7 @@ export class Client {
|
|
|
426
697
|
const body = { user_id: this.uid(userId), content, metadata };
|
|
427
698
|
if (opts.image !== undefined)
|
|
428
699
|
body.image = normalizeImage(opts.image);
|
|
700
|
+
warnOnUnknownMetadata(metadata);
|
|
429
701
|
return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
|
|
430
702
|
}
|
|
431
703
|
async store(content, userId, metadata = {}, opts = {}) {
|
|
@@ -446,6 +718,7 @@ export class Client {
|
|
|
446
718
|
async search(query, userId, limit = 10, opts = {}) {
|
|
447
719
|
checkSearchOpts(opts);
|
|
448
720
|
warnOnUnknownFilters(opts.filters);
|
|
721
|
+
limit ??= 10;
|
|
449
722
|
checkCount(limit, "limit");
|
|
450
723
|
const { extra, ...known } = opts;
|
|
451
724
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -454,6 +727,7 @@ export class Client {
|
|
|
454
727
|
async searchSelf(query, userId, limit = 10, opts = {}) {
|
|
455
728
|
checkSearchOpts(opts);
|
|
456
729
|
warnOnUnknownFilters(opts.filters);
|
|
730
|
+
limit ??= 10;
|
|
457
731
|
checkCount(limit, "limit");
|
|
458
732
|
const { extra, ...known } = opts;
|
|
459
733
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -462,6 +736,7 @@ export class Client {
|
|
|
462
736
|
async searchFull(query, userId, limit = 10, opts = {}) {
|
|
463
737
|
checkSearchOpts(opts);
|
|
464
738
|
warnOnUnknownFilters(opts.filters);
|
|
739
|
+
limit ??= 10;
|
|
465
740
|
checkCount(limit, "limit");
|
|
466
741
|
const { extra, ...known } = opts;
|
|
467
742
|
const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
|
|
@@ -475,11 +750,11 @@ export class Client {
|
|
|
475
750
|
async recall(query, userId, opts = {}) {
|
|
476
751
|
checkOpts(opts, KNOWN_RECALL_KEYS, "recall");
|
|
477
752
|
const body = { user_id: this.uid(userId), query };
|
|
478
|
-
if (opts.limit
|
|
753
|
+
if (opts.limit != null) {
|
|
479
754
|
checkCount(opts.limit, "limit");
|
|
480
755
|
body.limit = opts.limit;
|
|
481
756
|
}
|
|
482
|
-
if (opts.context_limit
|
|
757
|
+
if (opts.context_limit != null) {
|
|
483
758
|
checkContextLimit(opts.context_limit);
|
|
484
759
|
body.context_limit = opts.context_limit;
|
|
485
760
|
}
|
|
@@ -509,7 +784,8 @@ export class Client {
|
|
|
509
784
|
return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then(memoryFromGet);
|
|
510
785
|
}
|
|
511
786
|
async listMemories(userId, opts = {}) {
|
|
512
|
-
const
|
|
787
|
+
const limit = optRange(opts.limit, "limit", 1, LIST_LIMIT_MAX);
|
|
788
|
+
const body = { user_id: this.uid(userId), limit: limit ?? 100 };
|
|
513
789
|
if (opts.cursor)
|
|
514
790
|
body.cursor = opts.cursor;
|
|
515
791
|
return this.post("/api/v1/memory/list", body);
|
|
@@ -519,16 +795,21 @@ export class Client {
|
|
|
519
795
|
const seen = new Set();
|
|
520
796
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
521
797
|
const p = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
|
|
522
|
-
|
|
798
|
+
const rows = asRecords(p.memories);
|
|
799
|
+
for (const m of rows)
|
|
523
800
|
yield m;
|
|
524
801
|
const next = p.next_cursor ?? undefined;
|
|
525
|
-
if (!next
|
|
802
|
+
if (!next)
|
|
803
|
+
return;
|
|
804
|
+
if (seen.has(next)) {
|
|
805
|
+
if (rows.length)
|
|
806
|
+
throw truncatedWalk(`stopped at page ${page + 1}: the service repeated a cursor, so the store did not end.`);
|
|
526
807
|
return;
|
|
808
|
+
}
|
|
527
809
|
seen.add(next);
|
|
528
810
|
cursor = next;
|
|
529
811
|
}
|
|
530
|
-
throw
|
|
531
|
-
`answer, not the whole store.`);
|
|
812
|
+
throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
|
|
532
813
|
}
|
|
533
814
|
async exportMemories(userId) {
|
|
534
815
|
const out = [];
|
|
@@ -553,14 +834,16 @@ export class Client {
|
|
|
553
834
|
throw new Error("memoryId is required — the id of the memory whose image you want removed.");
|
|
554
835
|
}
|
|
555
836
|
const body = { user_id: this.uid(userId), memory_id: memoryId.trim() };
|
|
556
|
-
if (opts.preview)
|
|
557
|
-
|
|
837
|
+
if (!opts.preview)
|
|
838
|
+
return this.remove("/api/v1/memory/image", body);
|
|
839
|
+
body.preview = true;
|
|
558
840
|
return this.request("DELETE", "/api/v1/memory/image", body);
|
|
559
841
|
}
|
|
560
842
|
async listImages(userId, opts = {}) {
|
|
843
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
561
844
|
const body = { user_id: this.uid(userId) };
|
|
562
|
-
if (
|
|
563
|
-
body.limit =
|
|
845
|
+
if (limit !== undefined)
|
|
846
|
+
body.limit = limit;
|
|
564
847
|
if (opts.before !== undefined)
|
|
565
848
|
body.before = opts.before;
|
|
566
849
|
if (opts.skipIds !== undefined)
|
|
@@ -573,26 +856,30 @@ export class Client {
|
|
|
573
856
|
const seen = new Set();
|
|
574
857
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
575
858
|
const p = await this.listImages(userId, { limit: opts.pageSize, before, skipIds });
|
|
576
|
-
|
|
859
|
+
const rows = asRecords(p.images);
|
|
860
|
+
for (const m of rows)
|
|
577
861
|
yield m;
|
|
578
862
|
if (!p.has_more || !p.next_before)
|
|
579
863
|
return;
|
|
580
864
|
const key = `${p.next_before}|${(Array.isArray(p.next_skip_ids) ? p.next_skip_ids : []).join(",")}`;
|
|
581
|
-
if (seen.has(key))
|
|
865
|
+
if (seen.has(key)) {
|
|
866
|
+
if (rows.length)
|
|
867
|
+
throw truncatedWalk(`stopped at page ${page + 1}: the service repeated a cursor, so the store did not end.`);
|
|
582
868
|
return;
|
|
869
|
+
}
|
|
583
870
|
seen.add(key);
|
|
584
871
|
before = p.next_before;
|
|
585
872
|
skipIds = Array.isArray(p.next_skip_ids) ? p.next_skip_ids : undefined;
|
|
586
873
|
}
|
|
587
|
-
throw
|
|
588
|
-
`answer, not the whole store.`);
|
|
874
|
+
throw truncatedWalk(`stopped after ${MAX_PAGES} pages — the store did not end.`);
|
|
589
875
|
}
|
|
590
876
|
async revisions(userId, opts = {}) {
|
|
877
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
591
878
|
const body = {};
|
|
592
879
|
if (opts.include !== undefined)
|
|
593
880
|
body.include = opts.include;
|
|
594
|
-
if (
|
|
595
|
-
body.limit =
|
|
881
|
+
if (limit !== undefined)
|
|
882
|
+
body.limit = limit;
|
|
596
883
|
if (opts.before !== undefined)
|
|
597
884
|
body.before = opts.before;
|
|
598
885
|
if (opts.skipIds !== undefined)
|
|
@@ -610,9 +897,10 @@ export class Client {
|
|
|
610
897
|
if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
|
|
611
898
|
throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
|
|
612
899
|
}
|
|
900
|
+
const limit = optRange(opts.limit, "limit", PAGE_LIMIT_MIN, PAGE_LIMIT_MAX);
|
|
613
901
|
const body = { user_id: this.uid(userId), speaker: speaker.trim() };
|
|
614
|
-
if (
|
|
615
|
-
body.limit =
|
|
902
|
+
if (limit !== undefined)
|
|
903
|
+
body.limit = limit;
|
|
616
904
|
if (opts.before !== undefined)
|
|
617
905
|
body.before = opts.before;
|
|
618
906
|
if (opts.skipIds !== undefined)
|
|
@@ -647,7 +935,7 @@ export class Client {
|
|
|
647
935
|
if (typeof userId !== "string" || !userId.trim())
|
|
648
936
|
throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
|
|
649
937
|
warnIfStoreIdCollapses(userId);
|
|
650
|
-
return this.
|
|
938
|
+
return this.remove("/api/v1/memory/collection", { user_id: userId });
|
|
651
939
|
}
|
|
652
940
|
async addSpeaker(speaker, userId) {
|
|
653
941
|
return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
@@ -657,7 +945,7 @@ export class Client {
|
|
|
657
945
|
return { ...r, speakers: asRecords(r?.speakers) };
|
|
658
946
|
}
|
|
659
947
|
async removeSpeaker(speaker, userId) {
|
|
660
|
-
return this.
|
|
948
|
+
return this.remove("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
661
949
|
}
|
|
662
950
|
async delete(userId, memoryId) {
|
|
663
951
|
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
@@ -674,22 +962,26 @@ export class Client {
|
|
|
674
962
|
post(path, body, idempotencyKey) {
|
|
675
963
|
return this.request("POST", path, body, idempotencyKey);
|
|
676
964
|
}
|
|
965
|
+
remove(path, body) {
|
|
966
|
+
return this.request("DELETE", path, body, undefined, true);
|
|
967
|
+
}
|
|
677
968
|
deadlineAt() {
|
|
678
969
|
return this.deadlineMs === undefined ? undefined : Date.now() + this.deadlineMs;
|
|
679
970
|
}
|
|
680
|
-
beginAttempt(deadlineAt) {
|
|
971
|
+
beginAttempt(deadlineAt, answer) {
|
|
681
972
|
let why = "timeout";
|
|
682
973
|
let budget = this.timeoutMs;
|
|
683
974
|
if (deadlineAt !== undefined) {
|
|
684
975
|
const left = deadlineAt - Date.now();
|
|
685
976
|
if (left <= 0)
|
|
686
|
-
throw new APIConnectionError(0, this.abortMessage("deadline"));
|
|
977
|
+
throw answer ?? new APIConnectionError(0, this.abortMessage("deadline"));
|
|
687
978
|
if (left < budget) {
|
|
688
979
|
budget = left;
|
|
689
980
|
why = "deadline";
|
|
690
981
|
}
|
|
691
982
|
}
|
|
692
983
|
const ctrl = new AbortController();
|
|
984
|
+
const endsAt = Date.now() + budget;
|
|
693
985
|
const timer = setTimeout(() => ctrl.abort(), budget);
|
|
694
986
|
const outer = this.signal;
|
|
695
987
|
const onCaller = () => {
|
|
@@ -709,6 +1001,7 @@ export class Client {
|
|
|
709
1001
|
outer?.removeEventListener("abort", onCaller);
|
|
710
1002
|
},
|
|
711
1003
|
why: () => why,
|
|
1004
|
+
endsAt,
|
|
712
1005
|
};
|
|
713
1006
|
}
|
|
714
1007
|
abortMessage(why) {
|
|
@@ -718,10 +1011,10 @@ export class Client {
|
|
|
718
1011
|
return `deadline of ${this.deadlineMs}ms exhausted`;
|
|
719
1012
|
return `request timed out after ${this.timeoutMs}ms`;
|
|
720
1013
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1014
|
+
fits(ms, deadlineAt) {
|
|
1015
|
+
return deadlineAt === undefined || ms <= deadlineAt - Date.now();
|
|
1016
|
+
}
|
|
1017
|
+
async pause(ms) {
|
|
725
1018
|
const outer = this.signal;
|
|
726
1019
|
if (!outer) {
|
|
727
1020
|
await new Promise((r) => setTimeout(r, ms));
|
|
@@ -739,17 +1032,83 @@ export class Client {
|
|
|
739
1032
|
outer.addEventListener("abort", done);
|
|
740
1033
|
});
|
|
741
1034
|
}
|
|
742
|
-
|
|
1035
|
+
networkError(e, path) {
|
|
1036
|
+
let text = typeof e?.message === "string" && e.message ? e.message : String(e);
|
|
1037
|
+
const q = path.indexOf("?");
|
|
1038
|
+
if (q !== -1) {
|
|
1039
|
+
const query = path.slice(q);
|
|
1040
|
+
text = text.split(query).join("");
|
|
1041
|
+
try {
|
|
1042
|
+
text = text.split(decodeURIComponent(query)).join("");
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
text = cleanServerText(maskUserinfo(text), 512);
|
|
1048
|
+
const code = causeCode(e);
|
|
1049
|
+
if (code && !text.includes(code))
|
|
1050
|
+
text += ` (${code})`;
|
|
1051
|
+
const quotable = this.userinfo || q !== -1;
|
|
1052
|
+
return new APIConnectionError(0, `network error: ${text}`, undefined, quotable ? undefined : { cause: e });
|
|
1053
|
+
}
|
|
1054
|
+
isRedirected(res) {
|
|
1055
|
+
return (res.status >= 300 && res.status < 400) || res.type === "opaqueredirect" || res.redirected === true;
|
|
1056
|
+
}
|
|
1057
|
+
errorFromAnswer(res, body, retryAfter, mayBeDeleted = false, unread) {
|
|
1058
|
+
const e = parseErrorText(new TextDecoder().decode(body));
|
|
1059
|
+
let msg = cleanServerText(e.message) || `HTTP ${res.status}`;
|
|
1060
|
+
if (unread)
|
|
1061
|
+
msg += ` (the error body could not be read: ${cleanServerText(unread, 512)})`;
|
|
1062
|
+
const requestId = e.requestId === undefined ? undefined : cleanServerText(e.requestId, 256) || undefined;
|
|
1063
|
+
const err = errorFor(res.status, msg, requestId, {
|
|
1064
|
+
type: e.type === undefined ? undefined : cleanServerText(e.type, 256) || undefined,
|
|
1065
|
+
details: cleanDetails(e.details),
|
|
1066
|
+
retryAfter: retryAfter === undefined ? undefined : Math.ceil(retryAfter / 1000),
|
|
1067
|
+
});
|
|
1068
|
+
if (mayBeDeleted && err instanceof NotFoundError) {
|
|
1069
|
+
err.message += " (an earlier attempt may already have deleted it)";
|
|
1070
|
+
}
|
|
1071
|
+
return err;
|
|
1072
|
+
}
|
|
1073
|
+
async send(method, path, payload, extraHeaders, removes = false, reads = false) {
|
|
1074
|
+
if (this.pageRelative && !this.customFetch && !parseBase(this.base)) {
|
|
1075
|
+
throw new Error(`${notAUrl(this.base)}. A relative baseUrl needs a page to resolve against, and this runtime has none.`);
|
|
1076
|
+
}
|
|
743
1077
|
const attempts = this.maxRetries + 1;
|
|
1078
|
+
const upper = method.toUpperCase();
|
|
1079
|
+
const idempotent = IDEMPOTENT_METHODS.has(upper);
|
|
1080
|
+
const logPath = path.split("?")[0];
|
|
744
1081
|
const deadlineAt = this.deadlineAt();
|
|
1082
|
+
const headers = extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers;
|
|
1083
|
+
let ambiguous = false;
|
|
1084
|
+
let refused;
|
|
745
1085
|
for (let attempt = 0;; attempt++) {
|
|
746
|
-
const
|
|
1086
|
+
const last = attempt + 1 >= attempts;
|
|
1087
|
+
const tag = `(attempt ${attempt + 1}/${attempts})`;
|
|
1088
|
+
const start = Date.now();
|
|
1089
|
+
const previous = refused;
|
|
1090
|
+
refused = undefined;
|
|
1091
|
+
const att = this.beginAttempt(deadlineAt, previous);
|
|
1092
|
+
const pauseWithoutAnswer = async (why) => {
|
|
1093
|
+
const delay = this.backoffMs(attempt);
|
|
1094
|
+
if (!this.fits(delay, deadlineAt))
|
|
1095
|
+
throw previous ?? new APIConnectionError(0, this.abortMessage("deadline"));
|
|
1096
|
+
logDebug(`${method} ${logPath}: ${why}. Retrying in ${Math.round(delay)}ms ${tag}`);
|
|
1097
|
+
await this.pause(delay);
|
|
1098
|
+
};
|
|
1099
|
+
const cutShort = () => {
|
|
1100
|
+
const why = att.why();
|
|
1101
|
+
if (why === "deadline" && (idempotent || reads || (upper === "POST" && READ_POSTS.has(logPath))) && previous) {
|
|
1102
|
+
return previous;
|
|
1103
|
+
}
|
|
1104
|
+
return new APIConnectionError(0, this.abortMessage(why));
|
|
1105
|
+
};
|
|
747
1106
|
let res;
|
|
748
1107
|
try {
|
|
749
1108
|
res = await this.fetchImpl(`${this.base}${path}`, {
|
|
750
|
-
method
|
|
751
|
-
headers
|
|
752
|
-
body:
|
|
1109
|
+
method,
|
|
1110
|
+
headers,
|
|
1111
|
+
body: payload,
|
|
753
1112
|
signal: att.signal,
|
|
754
1113
|
redirect: "manual",
|
|
755
1114
|
});
|
|
@@ -757,63 +1116,109 @@ export class Client {
|
|
|
757
1116
|
catch (e) {
|
|
758
1117
|
att.clear();
|
|
759
1118
|
const timedOut = e?.name === "AbortError" || att.signal.aborted;
|
|
760
|
-
if (
|
|
761
|
-
|
|
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");
|
|
762
1126
|
continue;
|
|
763
1127
|
}
|
|
764
|
-
throw
|
|
1128
|
+
throw this.networkError(e, path);
|
|
765
1129
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
void res.body?.cancel();
|
|
770
|
-
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);
|
|
771
1133
|
}
|
|
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}`);
|
|
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);
|
|
794
1142
|
}
|
|
795
|
-
|
|
1143
|
+
};
|
|
1144
|
+
const readErrorBody = async (withinMs) => {
|
|
796
1145
|
try {
|
|
797
|
-
|
|
1146
|
+
return { body: await this.readCappedBytes(res, withinMs) };
|
|
798
1147
|
}
|
|
799
1148
|
catch (e) {
|
|
800
|
-
|
|
801
|
-
|
|
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
|
+
}
|
|
802
1179
|
}
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
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);
|
|
806
1199
|
}
|
|
807
|
-
|
|
808
|
-
|
|
1200
|
+
const body = await readBody();
|
|
1201
|
+
if (body === null) {
|
|
1202
|
+
ambiguous = true;
|
|
1203
|
+
att.clear();
|
|
1204
|
+
await pauseWithoutAnswer("body dropped mid-stream");
|
|
1205
|
+
continue;
|
|
809
1206
|
}
|
|
810
|
-
|
|
1207
|
+
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms ${tag}`);
|
|
1208
|
+
return { res, body };
|
|
811
1209
|
}
|
|
812
1210
|
finally {
|
|
813
1211
|
att.clear();
|
|
814
1212
|
}
|
|
815
1213
|
}
|
|
816
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
|
+
}
|
|
817
1222
|
idemHeader(key) {
|
|
818
1223
|
if (key === undefined || key === null)
|
|
819
1224
|
return undefined;
|
|
@@ -823,58 +1228,72 @@ export class Client {
|
|
|
823
1228
|
return { "Idempotency-Key": key };
|
|
824
1229
|
}
|
|
825
1230
|
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
|
-
}
|
|
1231
|
+
const ra = retryAfterMs(retryAfter ?? null);
|
|
1232
|
+
if (ra !== undefined)
|
|
1233
|
+
return ra;
|
|
834
1234
|
return Math.min(8_000, 500 * 2 ** attempt) + Math.random() * 250;
|
|
835
1235
|
}
|
|
836
|
-
async
|
|
837
|
-
return new TextDecoder().decode(await this.readCappedBytes(res));
|
|
838
|
-
}
|
|
839
|
-
async readCappedBytes(res) {
|
|
1236
|
+
async readCappedBytes(res, withinMs) {
|
|
840
1237
|
const cl = res.headers.get("content-length");
|
|
841
1238
|
if (cl && Number(cl) > MAX_RESPONSE_BYTES) {
|
|
1239
|
+
discard(res);
|
|
842
1240
|
throw new WosError(res.status, `response too large (${cl} bytes) — refusing to buffer it`);
|
|
843
1241
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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;
|
|
848
1285
|
}
|
|
849
1286
|
return buf;
|
|
850
1287
|
}
|
|
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);
|
|
1288
|
+
catch (e) {
|
|
1289
|
+
throw late && !(e instanceof SlowBody) ? slow() : e;
|
|
864
1290
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
for (const p of parts) {
|
|
868
|
-
buf.set(p, off);
|
|
869
|
-
off += p.byteLength;
|
|
1291
|
+
finally {
|
|
1292
|
+
clearTimeout(timer);
|
|
870
1293
|
}
|
|
871
|
-
return buf;
|
|
872
1294
|
}
|
|
873
|
-
async request(method, path, body, idempotencyKey) {
|
|
1295
|
+
async request(method, path, body, idempotencyKey, removes = false) {
|
|
874
1296
|
const extraHeaders = this.idemHeader(idempotencyKey);
|
|
875
|
-
const attempts = this.maxRetries + 1;
|
|
876
|
-
const logPath = path.split("?")[0];
|
|
877
|
-
const deadlineAt = this.deadlineAt();
|
|
878
1297
|
let payload;
|
|
879
1298
|
try {
|
|
880
1299
|
payload = body === undefined ? undefined : JSON.stringify(body);
|
|
@@ -883,117 +1302,28 @@ export class Client {
|
|
|
883
1302
|
throw new Error(`request body could not be serialized: ${e instanceof Error ? e.message : String(e)}. ` +
|
|
884
1303
|
"Pass plain JSON values — an ORM entity, a BigInt or a circular reference cannot be sent.");
|
|
885
1304
|
}
|
|
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;
|
|
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;
|
|
995
1325
|
}
|
|
996
|
-
|
|
1326
|
+
return data;
|
|
997
1327
|
}
|
|
998
1328
|
}
|
|
999
1329
|
export const WME = Client;
|