@selvajs/compute 5.0.1 → 5.1.0-beta.0

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.
@@ -1,72 +1,3 @@
1
- // src/core/compute-fetch/retry.ts
2
- const DEFAULT_RETRY = {
3
- attempts: 0,
4
- baseDelayMs: 500,
5
- maxDelayMs: 3e4,
6
- retryOn429: true
7
- };
8
- const RETRYABLE_STATUS = /* @__PURE__ */ new Set([
9
- 502,
10
- 503,
11
- 504
12
- ]);
13
- /**
14
- * Ceiling on a server-sent `Retry-After` wait. The server's window outranks
15
- * `RetryPolicy.maxDelayMs` (retrying sooner all but guarantees another 429), but a bad or hostile
16
- * header must not park the client for minutes. A `maxDelayMs` above this cap raises it.
17
- */
18
- const RETRY_AFTER_CAP_MS = 6e4;
19
- function resolveRetryPolicy(policy) {
20
- if (!policy) return DEFAULT_RETRY;
21
- return {
22
- attempts: policy.attempts ?? DEFAULT_RETRY.attempts,
23
- baseDelayMs: policy.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
24
- maxDelayMs: policy.maxDelayMs ?? DEFAULT_RETRY.maxDelayMs,
25
- retryOn429: policy.retryOn429 ?? DEFAULT_RETRY.retryOn429
26
- };
27
- }
28
- function isRetryableStatus(status, policy) {
29
- return RETRYABLE_STATUS.has(status) || policy.retryOn429 && status === 429;
30
- }
31
- /** Delay before the next attempt: the server's `Retry-After` when it sent one, else backoff. */
32
- function retryDelay(retryAfter, attempt, policy) {
33
- const retryAfterMs = parseRetryAfter(retryAfter);
34
- return retryAfterMs === null ? backoffDelay(attempt, policy) : Math.min(retryAfterMs, Math.max(policy.maxDelayMs, RETRY_AFTER_CAP_MS));
35
- }
36
- /** A `Retry-After` value (seconds or HTTP-date) in ms; null when missing or unparseable. */
37
- function parseRetryAfter(headerValue) {
38
- if (!headerValue) return null;
39
- const seconds = Number(headerValue);
40
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
41
- const dateMs = Date.parse(headerValue);
42
- if (Number.isFinite(dateMs)) {
43
- const delta = dateMs - Date.now();
44
- return delta > 0 ? delta : 0;
45
- }
46
- return null;
47
- }
48
- function backoffDelay(attempt, policy) {
49
- const exponential = policy.baseDelayMs * Math.pow(2, attempt);
50
- const jitter = Math.random() * policy.baseDelayMs;
51
- return Math.min(exponential + jitter, policy.maxDelayMs);
52
- }
53
- function sleep(ms, signal) {
54
- return new Promise((resolve, reject) => {
55
- if (signal?.aborted) {
56
- reject(new DOMException("Aborted", "AbortError"));
57
- return;
58
- }
59
- const id = setTimeout(() => {
60
- signal?.removeEventListener("abort", onAbort);
61
- resolve();
62
- }, ms);
63
- const onAbort = () => {
64
- clearTimeout(id);
65
- reject(new DOMException("Aborted", "AbortError"));
66
- };
67
- signal?.addEventListener("abort", onAbort, { once: true });
68
- });
69
- }
70
1
  // src/core/errors.ts
