wontopos 2.2.40 → 2.2.41

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