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