wontopos 2.2.35 → 2.2.37
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 +36 -4
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/wontopos.js +900 -0
- package/dist/wontopos.d.ts +102 -0
- package/dist/wontopos.js +142 -663
- package/package.json +5 -4
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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
|
+
exports._resetStoreIdWarnings = _resetStoreIdWarnings;
|
|
5
|
+
exports._resetFilterWarnings = _resetFilterWarnings;
|
|
6
|
+
const VERSION = "2.2.37";
|
|
7
|
+
const RUNTIME = (() => {
|
|
8
|
+
const p = globalThis?.process;
|
|
9
|
+
return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
|
|
10
|
+
})();
|
|
11
|
+
const USER_AGENT = `wontopos-node/${VERSION}${RUNTIME}`;
|
|
12
|
+
function logDebug(msg) {
|
|
13
|
+
const env = globalThis?.process?.env;
|
|
14
|
+
if (typeof env?.WONTOPOS_LOG === "string" && env.WONTOPOS_LOG.toLowerCase() === "debug") {
|
|
15
|
+
console.error(`wontopos: ${msg}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const DEFAULT_BASE_URL = "https://api.wontopos.com";
|
|
19
|
+
const DEFAULT_MODEL = "tablet-2";
|
|
20
|
+
const RETRY_ALWAYS = new Set([429]);
|
|
21
|
+
const RETRY_IF_IDEMPOTENT = new Set([502, 503]);
|
|
22
|
+
const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
23
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
24
|
+
const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
25
|
+
const MODEL_RE = /^[A-Za-z0-9._-]+$/;
|
|
26
|
+
const MAX_ERR_MSG = 4096;
|
|
27
|
+
const IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9._:\-]{1,128}$/;
|
|
28
|
+
function normalizeStoreId(id) {
|
|
29
|
+
return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
30
|
+
}
|
|
31
|
+
const WARNED_STORE_IDS_MAX = 1024;
|
|
32
|
+
const WARNED_FILTER_KEYS_MAX = 1024;
|
|
33
|
+
const warnedStoreIds = new Set();
|
|
34
|
+
function warnIfStoreIdCollapses(id) {
|
|
35
|
+
if (!id || warnedStoreIds.has(id))
|
|
36
|
+
return;
|
|
37
|
+
const normalized = normalizeStoreId(id);
|
|
38
|
+
if (normalized === id)
|
|
39
|
+
return;
|
|
40
|
+
warnedStoreIds.add(id);
|
|
41
|
+
while (warnedStoreIds.size > WARNED_STORE_IDS_MAX) {
|
|
42
|
+
const oldest = warnedStoreIds.values().next().value;
|
|
43
|
+
if (oldest === undefined)
|
|
44
|
+
break;
|
|
45
|
+
warnedStoreIds.delete(oldest);
|
|
46
|
+
}
|
|
47
|
+
console.warn(`wontopos: store id ${JSON.stringify(id)} is stored as ${JSON.stringify(normalized)} ` +
|
|
48
|
+
`(lowercased, and anything outside [a-z0-9_] becomes "_"). Ids that differ only by case or ` +
|
|
49
|
+
`punctuation share ONE store and therefore one set of memories — if these ids come from your ` +
|
|
50
|
+
`end users, normalize them yourself first so two people can never collide.`);
|
|
51
|
+
}
|
|
52
|
+
function _resetStoreIdWarnings() {
|
|
53
|
+
warnedStoreIds.clear();
|
|
54
|
+
}
|
|
55
|
+
const NO_BODY_STATUS = new Set([204, 205, 304]);
|
|
56
|
+
const MAX_PAGES = 20_000;
|
|
57
|
+
exports.SEARCH_LIMIT_MIN = 5;
|
|
58
|
+
exports.SEARCH_LIMIT_MAX = 20;
|
|
59
|
+
exports.CONTEXT_LIMIT_MIN = 0;
|
|
60
|
+
exports.CONTEXT_LIMIT_MAX = 20;
|
|
61
|
+
function checkCount(limit, name) {
|
|
62
|
+
if (!Number.isInteger(limit) || limit < exports.SEARCH_LIMIT_MIN || limit > exports.SEARCH_LIMIT_MAX) {
|
|
63
|
+
throw new Error(`${name} must be an integer between ${exports.SEARCH_LIMIT_MIN} and ${exports.SEARCH_LIMIT_MAX}, got ${limit}. ` +
|
|
64
|
+
`Out of range is refused rather than adjusted, so a short answer always means ` +
|
|
65
|
+
`the store was short.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function checkContextLimit(n) {
|
|
69
|
+
if (!Number.isInteger(n) || n < exports.CONTEXT_LIMIT_MIN || n > exports.CONTEXT_LIMIT_MAX) {
|
|
70
|
+
throw new Error(`context_limit must be an integer between ${exports.CONTEXT_LIMIT_MIN} and ${exports.CONTEXT_LIMIT_MAX}, got ${n}.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function normalizeImage(image) {
|
|
74
|
+
if (!image || typeof image !== "object") {
|
|
75
|
+
throw new Error("image must be an object — { data: '<base64>' }.");
|
|
76
|
+
}
|
|
77
|
+
if (typeof image.data !== "string" || image.data.trim() === "") {
|
|
78
|
+
throw new Error("image.data is required — base64 of the image (a data: URL is fine).");
|
|
79
|
+
}
|
|
80
|
+
const raw = image.data.trim();
|
|
81
|
+
const comma = raw.startsWith("data:") ? raw.indexOf(",") : -1;
|
|
82
|
+
const data = (comma === -1 ? raw : raw.slice(comma + 1)).replace(/\s+/g, "");
|
|
83
|
+
if (data === "")
|
|
84
|
+
throw new Error("image.data is empty after stripping its data: prefix.");
|
|
85
|
+
const out = { data };
|
|
86
|
+
if (image.reference !== undefined)
|
|
87
|
+
out.reference = image.reference;
|
|
88
|
+
if (image.taken_at !== undefined)
|
|
89
|
+
out.taken_at = image.taken_at;
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
const CONNECT_FAIL_CODES = new Set([
|
|
93
|
+
"ECONNREFUSED",
|
|
94
|
+
"ENOTFOUND",
|
|
95
|
+
"EAI_AGAIN",
|
|
96
|
+
"EHOSTUNREACH",
|
|
97
|
+
"ENETUNREACH",
|
|
98
|
+
"ENETDOWN",
|
|
99
|
+
"EADDRNOTAVAIL",
|
|
100
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
101
|
+
]);
|
|
102
|
+
function isConnectFailure(e) {
|
|
103
|
+
const stack = [e];
|
|
104
|
+
for (let steps = 0; stack.length && steps < 24; steps++) {
|
|
105
|
+
const cur = stack.pop();
|
|
106
|
+
if (!cur || typeof cur !== "object")
|
|
107
|
+
continue;
|
|
108
|
+
if (typeof cur.code === "string" && CONNECT_FAIL_CODES.has(cur.code))
|
|
109
|
+
return true;
|
|
110
|
+
if (cur.cause)
|
|
111
|
+
stack.push(cur.cause);
|
|
112
|
+
if (Array.isArray(cur.errors))
|
|
113
|
+
stack.push(...cur.errors.slice(0, 8));
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function maskKey(key) {
|
|
118
|
+
return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
|
|
119
|
+
}
|
|
120
|
+
function parseRateLimit(h) {
|
|
121
|
+
const num = (name) => {
|
|
122
|
+
const v = h.get(name);
|
|
123
|
+
if (v == null || v.trim() === "")
|
|
124
|
+
return null;
|
|
125
|
+
const n = Number(v);
|
|
126
|
+
return Number.isFinite(n) ? n : null;
|
|
127
|
+
};
|
|
128
|
+
const limit = num("X-RateLimit-Limit");
|
|
129
|
+
const remaining = num("X-RateLimit-Remaining");
|
|
130
|
+
const reset = num("X-RateLimit-Reset");
|
|
131
|
+
if (limit === null && remaining === null && reset === null)
|
|
132
|
+
return null;
|
|
133
|
+
return { limit, remaining, reset };
|
|
134
|
+
}
|
|
135
|
+
const asList = (x) => (Array.isArray(x) ? x : []);
|
|
136
|
+
const asObj = (x) => (x != null && typeof x === "object" && !Array.isArray(x) ? x : {});
|
|
137
|
+
const asRecords = (x) => asList(x).filter((m) => m != null && typeof m === "object" && !Array.isArray(m));
|
|
138
|
+
function mergeResults(r) {
|
|
139
|
+
const main = asRecords(r?.memories);
|
|
140
|
+
const mine = asRecords(r?.self_memories);
|
|
141
|
+
if (mine.length === 0)
|
|
142
|
+
return main;
|
|
143
|
+
const seen = new Set(main.map((m) => m.id).filter((id) => typeof id === "string"));
|
|
144
|
+
return main.concat(mine.filter((m) => !(typeof m.id === "string" && seen.has(m.id))));
|
|
145
|
+
}
|
|
146
|
+
function withForm(body, form, tz) {
|
|
147
|
+
if (form !== undefined)
|
|
148
|
+
body.form = form;
|
|
149
|
+
if (tz !== undefined)
|
|
150
|
+
body.tz = tz;
|
|
151
|
+
return body;
|
|
152
|
+
}
|
|
153
|
+
class WosError extends Error {
|
|
154
|
+
status;
|
|
155
|
+
requestId;
|
|
156
|
+
constructor(status, message, requestId) {
|
|
157
|
+
super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`);
|
|
158
|
+
this.name = new.target.name;
|
|
159
|
+
this.status = status;
|
|
160
|
+
this.requestId = requestId;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.WosError = WosError;
|
|
164
|
+
class APIConnectionError extends WosError {
|
|
165
|
+
}
|
|
166
|
+
exports.APIConnectionError = APIConnectionError;
|
|
167
|
+
class BadRequestError extends WosError {
|
|
168
|
+
}
|
|
169
|
+
exports.BadRequestError = BadRequestError;
|
|
170
|
+
class AuthenticationError extends WosError {
|
|
171
|
+
}
|
|
172
|
+
exports.AuthenticationError = AuthenticationError;
|
|
173
|
+
class PaymentRequiredError extends WosError {
|
|
174
|
+
}
|
|
175
|
+
exports.PaymentRequiredError = PaymentRequiredError;
|
|
176
|
+
class PermissionDeniedError extends WosError {
|
|
177
|
+
}
|
|
178
|
+
exports.PermissionDeniedError = PermissionDeniedError;
|
|
179
|
+
class NotFoundError extends WosError {
|
|
180
|
+
}
|
|
181
|
+
exports.NotFoundError = NotFoundError;
|
|
182
|
+
class ConflictError extends WosError {
|
|
183
|
+
}
|
|
184
|
+
exports.ConflictError = ConflictError;
|
|
185
|
+
class RateLimitError extends WosError {
|
|
186
|
+
}
|
|
187
|
+
exports.RateLimitError = RateLimitError;
|
|
188
|
+
class ServerError extends WosError {
|
|
189
|
+
}
|
|
190
|
+
exports.ServerError = ServerError;
|
|
191
|
+
const STATUS_ERRORS = {
|
|
192
|
+
400: BadRequestError,
|
|
193
|
+
401: AuthenticationError,
|
|
194
|
+
402: PaymentRequiredError,
|
|
195
|
+
403: PermissionDeniedError,
|
|
196
|
+
404: NotFoundError,
|
|
197
|
+
409: ConflictError,
|
|
198
|
+
429: RateLimitError,
|
|
199
|
+
};
|
|
200
|
+
function errorFor(status, message, requestId) {
|
|
201
|
+
const Cls = status === 0
|
|
202
|
+
? APIConnectionError
|
|
203
|
+
: STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
|
|
204
|
+
return new Cls(status, message, requestId);
|
|
205
|
+
}
|
|
206
|
+
const KNOWN_FILTER_KEYS = new Set([
|
|
207
|
+
"categories",
|
|
208
|
+
"event_from",
|
|
209
|
+
"event_to",
|
|
210
|
+
"time_from",
|
|
211
|
+
"time_to",
|
|
212
|
+
"min_importance",
|
|
213
|
+
]);
|
|
214
|
+
const warnedFilterKeys = new Set();
|
|
215
|
+
function warnOnUnknownFilters(filters) {
|
|
216
|
+
if (!filters || typeof filters !== "object" || Array.isArray(filters))
|
|
217
|
+
return;
|
|
218
|
+
for (const k of Object.keys(filters)) {
|
|
219
|
+
if (KNOWN_FILTER_KEYS.has(k) || warnedFilterKeys.has(k))
|
|
220
|
+
continue;
|
|
221
|
+
warnedFilterKeys.add(k);
|
|
222
|
+
while (warnedFilterKeys.size > WARNED_FILTER_KEYS_MAX) {
|
|
223
|
+
const oldest = warnedFilterKeys.values().next().value;
|
|
224
|
+
if (oldest === undefined)
|
|
225
|
+
break;
|
|
226
|
+
warnedFilterKeys.delete(oldest);
|
|
227
|
+
}
|
|
228
|
+
console.warn(`wontopos: unknown search filter ${JSON.stringify(k)} — the API drops keys it does not know, ` +
|
|
229
|
+
`so this filter has NO effect and the search is wider than you think. ` +
|
|
230
|
+
`Known keys: ${[...KNOWN_FILTER_KEYS].join(", ")}.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function _resetFilterWarnings() {
|
|
234
|
+
warnedFilterKeys.clear();
|
|
235
|
+
}
|
|
236
|
+
const DEFAULT_USER = "default";
|
|
237
|
+
class Client {
|
|
238
|
+
base;
|
|
239
|
+
headers;
|
|
240
|
+
timeoutMs;
|
|
241
|
+
apiKey;
|
|
242
|
+
model;
|
|
243
|
+
maxRetries;
|
|
244
|
+
deadlineMs;
|
|
245
|
+
signal;
|
|
246
|
+
fetchImpl;
|
|
247
|
+
defaultUser;
|
|
248
|
+
_rateLimit = null;
|
|
249
|
+
get rateLimit() {
|
|
250
|
+
return this._rateLimit;
|
|
251
|
+
}
|
|
252
|
+
constructor(opts) {
|
|
253
|
+
const key = (opts?.apiKey ?? "").trim();
|
|
254
|
+
if (!key)
|
|
255
|
+
throw new Error("apiKey is required");
|
|
256
|
+
if (/\s/.test(key))
|
|
257
|
+
throw new Error("apiKey contains whitespace - check for a stray newline or paste error");
|
|
258
|
+
if (/[\x00-\x1f\x7f]/.test(key))
|
|
259
|
+
throw new Error("apiKey contains a control character - check for a stray byte or paste error");
|
|
260
|
+
this.apiKey = key;
|
|
261
|
+
this.base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
262
|
+
const tm = opts.timeoutMs;
|
|
263
|
+
this.timeoutMs = typeof tm === "number" && Number.isFinite(tm) && tm > 0 ? tm : 30_000;
|
|
264
|
+
this.model = opts.model ?? DEFAULT_MODEL;
|
|
265
|
+
if (this.model && !MODEL_RE.test(this.model)) {
|
|
266
|
+
throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
|
|
267
|
+
}
|
|
268
|
+
this.defaultUser = opts.userId || DEFAULT_USER;
|
|
269
|
+
const mr = opts.maxRetries ?? opts.retries;
|
|
270
|
+
this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
|
|
271
|
+
const dl = opts.deadlineMs;
|
|
272
|
+
this.deadlineMs = typeof dl === "number" && Number.isFinite(dl) && dl > 0 ? dl : undefined;
|
|
273
|
+
this.signal = opts.signal;
|
|
274
|
+
const f = opts.fetch;
|
|
275
|
+
if (f !== undefined && typeof f !== "function") {
|
|
276
|
+
throw new Error("fetch must be a function (a fetch-compatible transport)");
|
|
277
|
+
}
|
|
278
|
+
this.fetchImpl = f ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined);
|
|
279
|
+
if (typeof this.fetchImpl !== "function") {
|
|
280
|
+
throw new Error("no fetch available in this runtime — pass one as `fetch` (Node 18+ has it built in; older Node needs undici)");
|
|
281
|
+
}
|
|
282
|
+
this.headers = {
|
|
283
|
+
"X-API-Key": this.apiKey,
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
"User-Agent": USER_AGENT,
|
|
286
|
+
};
|
|
287
|
+
if (this.model)
|
|
288
|
+
this.headers["X-WOS-Model"] = this.model;
|
|
289
|
+
let host;
|
|
290
|
+
try {
|
|
291
|
+
host = new URL(this.base).hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
host = "";
|
|
295
|
+
}
|
|
296
|
+
if (/^http:\/\//i.test(this.base) && !LOOPBACK_HOSTS.has(host)) {
|
|
297
|
+
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
298
|
+
}
|
|
299
|
+
Object.defineProperty(this, "apiKey", { enumerable: false });
|
|
300
|
+
Object.defineProperty(this, "headers", { enumerable: false });
|
|
301
|
+
}
|
|
302
|
+
static fromEnv(opts = {}) {
|
|
303
|
+
const env = globalThis?.process?.env;
|
|
304
|
+
const key = [env?.WONTOPOS_API_KEY, env?.WOS_API_KEY].find((v) => v && v.trim());
|
|
305
|
+
if (!key)
|
|
306
|
+
throw new Error("set WONTOPOS_API_KEY (or WOS_API_KEY) in the environment");
|
|
307
|
+
return new Client({ ...opts, apiKey: key });
|
|
308
|
+
}
|
|
309
|
+
toJSON() {
|
|
310
|
+
return { baseUrl: this.base, model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
|
|
311
|
+
}
|
|
312
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
313
|
+
return `Client(${this.base}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
|
|
314
|
+
}
|
|
315
|
+
uid(userId) {
|
|
316
|
+
if (userId !== undefined && (typeof userId !== "string" || !userId.trim())) {
|
|
317
|
+
throw new Error(`userId must be a non-blank string; got ${JSON.stringify(userId)}. Omit it to use the client's ` +
|
|
318
|
+
"default store, or pass a real store id — anything else would silently write into the default store.");
|
|
319
|
+
}
|
|
320
|
+
const id = userId || this.defaultUser;
|
|
321
|
+
warnIfStoreIdCollapses(id);
|
|
322
|
+
return id;
|
|
323
|
+
}
|
|
324
|
+
clone(overrides) {
|
|
325
|
+
return new Client({
|
|
326
|
+
apiKey: this.apiKey,
|
|
327
|
+
baseUrl: this.base,
|
|
328
|
+
timeoutMs: this.timeoutMs,
|
|
329
|
+
model: this.model,
|
|
330
|
+
userId: this.defaultUser,
|
|
331
|
+
maxRetries: this.maxRetries,
|
|
332
|
+
fetch: this.fetchImpl,
|
|
333
|
+
deadlineMs: this.deadlineMs,
|
|
334
|
+
signal: this.signal,
|
|
335
|
+
...overrides,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
withModel(model) {
|
|
339
|
+
return this.clone({ model });
|
|
340
|
+
}
|
|
341
|
+
withUser(userId) {
|
|
342
|
+
return this.clone({ userId });
|
|
343
|
+
}
|
|
344
|
+
withSignal(signal) {
|
|
345
|
+
return this.clone({ signal });
|
|
346
|
+
}
|
|
347
|
+
withDeadline(deadlineMs) {
|
|
348
|
+
return this.clone({ deadlineMs });
|
|
349
|
+
}
|
|
350
|
+
withTimeout(timeoutMs) {
|
|
351
|
+
return this.clone({ timeoutMs });
|
|
352
|
+
}
|
|
353
|
+
withRetries(maxRetries) {
|
|
354
|
+
return this.clone({ maxRetries });
|
|
355
|
+
}
|
|
356
|
+
add(content, userId, metadata = {}, opts = {}) {
|
|
357
|
+
const body = { user_id: this.uid(userId), content, metadata };
|
|
358
|
+
if (opts.image !== undefined)
|
|
359
|
+
body.image = normalizeImage(opts.image);
|
|
360
|
+
return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
|
|
361
|
+
}
|
|
362
|
+
store(content, userId, metadata = {}, opts = {}) {
|
|
363
|
+
return this.add(content, userId, metadata, opts);
|
|
364
|
+
}
|
|
365
|
+
addTurn(userMsg, assistantMsg, userId, opts = {}) {
|
|
366
|
+
return this.post("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, opts.idempotencyKey);
|
|
367
|
+
}
|
|
368
|
+
addBulk(content, userId, category = "general", timestamp, opts = {}) {
|
|
369
|
+
const body = { user_id: this.uid(userId), content, category };
|
|
370
|
+
if (timestamp)
|
|
371
|
+
body.timestamp = timestamp;
|
|
372
|
+
return this.post("/api/v1/memory/bulk-store", body, opts.idempotencyKey);
|
|
373
|
+
}
|
|
374
|
+
update(oldMemoryId, newContent, userId, opts = {}) {
|
|
375
|
+
return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, opts.idempotencyKey);
|
|
376
|
+
}
|
|
377
|
+
async search(query, userId, limit = 10, opts = {}) {
|
|
378
|
+
warnOnUnknownFilters(opts.filters);
|
|
379
|
+
checkCount(limit, "limit");
|
|
380
|
+
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
381
|
+
return mergeResults(r);
|
|
382
|
+
}
|
|
383
|
+
async searchSelf(query, userId, limit = 10, opts = {}) {
|
|
384
|
+
warnOnUnknownFilters(opts.filters);
|
|
385
|
+
checkCount(limit, "limit");
|
|
386
|
+
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
387
|
+
return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
|
|
388
|
+
}
|
|
389
|
+
async searchFull(query, userId, limit = 10, opts = {}) {
|
|
390
|
+
warnOnUnknownFilters(opts.filters);
|
|
391
|
+
checkCount(limit, "limit");
|
|
392
|
+
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
393
|
+
return {
|
|
394
|
+
...r,
|
|
395
|
+
memories: asRecords(r.memories),
|
|
396
|
+
self_memories: asRecords(r.self_memories),
|
|
397
|
+
images: asRecords(r.images),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async recall(query, userId, opts = {}) {
|
|
401
|
+
const body = { user_id: this.uid(userId), query };
|
|
402
|
+
if (opts.limit !== undefined) {
|
|
403
|
+
checkCount(opts.limit, "limit");
|
|
404
|
+
body.limit = opts.limit;
|
|
405
|
+
}
|
|
406
|
+
if (opts.context_limit !== undefined) {
|
|
407
|
+
checkContextLimit(opts.context_limit);
|
|
408
|
+
body.context_limit = opts.context_limit;
|
|
409
|
+
}
|
|
410
|
+
return this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz));
|
|
411
|
+
}
|
|
412
|
+
engram(name, query, userId, opts = {}) {
|
|
413
|
+
return this.post("/api/v1/engram/run", withForm({ name, user_id: this.uid(userId), query }, opts.form, opts.tz));
|
|
414
|
+
}
|
|
415
|
+
async history(userId) {
|
|
416
|
+
const r = await this.post("/api/v1/memory/history", { user_id: this.uid(userId) });
|
|
417
|
+
return asRecords(r.turns);
|
|
418
|
+
}
|
|
419
|
+
stats(userId) {
|
|
420
|
+
return this.post("/api/v1/memory/stats", { user_id: this.uid(userId) });
|
|
421
|
+
}
|
|
422
|
+
async usage(days = 7) {
|
|
423
|
+
if (!Number.isInteger(days) || days < 1 || days > 365) {
|
|
424
|
+
throw new Error(`days must be an integer between 1 and 365, got ${days}.`);
|
|
425
|
+
}
|
|
426
|
+
return this.request("GET", `/api/v1/won/usage?days=${days}`);
|
|
427
|
+
}
|
|
428
|
+
get(userId, memoryId) {
|
|
429
|
+
if (!memoryId || typeof memoryId !== "string") {
|
|
430
|
+
throw new Error("memoryId is required — the id that add/store or listMemories returned.");
|
|
431
|
+
}
|
|
432
|
+
return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId }).then((r) => asObj(r.memory));
|
|
433
|
+
}
|
|
434
|
+
listMemories(userId, opts = {}) {
|
|
435
|
+
const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
|
|
436
|
+
if (opts.cursor)
|
|
437
|
+
body.cursor = opts.cursor;
|
|
438
|
+
return this.post("/api/v1/memory/list", body);
|
|
439
|
+
}
|
|
440
|
+
async *iterMemories(userId, opts = {}) {
|
|
441
|
+
let cursor;
|
|
442
|
+
const seen = new Set();
|
|
443
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
444
|
+
const p = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
|
|
445
|
+
for (const m of asRecords(p.memories))
|
|
446
|
+
yield m;
|
|
447
|
+
const next = p.next_cursor ?? undefined;
|
|
448
|
+
if (!next || seen.has(next))
|
|
449
|
+
return;
|
|
450
|
+
seen.add(next);
|
|
451
|
+
cursor = next;
|
|
452
|
+
}
|
|
453
|
+
throw new Error(`stopped after ${MAX_PAGES} pages — the store did not end. This is a truncated ` +
|
|
454
|
+
`answer, not the whole store.`);
|
|
455
|
+
}
|
|
456
|
+
async exportMemories(userId) {
|
|
457
|
+
const out = [];
|
|
458
|
+
for await (const m of this.iterMemories(userId))
|
|
459
|
+
out.push(m);
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
getImage(userId, memoryId) {
|
|
463
|
+
if (!memoryId || typeof memoryId !== "string") {
|
|
464
|
+
throw new Error("memoryId is required — the id that add/store or listImages returned.");
|
|
465
|
+
}
|
|
466
|
+
return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId });
|
|
467
|
+
}
|
|
468
|
+
forgetImage(userId, memoryId, opts = {}) {
|
|
469
|
+
if (!memoryId || typeof memoryId !== "string") {
|
|
470
|
+
throw new Error("memoryId is required — the id of the memory whose image you want removed.");
|
|
471
|
+
}
|
|
472
|
+
const body = { user_id: this.uid(userId), memory_id: memoryId };
|
|
473
|
+
if (opts.preview)
|
|
474
|
+
body.preview = true;
|
|
475
|
+
return this.request("DELETE", "/api/v1/memory/image", body);
|
|
476
|
+
}
|
|
477
|
+
listImages(userId, opts = {}) {
|
|
478
|
+
const body = { user_id: this.uid(userId) };
|
|
479
|
+
if (opts.limit !== undefined)
|
|
480
|
+
body.limit = opts.limit;
|
|
481
|
+
if (opts.before !== undefined)
|
|
482
|
+
body.before = opts.before;
|
|
483
|
+
if (opts.skipIds !== undefined)
|
|
484
|
+
body.skip_ids = opts.skipIds;
|
|
485
|
+
return this.post("/api/v1/memory/images", body);
|
|
486
|
+
}
|
|
487
|
+
async *iterImages(userId, opts = {}) {
|
|
488
|
+
let before;
|
|
489
|
+
let skipIds;
|
|
490
|
+
const seen = new Set();
|
|
491
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
492
|
+
const p = await this.listImages(userId, { limit: opts.pageSize, before, skipIds });
|
|
493
|
+
for (const m of asRecords(p.images))
|
|
494
|
+
yield m;
|
|
495
|
+
if (!p.has_more || !p.next_before)
|
|
496
|
+
return;
|
|
497
|
+
const key = `${p.next_before}|${(Array.isArray(p.next_skip_ids) ? p.next_skip_ids : []).join(",")}`;
|
|
498
|
+
if (seen.has(key))
|
|
499
|
+
return;
|
|
500
|
+
seen.add(key);
|
|
501
|
+
before = p.next_before;
|
|
502
|
+
skipIds = Array.isArray(p.next_skip_ids) ? p.next_skip_ids : undefined;
|
|
503
|
+
}
|
|
504
|
+
throw new Error(`stopped after ${MAX_PAGES} pages — the store did not end. This is a truncated ` +
|
|
505
|
+
`answer, not the whole store.`);
|
|
506
|
+
}
|
|
507
|
+
revisions(userId, opts = {}) {
|
|
508
|
+
const body = {};
|
|
509
|
+
if (opts.include !== undefined)
|
|
510
|
+
body.include = opts.include;
|
|
511
|
+
if (opts.limit !== undefined)
|
|
512
|
+
body.limit = opts.limit;
|
|
513
|
+
if (opts.before !== undefined)
|
|
514
|
+
body.before = opts.before;
|
|
515
|
+
if (opts.skipIds !== undefined)
|
|
516
|
+
body.skip_ids = opts.skipIds;
|
|
517
|
+
body.user_id = this.uid(userId);
|
|
518
|
+
return this.post("/api/v1/won/revisions", body);
|
|
519
|
+
}
|
|
520
|
+
lineage(userId, memoryId) {
|
|
521
|
+
if (!memoryId || typeof memoryId !== "string") {
|
|
522
|
+
throw new Error("memoryId is required — the memory whose history you want.");
|
|
523
|
+
}
|
|
524
|
+
return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId });
|
|
525
|
+
}
|
|
526
|
+
bySpeaker(speaker, userId, opts = {}) {
|
|
527
|
+
if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
|
|
528
|
+
throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
|
|
529
|
+
}
|
|
530
|
+
const body = { user_id: this.uid(userId), speaker: speaker.trim() };
|
|
531
|
+
if (opts.limit !== undefined)
|
|
532
|
+
body.limit = opts.limit;
|
|
533
|
+
if (opts.before !== undefined)
|
|
534
|
+
body.before = opts.before;
|
|
535
|
+
if (opts.skipIds !== undefined)
|
|
536
|
+
body.skip_ids = opts.skipIds;
|
|
537
|
+
return this.post("/api/v1/memory/by-speaker", body);
|
|
538
|
+
}
|
|
539
|
+
async ping() {
|
|
540
|
+
await this.request("GET", "/api/v1/memory/collections");
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
async listModels() {
|
|
544
|
+
const data = await this.request("GET", "/api/v1/models");
|
|
545
|
+
return asRecords(data.models);
|
|
546
|
+
}
|
|
547
|
+
async listEngrams() {
|
|
548
|
+
const data = await this.request("GET", "/api/v1/engram");
|
|
549
|
+
return {
|
|
550
|
+
engrams: asRecords(data.engrams),
|
|
551
|
+
forms: asRecords(data.forms),
|
|
552
|
+
note: typeof data.note === "string" ? data.note : undefined,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
createStore(userId) {
|
|
556
|
+
return this.post("/api/v1/memory/collection", { user_id: this.uid(userId) });
|
|
557
|
+
}
|
|
558
|
+
async listStores() {
|
|
559
|
+
const r = await this.request("GET", "/api/v1/memory/collections");
|
|
560
|
+
return asRecords(r.collections);
|
|
561
|
+
}
|
|
562
|
+
deleteStore(userId) {
|
|
563
|
+
if (typeof userId !== "string" || !userId.trim())
|
|
564
|
+
throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
|
|
565
|
+
warnIfStoreIdCollapses(userId);
|
|
566
|
+
return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
|
|
567
|
+
}
|
|
568
|
+
addSpeaker(speaker, userId) {
|
|
569
|
+
return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
570
|
+
}
|
|
571
|
+
async listSpeakers(userId) {
|
|
572
|
+
const r = await this.request("GET", `/api/v1/memory/speakers?user_id=${encodeURIComponent(this.uid(userId))}`);
|
|
573
|
+
return { ...r, speakers: asRecords(r?.speakers) };
|
|
574
|
+
}
|
|
575
|
+
removeSpeaker(speaker, userId) {
|
|
576
|
+
return this.request("DELETE", "/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
|
|
577
|
+
}
|
|
578
|
+
delete(userId, memoryId) {
|
|
579
|
+
if (typeof memoryId !== "string" || !memoryId.trim()) {
|
|
580
|
+
throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
|
|
581
|
+
}
|
|
582
|
+
return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId.trim() });
|
|
583
|
+
}
|
|
584
|
+
deleteAll(userId) {
|
|
585
|
+
if (typeof userId !== "string" || !userId.trim())
|
|
586
|
+
throw new Error("userId is required (a non-blank string) for deleteAll — anything else would wipe the default store.");
|
|
587
|
+
warnIfStoreIdCollapses(userId);
|
|
588
|
+
return this.post("/api/v1/memory/forget", { user_id: userId });
|
|
589
|
+
}
|
|
590
|
+
post(path, body, idempotencyKey) {
|
|
591
|
+
return this.request("POST", path, body, idempotencyKey);
|
|
592
|
+
}
|
|
593
|
+
deadlineAt() {
|
|
594
|
+
return this.deadlineMs === undefined ? undefined : Date.now() + this.deadlineMs;
|
|
595
|
+
}
|
|
596
|
+
beginAttempt(deadlineAt) {
|
|
597
|
+
let why = "timeout";
|
|
598
|
+
let budget = this.timeoutMs;
|
|
599
|
+
if (deadlineAt !== undefined) {
|
|
600
|
+
const left = deadlineAt - Date.now();
|
|
601
|
+
if (left <= 0)
|
|
602
|
+
throw new APIConnectionError(0, this.abortMessage("deadline"));
|
|
603
|
+
if (left < budget) {
|
|
604
|
+
budget = left;
|
|
605
|
+
why = "deadline";
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const ctrl = new AbortController();
|
|
609
|
+
const timer = setTimeout(() => ctrl.abort(), budget);
|
|
610
|
+
const outer = this.signal;
|
|
611
|
+
const onCaller = () => {
|
|
612
|
+
why = "caller";
|
|
613
|
+
ctrl.abort();
|
|
614
|
+
};
|
|
615
|
+
if (outer) {
|
|
616
|
+
if (outer.aborted)
|
|
617
|
+
onCaller();
|
|
618
|
+
else
|
|
619
|
+
outer.addEventListener("abort", onCaller);
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
signal: ctrl.signal,
|
|
623
|
+
clear: () => {
|
|
624
|
+
clearTimeout(timer);
|
|
625
|
+
outer?.removeEventListener("abort", onCaller);
|
|
626
|
+
},
|
|
627
|
+
why: () => why,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
abortMessage(why) {
|
|
631
|
+
if (why === "caller")
|
|
632
|
+
return "cancelled — the caller's AbortSignal fired";
|
|
633
|
+
if (why === "deadline")
|
|
634
|
+
return `deadline of ${this.deadlineMs}ms exhausted`;
|
|
635
|
+
return `request timed out after ${this.timeoutMs}ms`;
|
|
636
|
+
}
|
|
637
|
+
async backoffSleep(ms, deadlineAt) {
|
|
638
|
+
if (deadlineAt !== undefined && ms > deadlineAt - Date.now()) {
|
|
639
|
+
throw new APIConnectionError(0, this.abortMessage("deadline"));
|
|
640
|
+
}
|
|
641
|
+
const outer = this.signal;
|
|
642
|
+
if (!outer) {
|
|
643
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (outer.aborted)
|
|
647
|
+
return;
|
|
648
|
+
await new Promise((resolve) => {
|
|
649
|
+
const done = () => {
|
|
650
|
+
clearTimeout(timer);
|
|
651
|
+
outer.removeEventListener("abort", done);
|
|
652
|
+
resolve();
|
|
653
|
+
};
|
|
654
|
+
const timer = setTimeout(done, ms);
|
|
655
|
+
outer.addEventListener("abort", done);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
async requestBytes(path, body) {
|
|
659
|
+
const attempts = this.maxRetries + 1;
|
|
660
|
+
const deadlineAt = this.deadlineAt();
|
|
661
|
+
for (let attempt = 0;; attempt++) {
|
|
662
|
+
const att = this.beginAttempt(deadlineAt);
|
|
663
|
+
let res;
|
|
664
|
+
try {
|
|
665
|
+
res = await this.fetchImpl(`${this.base}${path}`, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
headers: this.headers,
|
|
668
|
+
body: JSON.stringify(body),
|
|
669
|
+
signal: att.signal,
|
|
670
|
+
redirect: "manual",
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch (e) {
|
|
674
|
+
att.clear();
|
|
675
|
+
const timedOut = e?.name === "AbortError" || att.signal.aborted;
|
|
676
|
+
if (!timedOut && isConnectFailure(e) && attempt + 1 < attempts) {
|
|
677
|
+
await this.backoffSleep(this.backoffMs(attempt), deadlineAt);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
throw new APIConnectionError(0, timedOut ? this.abortMessage(att.why()) : `network error: ${e?.message ?? e}`);
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
684
|
+
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
685
|
+
void res.body?.cancel();
|
|
686
|
+
throw errorFor(res.status, "the API answered with a redirect; refusing to follow it");
|
|
687
|
+
}
|
|
688
|
+
if (!res.ok) {
|
|
689
|
+
if (RETRY_ALWAYS.has(res.status) && attempt + 1 < attempts) {
|
|
690
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
691
|
+
void res.body?.cancel();
|
|
692
|
+
att.clear();
|
|
693
|
+
await this.backoffSleep(this.backoffMs(attempt, retryAfter), deadlineAt);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
let msg = await this.readCapped(res).catch(() => "");
|
|
697
|
+
try {
|
|
698
|
+
const data = JSON.parse(msg);
|
|
699
|
+
const err = data?.error;
|
|
700
|
+
if (err && typeof err === "object")
|
|
701
|
+
msg = err.message ?? err.type ?? msg;
|
|
702
|
+
else if (typeof err === "string")
|
|
703
|
+
msg = err;
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
}
|
|
707
|
+
if (msg.length > MAX_ERR_MSG)
|
|
708
|
+
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
709
|
+
throw errorFor(res.status, msg || `HTTP ${res.status}`);
|
|
710
|
+
}
|
|
711
|
+
let buf;
|
|
712
|
+
try {
|
|
713
|
+
buf = await this.readCappedBytes(res);
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
if (e?.name === "AbortError" || att.signal.aborted) {
|
|
717
|
+
throw new APIConnectionError(0, this.abortMessage(att.why()));
|
|
718
|
+
}
|
|
719
|
+
throw e;
|
|
720
|
+
}
|
|
721
|
+
if (buf.byteLength === 0) {
|
|
722
|
+
throw new WosError(res.status, "empty image body — the service returned no bytes");
|
|
723
|
+
}
|
|
724
|
+
return { bytes: buf, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
att.clear();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
idemHeader(key) {
|
|
732
|
+
if (key === undefined || key === null)
|
|
733
|
+
return undefined;
|
|
734
|
+
if (!IDEMPOTENCY_KEY_RE.test(key)) {
|
|
735
|
+
throw new Error(`invalid idempotencyKey: ${JSON.stringify(key)} — 1-128 chars of [A-Za-z0-9._:-]`);
|
|
736
|
+
}
|
|
737
|
+
return { "Idempotency-Key": key };
|
|
738
|
+
}
|
|
739
|
+
backoffMs(attempt, retryAfter) {
|
|
740
|
+
if (retryAfter) {
|
|
741
|
+
const s = Number(retryAfter);
|
|
742
|
+
if (Number.isFinite(s) && s >= 0)
|
|
743
|
+
return Math.min(30_000, s * 1000);
|
|
744
|
+
const t = Date.parse(retryAfter);
|
|
745
|
+
if (Number.isFinite(t))
|
|
746
|
+
return Math.min(30_000, Math.max(0, t - Date.now()));
|
|
747
|
+
}
|
|
748
|
+
return Math.min(8_000, 500 * 2 ** attempt) + Math.random() * 250;
|
|
749
|
+
}
|
|
750
|
+
async readCapped(res) {
|
|
751
|
+
return new TextDecoder().decode(await this.readCappedBytes(res));
|
|
752
|
+
}
|
|
753
|
+
async readCappedBytes(res) {
|
|
754
|
+
const cl = res.headers.get("content-length");
|
|
755
|
+
if (cl && Number(cl) > MAX_RESPONSE_BYTES) {
|
|
756
|
+
throw new WosError(res.status, `response too large (${cl} bytes) — refusing to buffer it`);
|
|
757
|
+
}
|
|
758
|
+
if (!res.body) {
|
|
759
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
760
|
+
if (buf.byteLength > MAX_RESPONSE_BYTES) {
|
|
761
|
+
throw new WosError(res.status, `response too large (${buf.byteLength} bytes) — refusing it`);
|
|
762
|
+
}
|
|
763
|
+
return buf;
|
|
764
|
+
}
|
|
765
|
+
const reader = res.body.getReader();
|
|
766
|
+
const parts = [];
|
|
767
|
+
let size = 0;
|
|
768
|
+
for (;;) {
|
|
769
|
+
const { done, value } = await reader.read();
|
|
770
|
+
if (done)
|
|
771
|
+
break;
|
|
772
|
+
size += value.byteLength;
|
|
773
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
774
|
+
void reader.cancel();
|
|
775
|
+
throw new WosError(res.status, "response too large — refusing to buffer it");
|
|
776
|
+
}
|
|
777
|
+
parts.push(value);
|
|
778
|
+
}
|
|
779
|
+
const buf = new Uint8Array(size);
|
|
780
|
+
let off = 0;
|
|
781
|
+
for (const p of parts) {
|
|
782
|
+
buf.set(p, off);
|
|
783
|
+
off += p.byteLength;
|
|
784
|
+
}
|
|
785
|
+
return buf;
|
|
786
|
+
}
|
|
787
|
+
async request(method, path, body, idempotencyKey) {
|
|
788
|
+
const extraHeaders = this.idemHeader(idempotencyKey);
|
|
789
|
+
const attempts = this.maxRetries + 1;
|
|
790
|
+
const logPath = path.split("?")[0];
|
|
791
|
+
const deadlineAt = this.deadlineAt();
|
|
792
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
793
|
+
const start = Date.now();
|
|
794
|
+
const att = this.beginAttempt(deadlineAt);
|
|
795
|
+
let res;
|
|
796
|
+
try {
|
|
797
|
+
res = await this.fetchImpl(`${this.base}${path}`, {
|
|
798
|
+
method,
|
|
799
|
+
headers: extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers,
|
|
800
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
801
|
+
signal: att.signal,
|
|
802
|
+
redirect: "manual",
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
catch (e) {
|
|
806
|
+
att.clear();
|
|
807
|
+
const timedOut = e?.name === "AbortError" || att.signal.aborted;
|
|
808
|
+
const safe = IDEMPOTENT_METHODS.has(method.toUpperCase()) || isConnectFailure(e);
|
|
809
|
+
if (!timedOut && safe && attempt + 1 < attempts) {
|
|
810
|
+
const delay = this.backoffMs(attempt);
|
|
811
|
+
logDebug(`${method} ${logPath}: ${e?.code ?? e?.name ?? "network error"} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
812
|
+
await this.backoffSleep(delay, deadlineAt);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
throw new APIConnectionError(0, timedOut ? this.abortMessage(att.why()) : `network error: ${e?.message ?? e}`);
|
|
816
|
+
}
|
|
817
|
+
const retryable = RETRY_ALWAYS.has(res.status) ||
|
|
818
|
+
(RETRY_IF_IDEMPOTENT.has(res.status) && IDEMPOTENT_METHODS.has(method.toUpperCase()));
|
|
819
|
+
if (retryable && attempt + 1 < attempts) {
|
|
820
|
+
att.clear();
|
|
821
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
822
|
+
void res.body?.cancel();
|
|
823
|
+
const delay = this.backoffMs(attempt, retryAfter);
|
|
824
|
+
logDebug(`${method} ${logPath} -> ${res.status} — retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
|
|
825
|
+
await this.backoffSleep(delay, deadlineAt);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
829
|
+
att.clear();
|
|
830
|
+
void res.body?.cancel();
|
|
831
|
+
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
832
|
+
}
|
|
833
|
+
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
834
|
+
let text;
|
|
835
|
+
try {
|
|
836
|
+
text = await this.readCapped(res);
|
|
837
|
+
}
|
|
838
|
+
catch (e) {
|
|
839
|
+
if (e?.name === "AbortError" || att.signal.aborted) {
|
|
840
|
+
throw new APIConnectionError(0, this.abortMessage(att.why()));
|
|
841
|
+
}
|
|
842
|
+
if (e instanceof WosError)
|
|
843
|
+
throw e;
|
|
844
|
+
throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
att.clear();
|
|
848
|
+
}
|
|
849
|
+
logDebug(`${method} ${logPath} -> ${res.status} in ${Date.now() - start}ms (attempt ${attempt + 1}/${attempts})`);
|
|
850
|
+
if (!res.ok) {
|
|
851
|
+
let msg = text;
|
|
852
|
+
let requestId;
|
|
853
|
+
try {
|
|
854
|
+
const data = JSON.parse(text);
|
|
855
|
+
if (data && typeof data === "object") {
|
|
856
|
+
const err = data.error;
|
|
857
|
+
if (err && typeof err === "object") {
|
|
858
|
+
msg = err.message ?? err.type ?? msg;
|
|
859
|
+
if (typeof err.request_id === "string")
|
|
860
|
+
requestId = err.request_id;
|
|
861
|
+
}
|
|
862
|
+
else if (typeof err === "string")
|
|
863
|
+
msg = err;
|
|
864
|
+
else if (typeof data.message === "string")
|
|
865
|
+
msg = data.message;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
}
|
|
870
|
+
if (typeof msg === "string" && msg.length > MAX_ERR_MSG)
|
|
871
|
+
msg = msg.slice(0, MAX_ERR_MSG) + "…(truncated)";
|
|
872
|
+
throw errorFor(res.status, msg, requestId);
|
|
873
|
+
}
|
|
874
|
+
if (!text) {
|
|
875
|
+
if (NO_BODY_STATUS.has(res.status))
|
|
876
|
+
return {};
|
|
877
|
+
throw new WosError(res.status, "empty response body — expected a JSON object");
|
|
878
|
+
}
|
|
879
|
+
let data;
|
|
880
|
+
try {
|
|
881
|
+
data = JSON.parse(text);
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
throw new WosError(res.status, `invalid JSON in response: ${e?.message ?? e}`);
|
|
885
|
+
}
|
|
886
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
887
|
+
const got = data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
888
|
+
throw new WosError(res.status, `expected a JSON object in the response, got ${got}`);
|
|
889
|
+
}
|
|
890
|
+
if (res.headers.get("Idempotent-Replayed") === "true" && !("replayed" in data)) {
|
|
891
|
+
data.replayed = true;
|
|
892
|
+
}
|
|
893
|
+
return data;
|
|
894
|
+
}
|
|
895
|
+
throw new Error("retries exhausted");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
exports.Client = Client;
|
|
899
|
+
exports.WME = Client;
|
|
900
|
+
exports.default = Client;
|