71
2
  const ErrorCodes = {
72
3
  /** Connection failure, a gateway status (502/503/504), or a 2xx body cut off mid-stream. */
@@ -208,6 +139,179 @@ function setLogger(logger) {
208
139
  function enableDebugLogging() {
209
140
  setLogger(console);
210
141
  }
142
+ // src/core/utils/warnings.ts
143
+ const warned = /* @__PURE__ */ new Set();
144
+ /** Log `message` the first time `key` is seen: what it reports doesn't change per call. */
145
+ function warnOnce(key, message) {
146
+ if (warned.has(key)) return;
147
+ warned.add(key);
148
+ getLogger().warn(message);
149
+ }
150
+ /**
151
+ * True only in a real browser, where an API key in the config is exposed to users. jsdom defines
152
+ * `window` too, so Node is ruled out by `process.versions.node`, and by jsdom's user-agent marker
153
+ * for setups that hide `process`.
154
+ */
155
+ function isRealBrowser() {
156
+ if (typeof window === "undefined") return false;
157
+ if (globalThis.process?.versions?.node != null) return false;
158
+ const nav = globalThis.navigator;
159
+ const userAgent = typeof nav?.userAgent === "string" ? nav.userAgent : "";
160
+ if (/jsdom/i.test(userAgent)) return false;
161
+ return true;
162
+ }
163
+ function warnIfClientSide(functionName, suppress) {
164
+ if (suppress || !isRealBrowser()) return;
165
+ warnOnce(`client-side:${functionName}`, `Warning: ${functionName} is running on the client side. For better performance and security, consider running this on the server side.`);
166
+ }
167
+ // src/core/compute-fetch/request.ts
168
+ /** `serverUrl` as `validateServerUrl` returns it: no trailing slash. */
169
+ function buildUrl(endpoint, serverUrl) {
170
+ return `${serverUrl}/${endpoint.replace(/^\/+/, "")}`;
171
+ }
172
+ function isLocalhost(serverUrl) {
173
+ const hostname = new URL(serverUrl).hostname.toLowerCase();
174
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
175
+ }
176
+ const DEFAULT_API_KEY_HEADER = "RhinoComputeKey";
177
+ /**
178
+ * Caller headers plus credentials. Every request to the server builds its headers here,
179
+ * so a liveness probe authenticates exactly like the solve it gates.
180
+ */
181
+ function buildAuthHeaders(auth) {
182
+ return assertSendable(mergeUnder(auth.headers, credentialHeaders(auth)));
183
+ }
184
+ function buildHeaders(requestId, config) {
185
+ const headers = assertSendable(mergeUnder(config.headers, {
186
+ ...credentialHeaders(config),
187
+ ...sessionHeader(config.sessionId),
188
+ "X-Request-ID": requestId,
189
+ "Content-Type": "application/json"
190
+ }));
191
+ if (!config.apiKey && !config.authToken && !isLocalhost(config.serverUrl)) warnOnce(`no-auth:${config.serverUrl}`, `[Compute] Request [${requestId}] targets remote server (${config.serverUrl}) but no API key or auth token is configured. Requests may fail or be rate-limited. (warned once per server)`);
192
+ return headers;
193
+ }
194
+ /** `headers` with a JSON `Content-Type` that replaces one of any casing already there. */
195
+ function withJsonContentType(headers) {
196
+ return mergeUnder(headers, { "Content-Type": "application/json" });
197
+ }
198
+ function credentialHeaders(auth) {
199
+ return {
200
+ ...auth.authToken && { Authorization: auth.authToken },
201
+ ...auth.apiKey && { [auth.apiKeyHeader ?? DEFAULT_API_KEY_HEADER]: auth.apiKey }
202
+ };
203
+ }
204
+ const SESSION_ID = /^[!-~]{1,128}$/;
205
+ /**
206
+ * Compute drops a malformed id without an error, so the request would quietly lose its routing.
207
+ * The client and scheduler call this on entry too: by `buildHeaders` a solve has already taken a
208
+ * slot and loaded its definition.
209
+ */
210
+ function assertSessionId(sessionId) {
211
+ if (sessionId === void 0 || SESSION_ID.test(sessionId)) return;
212
+ throw new ComputeError("sessionId must be 1 to 128 characters from \"!\" to \"~\"", ErrorCodes.INVALID_CONFIG, { context: { length: sessionId.length } });
213
+ }
214
+ function sessionHeader(sessionId) {
215
+ assertSessionId(sessionId);
216
+ return sessionId === void 0 ? {} : { "X-Session-Id": sessionId };
217
+ }
218
+ /**
219
+ * Header names are case-insensitive and fetch joins duplicates, so a caller key
220
+ * matching a transport key in any case is dropped rather than sent alongside it.
221
+ */
222
+ function mergeUnder(caller, transport) {
223
+ const taken = new Set(Object.keys(transport).map((name) => name.toLowerCase()));
224
+ const kept = Object.entries(caller ?? {}).filter(([name]) => !taken.has(name.toLowerCase()));
225
+ return {
226
+ ...Object.fromEntries(kept),
227
+ ...transport
228
+ };
229
+ }
230
+ /**
231
+ * Left to fetch, a bad value becomes a retried TypeError that quotes it, and the value may be the
232
+ * API key. `Headers` throws the same message, so its error is not kept as the cause.
233
+ */
234
+ function assertSendable(headers) {
235
+ const check = new Headers();
236
+ for (const [name, value] of Object.entries(headers)) try {
237
+ check.set(name, value);
238
+ } catch {
239
+ throw new ComputeError(`Header "${name}" has an invalid name or value`, ErrorCodes.INVALID_CONFIG, { context: { header: name } });
240
+ }
241
+ return headers;
242
+ }
243
+ function generateRequestId() {
244
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
245
+ }
246
+ // src/core/compute-fetch/retry.ts
247
+ const DEFAULT_RETRY = {
248
+ attempts: 0,
249
+ baseDelayMs: 500,
250
+ maxDelayMs: 3e4,
251
+ retryOn429: true
252
+ };
253
+ const RETRYABLE_STATUS = /* @__PURE__ */ new Set([
254
+ 502,
255
+ 503,
256
+ 504
257
+ ]);
258
+ /**
259
+ * Ceiling on a server-sent `Retry-After` wait. The server's window outranks
260
+ * `RetryPolicy.maxDelayMs` (retrying sooner all but guarantees another 429), but a bad or hostile
261
+ * header must not park the client for minutes. A `maxDelayMs` above this cap raises it.
262
+ */
263
+ const RETRY_AFTER_CAP_MS = 6e4;
264
+ function resolveRetryPolicy(policy) {
265
+ if (!policy) return DEFAULT_RETRY;
266
+ return {
267
+ attempts: policy.attempts ?? DEFAULT_RETRY.attempts,
268
+ baseDelayMs: policy.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
269
+ maxDelayMs: policy.maxDelayMs ?? DEFAULT_RETRY.maxDelayMs,
270
+ retryOn429: policy.retryOn429 ?? DEFAULT_RETRY.retryOn429
271
+ };
272
+ }
273
+ function isRetryableStatus(status, policy) {
274
+ return RETRYABLE_STATUS.has(status) || policy.retryOn429 && status === 429;
275
+ }
276
+ /** Delay before the next attempt: the server's `Retry-After` when it sent one, else backoff. */
277
+ function retryDelay(retryAfter, attempt, policy) {
278
+ const retryAfterMs = parseRetryAfter(retryAfter);
279
+ return retryAfterMs === null ? backoffDelay(attempt, policy) : Math.min(retryAfterMs, Math.max(policy.maxDelayMs, RETRY_AFTER_CAP_MS));
280
+ }
281
+ /** A `Retry-After` value (seconds or HTTP-date) in ms; null when missing or unparseable. */
282
+ function parseRetryAfter(headerValue) {
283
+ if (!headerValue) return null;
284
+ const seconds = Number(headerValue);
285
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
286
+ const dateMs = Date.parse(headerValue);
287
+ if (Number.isFinite(dateMs)) {
288
+ const delta = dateMs - Date.now();
289
+ return delta > 0 ? delta : 0;
290
+ }
291
+ return null;
292
+ }
293
+ function backoffDelay(attempt, policy) {
294
+ const exponential = policy.baseDelayMs * Math.pow(2, attempt);
295
+ const jitter = Math.random() * policy.baseDelayMs;
296
+ return Math.min(exponential + jitter, policy.maxDelayMs);
297
+ }
298
+ function sleep(ms, signal) {
299
+ return new Promise((resolve, reject) => {
300
+ if (signal?.aborted) {
301
+ reject(new DOMException("Aborted", "AbortError"));
302
+ return;
303
+ }
304
+ const id = setTimeout(() => {
305
+ signal?.removeEventListener("abort", onAbort);
306
+ resolve();
307
+ }, ms);
308
+ const onAbort = () => {
309
+ clearTimeout(id);
310
+ reject(new DOMException("Aborted", "AbortError"));
311
+ };
312
+ signal?.addEventListener("abort", onAbort, { once: true });
313
+ });
314
+ }
211
315
  // src/core/utils/read-field.ts
212
316
  /**
213
317
  * Case-insensitive field reads for wire payloads.
@@ -312,95 +416,6 @@ function trimTrailing(value, char) {
312
416
  while (end > 0 && value[end - 1] === char) end--;
313
417
  return value.slice(0, end);
314
418
  }
315
- // src/core/utils/warnings.ts
316
- const warned = /* @__PURE__ */ new Set();
317
- /** Log `message` the first time `key` is seen: what it reports doesn't change per call. */
318
- function warnOnce(key, message) {
319
- if (warned.has(key)) return;
320
- warned.add(key);
321
- getLogger().warn(message);
322
- }
323
- /**
324
- * True only in a real browser, where an API key in the config is exposed to users. jsdom defines
325
- * `window` too, so Node is ruled out by `process.versions.node`, and by jsdom's user-agent marker
326
- * for setups that hide `process`.
327
- */
328
- function isRealBrowser() {
329
- if (typeof window === "undefined") return false;
330
- if (globalThis.process?.versions?.node != null) return false;
331
- const nav = globalThis.navigator;
332
- const userAgent = typeof nav?.userAgent === "string" ? nav.userAgent : "";
333
- if (/jsdom/i.test(userAgent)) return false;
334
- return true;
335
- }
336
- function warnIfClientSide(functionName, suppress) {
337
- if (suppress || !isRealBrowser()) return;
338
- warnOnce(`client-side:${functionName}`, `Warning: ${functionName} is running on the client side. For better performance and security, consider running this on the server side.`);
339
- }
340
- // src/core/compute-fetch/request.ts
341
- /** `serverUrl` as `validateServerUrl` returns it: no trailing slash. */
342
- function buildUrl(endpoint, serverUrl) {
343
- return `${serverUrl}/${endpoint.replace(/^\/+/, "")}`;
344
- }
345
- function isLocalhost(serverUrl) {
346
- const hostname = new URL(serverUrl).hostname.toLowerCase();
347
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
348
- }
349
- const DEFAULT_API_KEY_HEADER = "RhinoComputeKey";
350
- /**
351
- * Caller headers plus credentials. Every request to the server builds its headers here,
352
- * so a liveness probe authenticates exactly like the solve it gates.
353
- */
354
- function buildAuthHeaders(auth) {
355
- return assertSendable(mergeUnder(auth.headers, credentialHeaders(auth)));
356
- }
357
- function buildHeaders(requestId, config) {
358
- const headers = assertSendable(mergeUnder(config.headers, {
359
- ...credentialHeaders(config),
360
- "X-Request-ID": requestId,
361
- "Content-Type": "application/json"
362
- }));
363
- if (!config.apiKey && !config.authToken && !isLocalhost(config.serverUrl)) warnOnce(`no-auth:${config.serverUrl}`, `[Compute] Request [${requestId}] targets remote server (${config.serverUrl}) but no API key or auth token is configured. Requests may fail or be rate-limited. (warned once per server)`);
364
- return headers;
365
- }
366
- /** `headers` with a JSON `Content-Type` that replaces one of any casing already there. */
367
- function withJsonContentType(headers) {
368
- return mergeUnder(headers, { "Content-Type": "application/json" });
369
- }
370
- function credentialHeaders(auth) {
371
- return {
372
- ...auth.authToken && { Authorization: auth.authToken },
373
- ...auth.apiKey && { [auth.apiKeyHeader ?? DEFAULT_API_KEY_HEADER]: auth.apiKey }
374
- };
375
- }
376
- /**
377
- * Header names are case-insensitive and fetch joins duplicates, so a caller key
378
- * matching a transport key in any case is dropped rather than sent alongside it.
379
- */
380
- function mergeUnder(caller, transport) {
381
- const taken = new Set(Object.keys(transport).map((name) => name.toLowerCase()));
382
- const kept = Object.entries(caller ?? {}).filter(([name]) => !taken.has(name.toLowerCase()));
383
- return {
384
- ...Object.fromEntries(kept),
385
- ...transport
386
- };
387
- }
388
- /**
389
- * Left to fetch, a bad value becomes a retried TypeError that quotes it, and the value may be the
390
- * API key. `Headers` throws the same message, so its error is not kept as the cause.
391
- */
392
- function assertSendable(headers) {
393
- const check = new Headers();
394
- for (const [name, value] of Object.entries(headers)) try {
395
- check.set(name, value);
396
- } catch {
397
- throw new ComputeError(`Header "${name}" has an invalid name or value`, ErrorCodes.INVALID_CONFIG, { context: { header: name } });
398
- }
399
- return headers;
400
- }
401
- function generateRequestId() {
402
- return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
403
- }
404
419
  // src/core/utils/encoding.ts
405
420
  function getNodeBuffer() {
406
421
  const buf = globalThis.Buffer;
@@ -538,12 +553,20 @@ async function readBodyText(response, limit = Infinity) {
538
553
  truncated: false
539
554
  };
540
555
  }
556
+ // src/core/types.ts
557
+ /** Routing outcomes the VektorNode fork reports in `Server-Timing`; the parser drops any other. */
558
+ const AFFINITY_OUTCOMES = [
559
+ "hit",
560
+ "claimed",
561
+ "spilled",
562
+ "none"
563
+ ];
541
564
  // src/core/compute-fetch/server-timing.ts
542
565
  /**
543
566
  * Parse a W3C Server Timing header, as the solve endpoint emits it:
544
567
  * `decode;dur=3, solve;dur=120, gh;dur=110, encode;dur=8, recomputed;desc="4/97"`, or
545
- * `cache;desc="hit"` when the server answered from its result cache. Null when it carries no
546
- * recognised metric.
568
+ * `cache;desc="hit"` when the server answered from its result cache. A proxy's `affinity` can
569
+ * arrive alone, with no durations. Null when it carries no recognised entry.
547
570
  */
548
571
  function parseServerTiming(headerValue) {
549
572
  if (!headerValue) return null;
@@ -568,10 +591,18 @@ function parseServerTiming(headerValue) {
568
591
  } else if (name === "cache" && param(params, "desc") === "hit") {
569
592
  timing.cacheHit = true;
570
593
  sawMetric = true;
594
+ } else if (name === "affinity") {
595
+ const outcome = param(params, "desc");
596
+ if (!isAffinity(outcome)) continue;
597
+ timing.affinity = outcome;
598
+ sawMetric = true;
571
599
  }
572
600
  }
573
601
  return sawMetric ? timing : null;
574
602
  }
603
+ function isAffinity(value) {
604
+ return AFFINITY_OUTCOMES.includes(value);
605
+ }
575
606
  function param(params, key) {
576
607
  for (const p of params) {
577
608
  const [k, ...v] = p.split("=");
@@ -1374,6 +1405,6 @@ function saveFile(blob, filename) {
1374
1405
  a.remove();
1375
1406
  setTimeout(() => URL.revokeObjectURL(url), 1e4);
1376
1407
  }
1377
- export { readField as A, withJsonContentType as C, tryParseJson as D, validateServerUrl as E, ErrorCodes as F, abortedByCaller as I, sleep as L, getLogger as M, setLogger as N, redactUrl as O, ComputeError as P, buildAuthHeaders as S, DEFAULT_BLOCKED_HOST as T, readBodyText as _, groupFilesByRoot as a, detectBase64Payload as b, isDefinitionUrl as c, classifyProbeFailure as d, isRedirect as f, raceSignal as g, composeSignal as h, isBase64Flag as i, enableDebugLogging as j, hasField as k, fetchCompute as l, setResponseWireSize as m, downloadFileDataByRoot as n, subFolderSegments as o, getResponseWireSize as p, extractFilesFromComputeResponse as r, isDefinitionRef as s, downloadFileData as t, validateNumbers as u, base64ByteArray as v, warnIfClientSide as w, encodeStringToBase64 as x, decodeBase64ToBinary as y };
1408
+ export { buildAuthHeaders as A, validateServerUrl as C, readField as D, hasField as E, getLogger as F, setLogger as I, ComputeError as L, withJsonContentType as M, warnIfClientSide as N, sleep as O, enableDebugLogging as P, ErrorCodes as R, DEFAULT_BLOCKED_HOST as S, redactUrl as T, readBodyText as _, groupFilesByRoot as a, detectBase64Payload as b, isDefinitionUrl as c, classifyProbeFailure as d, isRedirect as f, raceSignal as g, composeSignal as h, isBase64Flag as i, mergeUnder as j, assertSessionId as k, fetchCompute as l, setResponseWireSize as m, downloadFileDataByRoot as n, subFolderSegments as o, getResponseWireSize as p, extractFilesFromComputeResponse as r, isDefinitionRef as s, downloadFileData as t, validateNumbers as u, base64ByteArray as v, tryParseJson as w, encodeStringToBase64 as x, decodeBase64ToBinary as y, abortedByCaller as z };
1378
1409
 
1379
- //# sourceMappingURL=handle-files-D-4X4Uh_.js.map
1410
+ //# sourceMappingURL=handle-files-CzqBN3MP.js.map