{"version":3,"file":"core-C9chLkyW.cjs","names":[],"sources":["../src/core/errors.ts","../src/core/utils/logger.ts","../src/core/utils/read-field.ts","../src/core/server/validate-server-url.ts","../src/core/server/classify-probe-failure.ts","../src/core/utils/encoding.ts","../src/core/compute-fetch/log.ts","../src/core/compute-fetch/request.ts","../src/core/compute-fetch/server-timing.ts","../src/core/compute-fetch/wire-size.ts","../src/core/compute-fetch/response.ts","../src/core/compute-fetch/retry.ts","../src/core/compute-fetch/signal.ts","../src/core/compute-fetch/compute-fetch.ts","../src/core/definition-ref.ts","../src/core/files/sub-folder.ts","../src/core/files/handle-files.ts"],"sourcesContent":["export const ErrorCodes = {\n\tNETWORK_ERROR: 'NETWORK_ERROR',\n\tAUTH_ERROR: 'AUTH_ERROR',\n\tVALIDATION_ERROR: 'VALIDATION_ERROR',\n\tCOMPUTATION_ERROR: 'COMPUTATION_ERROR',\n\tTIMEOUT_ERROR: 'TIMEOUT_ERROR',\n\tCORS_ERROR: 'CORS_ERROR',\n\t/** HTTP 404 — the endpoint path does not exist on the server (usually a typo'd route or wrong server). Never retried. */\n\tNOT_FOUND: 'NOT_FOUND',\n\t/** HTTP 429 — the server is rate-limiting requests. Retried (honoring `Retry-After`) once a retry policy sets `attempts > 0`; opt out via `RetryPolicy.retryOn429`. */\n\tRATE_LIMIT: 'RATE_LIMIT',\n\t/**\n\t * The server answered 2xx but the body is deterministically not JSON (the\n\t * response declared a non-JSON `Content-Type`, e.g. an HTML captive-portal or\n\t * proxy login page, or a misconfigured endpoint). Never retried — refetching\n\t * returns the same page.\n\t */\n\tINVALID_RESPONSE: 'INVALID_RESPONSE',\n\tUNKNOWN_ERROR: 'UNKNOWN_ERROR',\n\tINVALID_STATE: 'INVALID_STATE',\n\tINVALID_INPUT: 'INVALID_INPUT',\n\tINVALID_CONFIG: 'INVALID_CONFIG',\n\tBROWSER_ONLY: 'BROWSER_ONLY',\n\t/** The runtime lacks a required capability (e.g. no base64 codec — neither `Buffer` nor `atob`/`btoa`). */\n\tENVIRONMENT_ERROR: 'ENVIRONMENT_ERROR',\n\tENCODING_ERROR: 'ENCODING_ERROR',\n\t/** An input's `default` had a shape the normalizer didn't recognize (no innerTree key). */\n\tMALFORMED_DEFAULT: 'MALFORMED_DEFAULT',\n\t/** Scheduler latest-wins: this call was replaced by a newer one. */\n\tSUPERSEDED: 'SUPERSEDED',\n\t/** Scheduler / caller-supplied AbortSignal: this call was aborted. */\n\tABORTED: 'ABORTED',\n\t/**\n\t * Scheduler backpressure: the queue was already at `maxQueueDepth` when this\n\t * call arrived, so it was rejected immediately rather than queued. Retryable — the\n\t * caller (or an HTTP layer, as 503 + Retry-After) should back off and retry.\n\t * The error `context` carries `{ queueDepth, maxQueueDepth }`.\n\t */\n\tQUEUE_FULL: 'QUEUE_FULL',\n\t/**\n\t * Scheduler backpressure: this call sat queued longer than `queueWaitMs`\n\t * without starting, so it was rejected before wasting compute on a stale request.\n\t * Retryable. The error `context` carries `{ waitedMs, queueWaitMs }`.\n\t */\n\tQUEUE_TIMEOUT: 'QUEUE_TIMEOUT',\n\t/**\n\t * A solve referenced a definition by `pointer` (server-side cache key), but the\n\t * server no longer holds that definition (evicted / GC'd / a different child in\n\t * the pool / server restarted). The caller should retry with the full\n\t * definition. Surfaced when the server tags its error body with\n\t * `code: \"definition_not_cached\"`, so it survives production message-scrubbing.\n\t */\n\tDEFINITION_NOT_CACHED: 'DEFINITION_NOT_CACHED'\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n/**\n * Simplified error for Rhino Compute operations\n *\n * @public Use this for error handling with error codes and context.\n */\nexport class ComputeError extends Error {\n\tpublic readonly code: ErrorCode;\n\tpublic readonly statusCode?: number;\n\tpublic readonly context?: Record<string, unknown>;\n\tpublic readonly originalError?: Error;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: ErrorCode = ErrorCodes.UNKNOWN_ERROR,\n\t\toptions?: { statusCode?: number; context?: Record<string, unknown>; originalError?: Error }\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'ComputeError';\n\t\tthis.code = code;\n\t\tthis.statusCode = options?.statusCode;\n\t\tthis.context = options?.context;\n\t\tthis.originalError = options?.originalError;\n\t\tif (options?.originalError) {\n\t\t\t(this as { cause?: unknown }).cause = options.originalError;\n\t\t}\n\t}\n\n\t/**\n\t * Create an error for missing/empty values\n\t */\n\tstatic missingValues(\n\t\tinputName: string,\n\t\texpectedType?: string,\n\t\tcontext?: Record<string, unknown>\n\t) {\n\t\treturn new ComputeError(\n\t\t\t`Input \"${inputName}\" has no values defined${expectedType ? ` (expected ${expectedType})` : ''}`,\n\t\t\tErrorCodes.INVALID_INPUT,\n\t\t\t{ context: { inputName, expectedType, ...context } }\n\t\t);\n\t}\n\n\t/**\n\t * Create an error for unknown parameter type\n\t */\n\tstatic unknownParamType(\n\t\tparamType: string,\n\t\tparamName?: string,\n\t\tcontext?: Record<string, unknown>\n\t) {\n\t\treturn new ComputeError(`Unknown paramType: ${paramType}`, ErrorCodes.VALIDATION_ERROR, {\n\t\t\tcontext: { receivedParamType: paramType, paramName, ...context }\n\t\t});\n\t}\n}\n","import { ComputeError, ErrorCodes } from '../errors';\n\n/**\n * Logger interface for structured logging\n *\n * @public Implement this interface to provide custom logging behavior.\n */\nexport interface Logger {\n\tdebug(message: string, ...args: unknown[]): void;\n\tinfo(message: string, ...args: unknown[]): void;\n\twarn(message: string, ...args: unknown[]): void;\n\terror(message: string, ...args: unknown[]): void;\n}\n\n/**\n * No-op logger implementation (default)\n * @internal\n */\nclass NoOpLogger implements Logger {\n\tdebug(): void {}\n\tinfo(): void {}\n\twarn(): void {}\n\terror(): void {}\n}\n\n/**\n * Console logger implementation\n * @internal\n */\nclass ConsoleLogger implements Logger {\n\tdebug(message: string, ...args: unknown[]): void {\n\t\tconsole.debug(message, ...args);\n\t}\n\n\tinfo(message: string, ...args: unknown[]): void {\n\t\tconsole.info(message, ...args);\n\t}\n\n\twarn(message: string, ...args: unknown[]): void {\n\t\tconsole.warn(message, ...args);\n\t}\n\n\terror(message: string, ...args: unknown[]): void {\n\t\tconsole.error(message, ...args);\n\t}\n}\n\n/**\n * Internal logger instance\n * @internal\n */\nlet internalLogger: Logger = new NoOpLogger();\n\n/**\n * Get the current logger instance\n *\n * @returns The current logger instance\n */\nexport function getLogger(): Logger {\n\treturn internalLogger;\n}\n\n/**\n * Set a custom logger instance\n *\n * @public Use this to configure custom logging behavior.\n *\n * @param logger - Custom logger implementation or null to disable logging\n * @throws {ComputeError} `INVALID_CONFIG` if the logger is missing any of\n *   the four required methods — failing here beats a confusing\n *   \"getLogger().debug is not a function\" at some later, unrelated call site.\n *\n * @example\n * ```typescript\n * import { setLogger } from '@selvajs/compute/core';\n *\n * // Enable console logging\n * setLogger(console);\n *\n * // Use a custom logger\n * setLogger({\n *   debug: (msg, ...args) => myLogger.debug(msg, ...args),\n *   info: (msg, ...args) => myLogger.info(msg, ...args),\n *   warn: (msg, ...args) => myLogger.warn(msg, ...args),\n *   error: (msg, ...args) => myLogger.error(msg, ...args)\n * });\n *\n * // Disable logging\n * setLogger(null);\n * ```\n */\nexport function setLogger(logger: Logger | Console | null): void {\n\tif (logger === null) {\n\t\tinternalLogger = new NoOpLogger();\n\t\treturn;\n\t}\n\n\tconst missing = (['debug', 'info', 'warn', 'error'] as const).filter(\n\t\t(method) => typeof (logger as unknown as Record<string, unknown>)[method] !== 'function'\n\t);\n\tif (missing.length > 0) {\n\t\tthrow new ComputeError(\n\t\t\t`Logger is missing required method(s): ${missing.join(', ')}. A logger must implement debug, info, warn and error.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { missingMethods: missing } }\n\t\t);\n\t}\n\n\tinternalLogger = logger as Logger;\n}\n\n/**\n * Enable debug logging to console\n *\n * @public Convenience method to enable console logging.\n *\n * @example\n * ```typescript\n * import { enableDebugLogging } from '@selvajs/compute/core';\n *\n * enableDebugLogging();\n * ```\n */\nexport function enableDebugLogging(): void {\n\tsetLogger(new ConsoleLogger());\n}\n","/**\n * Case-insensitive single-key reader for wire payloads.\n *\n * The Rhino Compute family serializes the same logical field with different\n * casing depending on the server branch:\n *\n * - mcneel 8.x / 9.x: the IO schema is PascalCase (`ParamType`, `Default`,\n *   `InnerTree`, …) because those C# classes carry no `[JsonProperty]`.\n * - VektorNode Compute8: the IO schema is camelCase (`paramType`, `default`,\n *   …) because the fork added `[JsonProperty(\"camelCase\")]`, BUT the nested\n *   `default` DataTree wrapper stays PascalCase (`ParamName` / `InnerTree`)\n *   since `Resthopper.IO.DataTree` is an external type the fork can't attribute.\n *\n * So a single response can mix casings, and which casing a given field uses\n * depends on the server branch. Rather than deep-camelCasing the whole payload\n * (the old `camelcaseKeys` approach — which corrupted user-authored value-list\n * label keys and item `data` JSON), read the specific fields we care about\n * case-insensitively and leave everything else verbatim.\n *\n * Prefers an exact-case match when present, then falls back to the first\n * case-insensitive match. Returns `undefined` when no key matches.\n *\n * @param obj - The source object (any non-object input yields `undefined`).\n * @param name - The logical field name, in any casing.\n */\nexport function readField<T = unknown>(obj: unknown, name: string): T | undefined {\n\tif (!obj || typeof obj !== 'object') return undefined;\n\n\tconst record = obj as Record<string, unknown>;\n\t// Own-property check to avoid prototype chain lookups (see lowerKeyMap for full explanation).\n\tif (hasOwn(record, name)) return record[name] as T;\n\n\tconst key = lowerKeyMap(record).get(name.toLowerCase());\n\treturn key === undefined ? undefined : (record[key] as T);\n}\n\nconst hasOwn = (record: object, key: string): boolean =>\n\tObject.prototype.hasOwnProperty.call(record, key);\n\n/**\n * True when `obj` has a key matching `name` (case-insensitively). Distinguishes\n * \"field present but value is null/undefined\" from \"field absent\" — needed where\n * presence itself carries meaning (e.g. an `innerTree` that exists but is empty).\n */\nexport function hasField(obj: unknown, name: string): boolean {\n\tif (!obj || typeof obj !== 'object') return false;\n\tconst record = obj as Record<string, unknown>;\n\t// Own-property check only.\n\tif (hasOwn(record, name)) return true;\n\treturn lowerKeyMap(record).has(name.toLowerCase());\n}\n\n/**\n * Per-object cache of lowercased key → actual key. Invalidated by key-count changes.\n * Known limitation: key replacement without count change goes undetected (acceptable\n * since this mutation pattern doesn't occur in this codebase).\n */\nconst lowerKeyCache = new WeakMap<object, { keyCount: number; map: Map<string, string> }>();\n\nfunction lowerKeyMap(record: Record<string, unknown>): Map<string, string> {\n\tconst keys = Object.keys(record);\n\tconst cached = lowerKeyCache.get(record);\n\tif (cached && cached.keyCount === keys.length) return cached.map;\n\n\tconst map = new Map<string, string>();\n\tfor (const key of keys) {\n\t\tconst lower = key.toLowerCase();\n\t\tif (!map.has(lower)) map.set(lower, key);\n\t}\n\tlowerKeyCache.set(record, { keyCount: keys.length, map });\n\treturn map;\n}\n","import { ComputeError, ErrorCodes } from '@/core/errors';\n\n/**\n * The public McNeel endpoint's host — the default blocked host; users must point at\n * their own server. Compared against the parsed hostname lowercased and with any\n * trailing dot stripped, so the FQDN form (`compute.rhino3d.com.`) can't bypass it.\n * Known limitation: the endpoint's raw IP is not blocked — it sits behind a load\n * balancer with no single stable, verifiable address to pin.\n */\nexport const DEFAULT_BLOCKED_HOST = 'compute.rhino3d.com';\n\nexport interface ValidateServerUrlOptions {\n\t/**\n\t * Hostnames rejected as a `serverUrl` — a backend's shared public endpoint,\n\t * which callers must not point at. Defaults to `[DEFAULT_BLOCKED_HOST]`; pass\n\t * `[]` to block nothing. Compared lowercased with any trailing dot stripped.\n\t */\n\tblockedHosts?: readonly string[];\n}\n\n/**\n * Validate and normalize a compute `serverUrl`.\n *\n * This is the single source of truth for \"is this a usable server URL?\" — both\n * `GrasshopperClient` (via `normalizeComputeConfig`) and the standalone-exported\n * `ComputeServerStats` constructor delegate here, so a given URL is accepted or\n * rejected identically no matter which entry point a caller uses.\n *\n * Rules (all enforced, on the *trimmed* input — the trimmed form is what's\n * returned, so no stray whitespace survives into later `fetch` calls):\n * - non-empty (after trim)\n * - `http://` or `https://` scheme (case-insensitive, per RFC 3986)\n * - parseable by `new URL()`\n * - no embedded credentials (`http://user:pass@host`) — `fetch`/`new Request`\n *   reject credentialed URLs at runtime, so they must fail here instead\n * - no query string or fragment — endpoint paths are appended to this URL\n *   (`${serverUrl}/version`), which a `?…` or `#…` suffix would corrupt\n * - not a blocked host (by default the public McNeel endpoint) — compared by\n *   parsed hostname (lowercased, trailing dot stripped), so scheme, casing, port,\n *   path, trailing-slash, or FQDN-dot variants can't slip past the block\n *\n * @param raw - The candidate server URL.\n * @param options - Override the blocked-host list for a non-Rhino backend.\n * @returns The trimmed, normalized URL with any trailing slashes removed.\n * @throws {ComputeError} `INVALID_CONFIG` if any rule fails.\n */\nexport function validateServerUrl(raw: string, options?: ValidateServerUrlOptions): string {\n\tconst trimmed = raw?.trim() ?? '';\n\tif (!trimmed) {\n\t\tthrow new ComputeError('serverUrl is required', ErrorCodes.INVALID_CONFIG, {\n\t\t\tcontext: { receivedServerUrl: raw }\n\t\t});\n\t}\n\n\tif (!/^https?:\\/\\//i.test(trimmed)) {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must start with \"http://\" or \"https://\". ` +\n\t\t\t\t`For example: \"http://localhost:5000\" or \"https://example.com\"`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(trimmed);\n\t} catch (err) {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must be a valid URL. ` +\n\t\t\t\t`Received error: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{\n\t\t\t\tcontext: { receivedServerUrl: raw },\n\t\t\t\toriginalError: err instanceof Error ? err : undefined\n\t\t\t}\n\t\t);\n\t}\n\n\tif (parsed.username !== '' || parsed.password !== '') {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must not embed credentials (user:pass@host) — ` +\n\t\t\t\t`fetch rejects credentialed URLs at request time. Pass the API key separately.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\t// String check rather than parsed.search/hash: a bare trailing \"?\" or \"#\"\n\t// parses to an empty search/hash but still corrupts endpoint concatenation.\n\tif (trimmed.includes('?') || trimmed.includes('#')) {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must not contain a query string or fragment — ` +\n\t\t\t\t`endpoint paths are appended to this URL (e.g. \"\\${serverUrl}/version\"), ` +\n\t\t\t\t`and a \"?\" or \"#\" suffix would corrupt every request path.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\t// Lowercase + strip any trailing dot so the FQDN form can't bypass the block.\n\tconst hostname = parsed.hostname.toLowerCase().replace(/\\.+$/, '');\n\tconst blocked = (options?.blockedHosts ?? [DEFAULT_BLOCKED_HOST]).map((h) =>\n\t\th.toLowerCase().replace(/\\.+$/, '')\n\t);\n\tif (blocked.includes(hostname)) {\n\t\tthrow new ComputeError(\n\t\t\t'serverUrl must be set to your Compute server URL. The shared public endpoint is not allowed.',\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\treturn trimmed.replace(/\\/+$/, '');\n}\n","/**\n * Why a liveness probe failed, and whether waiting can change the answer.\n *\n * `retryable` is the whole point: a powered-off VM and a booting one both read\n * as \"offline\" from a single probe, but only one of them will ever come up. A\n * caller that can't tell them apart has to assume the optimistic case and burn\n * its full retry window on a machine that is simply off.\n */\nexport type ProbeVerdict =\n\t/** Nothing is listening on the port — the host answered, and said no. */\n\t| 'refused'\n\t/** The hostname does not resolve. */\n\t| 'dns'\n\t/** No answer within the timeout: a booting host swallows packets rather than refusing them. */\n\t| 'timeout'\n\t/** The server answered, but rejected the probe's credentials (401/403). */\n\t| 'unauthorized'\n\t/** The server answered with a non-2xx that isn't an auth rejection. */\n\t| 'http_error'\n\t/** Connection failed in a way we can't attribute. */\n\t| 'unknown';\n\nexport interface ProbeFailure {\n\tverdict: ProbeVerdict;\n\t/** Whether retrying the same probe could plausibly succeed later. */\n\tretryable: boolean;\n\t/** Operator-facing sentence. Never includes the API key. */\n\tsummary: string;\n}\n\n/**\n * `ECONNREFUSED` arrives spelled differently depending on the runtime and how\n * many layers wrapped it: Node puts the code on the error, undici nests it under\n * `cause`, and browsers collapse everything into an opaque \"Failed to fetch\".\n * Matching the stringified probe error covers all of them without the caller\n * having to normalize first.\n */\nconst REFUSED_PATTERN = /ECONNREFUSED|ERR_CONNECTION_REFUSED|connection refused/i;\nconst DNS_PATTERN = /ENOTFOUND|EAI_AGAIN|getaddrinfo|ERR_NAME_NOT_RESOLVED|dns/i;\nconst TIMEOUT_PATTERN =\n\t/TimeoutError|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|ENETUNREACH|abort|timed? ?out/i;\n\n/**\n * Classify a {@link ComputeServerStats.probeServer} result.\n *\n * A DNS failure is treated as retryable even though a missing record won't fix\n * itself: `EAI_AGAIN` is a *timeout* talking to the resolver, and the two are\n * not distinguishable from the error alone. Retrying a genuinely-wrong hostname\n * costs one window; giving up on a resolver hiccup breaks a server that is fine.\n *\n * @param probe - The `{ online, status?, error? }` a probe returned. An `online`\n *   probe is not a failure and yields `null`.\n */\nexport function classifyProbeFailure(probe: {\n\tonline: boolean;\n\tstatus?: number;\n\terror?: string;\n}): ProbeFailure | null {\n\tif (probe.online) return null;\n\n\tif (probe.status !== undefined) {\n\t\tif (probe.status === 401 || probe.status === 403) {\n\t\t\treturn {\n\t\t\t\tverdict: 'unauthorized',\n\t\t\t\tretryable: false,\n\t\t\t\tsummary: `The server rejected the liveness probe with HTTP ${probe.status} — check the API key.`\n\t\t\t};\n\t\t}\n\t\t// 5xx from a proxy in front of a starting child does clear on its own;\n\t\t// a 4xx means we asked for something this server will never serve.\n\t\tconst retryable = probe.status >= 500;\n\t\treturn {\n\t\t\tverdict: 'http_error',\n\t\t\tretryable,\n\t\t\tsummary: retryable\n\t\t\t\t? `The server answered HTTP ${probe.status} — it may still be starting up.`\n\t\t\t\t: `The server answered HTTP ${probe.status}, which will not change on retry.`\n\t\t};\n\t}\n\n\tconst error = probe.error ?? '';\n\n\tif (REFUSED_PATTERN.test(error)) {\n\t\treturn {\n\t\t\tverdict: 'refused',\n\t\t\tretryable: false,\n\t\t\tsummary:\n\t\t\t\t'Connection refused — the host is reachable but nothing is listening on that port. ' +\n\t\t\t\t'Rhino.Compute is not running, or the URL names the wrong port.'\n\t\t};\n\t}\n\n\tif (DNS_PATTERN.test(error)) {\n\t\treturn {\n\t\t\tverdict: 'dns',\n\t\t\tretryable: true,\n\t\t\tsummary: 'The server hostname could not be resolved — check the URL and DNS.'\n\t\t};\n\t}\n\n\tif (TIMEOUT_PATTERN.test(error)) {\n\t\treturn {\n\t\t\tverdict: 'timeout',\n\t\t\tretryable: true,\n\t\t\tsummary:\n\t\t\t\t'No response before the timeout — the machine may be powered off, blocked by a ' +\n\t\t\t\t'firewall, or still booting.'\n\t\t};\n\t}\n\n\treturn {\n\t\tverdict: 'unknown',\n\t\tretryable: true,\n\t\tsummary: error ? `The connection failed: ${error}` : 'The connection failed.'\n\t};\n}\n","import { ComputeError, ErrorCodes } from '../errors';\n\n/** Node's `Buffer` when present (faster path), else `undefined` in browsers/workers. */\nfunction getNodeBuffer(): typeof Buffer | undefined {\n\tconst buf = (globalThis as { Buffer?: typeof Buffer }).Buffer;\n\treturn typeof buf === 'function' ? buf : undefined;\n}\n\n/**\n * Encodes a string to base64 (Node 20+ safe)\n *\n * @internal\n * @param str - String to encode\n * @returns Base64 encoded string\n */\nexport function encodeStringToBase64(str: string): string {\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\treturn Buffer.from(str, 'utf-8').toString('base64');\n\t}\n\t// Browser/worker fallback: UTF-8 encode, then reuse the byte-array encoder.\n\treturn base64ByteArray(new TextEncoder().encode(str));\n}\n\n/**\n * Checks if a string is syntactically valid base64 (strict form: length a multiple of 4).\n * Note: this is validity checking, not detection — `\"test\"` is valid base64 here.\n * To detect whether an untyped string should be treated as base64 content, use {@link detectBase64Payload}.\n *\n * @internal\n * @param str - String to check\n * @returns True if the string is valid base64\n */\nexport function isBase64(str: string): boolean {\n\tif (!str || str.length < 2) return false;\n\t// Length must be a multiple of 4, only alphabet chars + at most 2 trailing '='\n\tif (str.length % 4 !== 0) return false;\n\treturn /^[A-Za-z0-9+/]+={0,2}$/.test(str);\n}\n\n/**\n * Minimum base64 length to treat bare strings as base64 content (excludes padding).\n * 64 chars ≈ 48 decoded bytes — high enough to avoid false positives on human strings.\n *\n * @internal\n */\nexport const BASE64_DETECT_MIN_LENGTH = 64;\n\n/** Lookup for a base64 character's 6-bit value; -1 when not in the alphabet. */\nconst B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\n/**\n * Detects whether an untyped string is base64-encoded content.\n * Uses normalization, length checks, and canonical round-trip validation.\n * Perfect detection is impossible — pass `Uint8Array` instead if exact detection is required.\n *\n * @internal\n * @param str - String to inspect\n * @returns Canonical base64 form when confident, else `null` (treat as plain text)\n */\nexport function detectBase64Payload(str: string): string | null {\n\tif (!str) return null;\n\t// Forgiving-base64 normalization, mirroring decodeBase64ToBinary: strip\n\t// ASCII whitespace, then drop legal trailing padding.\n\tlet data = str.replace(/[\\t\\n\\f\\r ]/g, '');\n\tif (data.length % 4 === 0) data = data.replace(/={1,2}$/, '');\n\n\tconst rem = data.length % 4;\n\tif (rem === 1) return null; // no base64 encoding produces this length\n\tif (data.length < BASE64_DETECT_MIN_LENGTH) return null;\n\tif (!/^[A-Za-z0-9+/]*$/.test(data)) return null;\n\n\t// Canonical round-trip check without decoding: the final character's\n\t// unused low bits must be zero, otherwise re-encoding the decoded bytes\n\t// would not reproduce the input (i.e. it wasn't produced by an encoder).\n\tconst lastValue = B64_ALPHABET.indexOf(data.charAt(data.length - 1));\n\tif (rem === 2 && (lastValue & 0x0f) !== 0) return null;\n\tif (rem === 3 && (lastValue & 0x03) !== 0) return null;\n\n\treturn rem === 0 ? data : data + '=='.slice(0, 4 - rem);\n}\n\n/**\n * Decodes a base64 string to binary data (Uint8Array).\n * Normalizes and validates input per WHATWG forgiving-base64 so both runtimes fail consistently.\n *\n * @param base64File - Base64 encoded string\n * @returns Decoded binary data as Uint8Array\n * @throws {ComputeError} `ENCODING_ERROR` if invalid, or `ENVIRONMENT_ERROR` if the runtime has no decoder\n */\nexport function decodeBase64ToBinary(base64File: string): Uint8Array {\n\t// Forgiving-base64 normalization: strip ASCII whitespace (wrapped /\n\t// pretty-printed payloads), then drop trailing padding only where the spec\n\t// allows it (total length a multiple of 4).\n\tlet data = base64File.replace(/[\\t\\n\\f\\r ]/g, '');\n\tif (data.length % 4 === 0) data = data.replace(/={1,2}$/, '');\n\tif (data.length % 4 === 1 || !/^[A-Za-z0-9+/]*$/.test(data)) {\n\t\tthrow new ComputeError('Invalid base64 input.', ErrorCodes.ENCODING_ERROR, {\n\t\t\tcontext: { inputLength: base64File.length }\n\t\t});\n\t}\n\n\t// Prefer Buffer in Node — it's faster and avoids the latin-1 string detour\n\t// that atob + charCodeAt requires.\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\t// Copy the bytes out of the Buffer: small Buffer.from results are views\n\t\t// over Node's shared 8 KiB pool slab, so returning a view would retain\n\t\t// the whole slab and expose unrelated pooled bytes to any consumer that\n\t\t// touches `.buffer` (re-wrapping, structuredClone, postMessage transfer).\n\t\t// `new Uint8Array(typedArray)` copies into a fresh, exactly-sized buffer.\n\t\treturn new Uint8Array(Buffer.from(data, 'base64'));\n\t}\n\tif (typeof globalThis.atob === 'function') {\n\t\tconst binary = globalThis.atob(data);\n\t\tconst bytes = new Uint8Array(binary.length);\n\t\tfor (let i = 0; i < binary.length; i++) {\n\t\t\tbytes[i] = binary.charCodeAt(i) & 0xff;\n\t\t}\n\t\treturn bytes;\n\t}\n\n\tthrow new ComputeError(\n\t\t'Base64 decoding not supported in this environment.',\n\t\tErrorCodes.ENVIRONMENT_ERROR,\n\t\t{ context: { environmentInfo: 'atob or Buffer not available' } }\n\t);\n}\n\n/**\n * UTF-8 byte length of a string without allocating an encoded copy.\n * Avoids doubling memory on large strings that `TextEncoder.encode` would require.\n *\n * @internal\n */\nexport function utf8ByteLength(str: string): number {\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\treturn Buffer.byteLength(str, 'utf-8');\n\t}\n\tlet bytes = 0;\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst code = str.charCodeAt(i);\n\t\tif (code < 0x80) {\n\t\t\tbytes += 1;\n\t\t} else if (code < 0x800) {\n\t\t\tbytes += 2;\n\t\t} else if (\n\t\t\tcode >= 0xd800 &&\n\t\t\tcode <= 0xdbff &&\n\t\t\ti + 1 < str.length &&\n\t\t\t(str.charCodeAt(i + 1) & 0xfc00) === 0xdc00\n\t\t) {\n\t\t\t// Surrogate pair → one 4-byte code point; lone surrogates fall through\n\t\t\t// to 3 bytes (the replacement-character encoding TextEncoder emits).\n\t\t\tbytes += 4;\n\t\t\ti++;\n\t\t} else {\n\t\t\tbytes += 3;\n\t\t}\n\t}\n\treturn bytes;\n}\n\n/**\n * Encodes binary data (Uint8Array) to base64 string.\n * Uses Node's `Buffer` when available, falls back to `btoa` in browsers/workers.\n *\n * @internal\n */\nexport function base64ByteArray(bytes: Uint8Array): string {\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\treturn Buffer.from(bytes).toString('base64');\n\t}\n\tif (typeof globalThis.btoa === 'function') {\n\t\t// Encode chunk-by-chunk and join the base64 pieces, instead of building\n\t\t// one full-input latin-1 string and btoa-ing it (which peaked at ~3×\n\t\t// input memory). The chunk size MUST be a multiple of 3 bytes so every\n\t\t// non-final chunk encodes to whole base64 quanta with no padding —\n\t\t// concatenating the pieces is then byte-for-byte identical to encoding\n\t\t// the whole input at once. 32766 = 3 × 10922, and stays well under the\n\t\t// fromCharCode.apply argument-count limit.\n\t\tconst CHUNK = 32766;\n\t\tconst parts: string[] = [];\n\t\tfor (let i = 0; i < bytes.length; i += CHUNK) {\n\t\t\t// A Uint8Array subarray is array-like, so pass it straight to\n\t\t\t// fromCharCode.apply — no need to copy it into a plain Array first.\n\t\t\tparts.push(\n\t\t\t\tglobalThis.btoa(\n\t\t\t\t\tString.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK) as unknown as number[])\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\treturn parts.join('');\n\t}\n\tthrow new ComputeError(\n\t\t'Base64 encoding not supported in this environment.',\n\t\tErrorCodes.ENVIRONMENT_ERROR,\n\t\t{ context: { environmentInfo: 'btoa or Buffer not available' } }\n\t);\n}\n","import { getLogger } from '../utils/logger';\n\n/**\n * Debug-gated log. Every transport module routes through this rather than calling the logger\n * directly, so `config.debug` is the single switch for the whole request path.\n */\nexport function log(message: string, debug?: boolean): void {\n\tif (debug) getLogger().debug(message);\n}\n","import { getLogger } from '../utils/logger';\n\nimport type { ComputeConfig } from '../types';\n\nexport function buildUrl(endpoint: string, serverUrl: string): string {\n\tconst base = serverUrl.replace(/\\/+$/, '');\n\tconst path = endpoint.replace(/^\\/+/, '');\n\treturn `${base}/${path}`;\n}\n\nexport function isLocalhost(serverUrl: string): boolean {\n\ttry {\n\t\t// `hostname` (not `host`) strips the port; IPv6 hostnames keep their\n\t\t// brackets, so `http://[::1]:6500` yields `[::1]`.\n\t\tconst hostname = new URL(serverUrl).hostname.toLowerCase();\n\t\treturn hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';\n\t} catch {\n\t\treturn /(localhost|127\\.0\\.0\\.1|\\[::1\\])/i.test(serverUrl);\n\t}\n}\n\n/** Server URLs already warned about missing auth — warn once per server, not per request. */\nconst warnedNoAuth = new Set<string>();\n\n/** Header name rhino.compute reads the API key from. Override via `ComputeConfig.apiKeyHeader`. */\nexport const DEFAULT_API_KEY_HEADER = 'RhinoComputeKey';\n\nexport function buildHeaders(requestId: string, config: ComputeConfig): HeadersInit {\n\tconst headers: HeadersInit = {\n\t\t// Caller headers first so the transport's own headers below OVERWRITE them —\n\t\t// a caller can never clobber the request id, content type, or auth.\n\t\t...config.headers,\n\t\t'X-Request-ID': requestId,\n\t\t'Content-Type': 'application/json',\n\t\t...(config.authToken && { Authorization: config.authToken }),\n\t\t...(config.apiKey && { [config.apiKeyHeader ?? DEFAULT_API_KEY_HEADER]: config.apiKey })\n\t};\n\n\tif (\n\t\t!config.apiKey &&\n\t\t!config.authToken &&\n\t\t!warnedNoAuth.has(config.serverUrl) &&\n\t\t!isLocalhost(config.serverUrl)\n\t) {\n\t\twarnedNoAuth.add(config.serverUrl);\n\t\tgetLogger().warn(\n\t\t\t`⚠️ [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)`\n\t\t);\n\t}\n\n\treturn headers;\n}\n\nexport function generateRequestId(): string {\n\treturn `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\n}\n","import { log } from './log';\n\nimport type { ServerTiming } from '../types';\n\n/**\n * Parse a `Server-Timing` header value into typed durations (ms).\n *\n * Header grammar (RFC 9110 §10.1.10), as emitted by the solve endpoint:\n *   `decode;dur=3, solve;dur=120, encode;dur=8`\n *\n * Returns null when the header is absent or carries no recognizable metric, so\n * the caller can skip the callback entirely.\n *\n * @internal exported for tests\n */\nexport function parseServerTiming(headerValue: string | null): ServerTiming | null {\n\tif (!headerValue) return null;\n\tconst timing: ServerTiming = { raw: headerValue };\n\tlet sawMetric = false;\n\tfor (const part of headerValue.split(',')) {\n\t\tconst [name, ...params] = part.trim().split(';');\n\t\tconst durParam = params.find((p) => p.trim().toLowerCase().startsWith('dur'));\n\t\tif (!durParam) continue;\n\t\tconst dur = Number(durParam.split('=')[1]);\n\t\tif (!Number.isFinite(dur)) continue;\n\t\tconst key = name.trim().toLowerCase();\n\t\tif (key === 'decode' || key === 'solve' || key === 'encode') {\n\t\t\ttiming[key] = dur;\n\t\t\tsawMetric = true;\n\t\t}\n\t}\n\treturn sawMetric ? timing : null;\n}\n\n/**\n * Surface the server's per-request timing breakdown (if it sent one and a\n * caller is listening). Best-effort: a throwing callback must not fail the\n * request. Called on the success path AND the 500-with-values partial-success\n * path — solves that completed with Grasshopper errors carry real timings too.\n */\nexport function fireServerTiming(\n\tresponse: Response,\n\trequestId: string,\n\tonServerTiming: ((timing: ServerTiming, requestId: string) => void) | undefined,\n\tdebug?: boolean\n): void {\n\tif (!onServerTiming) return;\n\tconst timing = parseServerTiming(response.headers.get('Server-Timing'));\n\tif (!timing) return;\n\ttry {\n\t\tonServerTiming(timing, requestId);\n\t} catch (err) {\n\t\tif (debug) log(`   onServerTiming callback threw: ${err}`, true);\n\t}\n}\n","/**\n * Side-channel carrying a response's wire size (its JSON text length) alongside\n * the parsed object, so downstream caches can budget by bytes without\n * re-serializing (audit C2/C3 — the response tree can be hundreds of MB, and\n * every extra `JSON.stringify` pass over it is a real cost).\n *\n * A `WeakMap` rather than a property on the response: the response type is the\n * server's schema, and an extra enumerable field would leak into every\n * `JSON.stringify(response)` on the way back out to clients. Derived copies\n * (e.g. the `algo`-stripped shallow copy in `runSolve`) must re-register — the\n * hint follows object identity, not content.\n *\n * The size is `text.length` (UTF-16 code units), not strict UTF-8 bytes —\n * compute responses are ASCII-dominated JSON (base64 + numerals), so the two\n * are interchangeable for budgeting purposes and `.length` is free.\n */\n\nconst wireSizes = new WeakMap<object, number>();\n\n/** Record `response`'s wire size. No-op for non-object/null values. */\nexport function setResponseWireSize(response: unknown, size: number): void {\n\tif (typeof response !== 'object' || response === null) return;\n\twireSizes.set(response, size);\n}\n\n/** The wire size recorded for `response`, or undefined when never registered. */\nexport function getResponseWireSize(response: unknown): number | undefined {\n\tif (typeof response !== 'object' || response === null) return undefined;\n\treturn wireSizes.get(response);\n}\n","import { ComputeError, ErrorCodes, type ErrorCode } from '../errors';\nimport { readField } from '../utils/read-field';\nimport { log } from './log';\nimport { fireServerTiming } from './server-timing';\nimport { setResponseWireSize } from './wire-size';\n\nimport type { ServerErrorCodeMap, ServerTiming } from '../types';\n\n/** Upper bound for the raw server body stored on error `context.responseBody`. */\nconst MAX_CONTEXT_BODY_CHARS = 4096;\n\n/** Truncate a server body for storage on error context — bounded, with an honest marker. */\nfunction truncateBody(body: string): string {\n\tif (body.length <= MAX_CONTEXT_BODY_CHARS) return body;\n\treturn `${body.slice(0, MAX_CONTEXT_BODY_CHARS)}… [truncated ${body.length - MAX_CONTEXT_BODY_CHARS} chars]`;\n}\n\nexport function throwHttpError(\n\tresponse: Response,\n\tfullUrl: string,\n\trequestId: string,\n\trequestSize: number,\n\tserverUrl: string,\n\terrorBody: string,\n\tserverCode?: string,\n\trawBody?: string,\n\tserverErrorCodes?: ServerErrorCodeMap\n): never {\n\tconst { status, statusText } = response;\n\n\tconst responseHeaders: Record<string, string> = {};\n\tresponse.headers.forEach((value, key) => {\n\t\tresponseHeaders[key] = value;\n\t});\n\n\tconst trimmed = errorBody.trim();\n\tconst bodyHint = trimmed ? ` — ${trimmed.slice(0, 200)}${trimmed.length > 200 ? '…' : ''}` : '';\n\n\t// context.responseBody holds the RAW server body (what actually came over the\n\t// wire), bounded to MAX_CONTEXT_BODY_CHARS so a huge body isn't pinned for the\n\t// error's lifetime. `errorBody` may have been rewritten into a synthesized\n\t// message (500 exception shape) — that goes in the message, not the context.\n\tconst storedBody = rawBody ?? errorBody;\n\tconst context = {\n\t\turl: fullUrl,\n\t\trequestId,\n\t\tmethod: 'POST',\n\t\trequestSize,\n\t\tserverUrl,\n\t\tresponseBody: storedBody ? truncateBody(storedBody) : undefined,\n\t\tresponseHeaders\n\t};\n\n\tconst errorMap: Record<number, { message: string; code: ErrorCode }> = {\n\t\t400: {\n\t\t\tmessage: `Bad request: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.VALIDATION_ERROR\n\t\t},\n\t\t401: { message: `HTTP ${status}: ${statusText}${bodyHint}`, code: ErrorCodes.AUTH_ERROR },\n\t\t403: { message: `HTTP ${status}: ${statusText}${bodyHint}`, code: ErrorCodes.AUTH_ERROR },\n\t\t404: { message: `Endpoint not found: ${fullUrl}`, code: ErrorCodes.NOT_FOUND },\n\t\t413: {\n\t\t\tmessage: `Request too large: ${(requestSize / 1024).toFixed(2)}KB`,\n\t\t\tcode: ErrorCodes.VALIDATION_ERROR\n\t\t},\n\t\t429: { message: `Rate limit exceeded${bodyHint}`, code: ErrorCodes.RATE_LIMIT },\n\t\t500: { message: `Server error: ${statusText}${bodyHint}`, code: ErrorCodes.COMPUTATION_ERROR },\n\t\t502: {\n\t\t\tmessage: `Bad gateway: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.NETWORK_ERROR\n\t\t},\n\t\t503: {\n\t\t\tmessage: `Service unavailable: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.NETWORK_ERROR\n\t\t},\n\t\t504: {\n\t\t\tmessage: `Service unavailable: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.NETWORK_ERROR\n\t\t}\n\t};\n\n\tconst error = errorMap[status] || {\n\t\tmessage: `HTTP ${status}: ${statusText}${bodyHint}`,\n\t\tcode: ErrorCodes.UNKNOWN_ERROR\n\t};\n\n\t// A machine code in the server's error body outranks the status-based mapping:\n\t// it's stable across the server's production message-scrubbing, where the human\n\t// message is replaced with a generic string. Keep the status-derived message\n\t// for context.\n\tconst code = mapServerErrorCode(serverCode, serverErrorCodes) ?? error.code;\n\n\tthrow new ComputeError(error.message, code, { statusCode: status, context });\n}\n\n/**\n * Map a server-supplied error code (from the JSON error body's `code` field) to\n * one of our {@link ErrorCodes}. Returns `undefined` for an absent or unknown\n * code so the caller falls back to its status-based mapping.\n *\n * Which wire codes exist is backend-specific, so the table comes from the caller\n * ({@link ComputeConfig.serverErrorCodes}) — core does not know any of them.\n */\nexport function mapServerErrorCode(\n\tserverCode?: string,\n\tserverErrorCodes?: ServerErrorCodeMap\n): ErrorCode | undefined {\n\tif (!serverCode || !serverErrorCodes) return undefined;\n\treturn serverErrorCodes[serverCode];\n}\n\nexport async function handleResponse(\n\tresponse: Response,\n\tfullUrl: string,\n\trequestId: string,\n\trequestSize: number,\n\tserverUrl: string,\n\tstartTime: number,\n\tdebug?: boolean,\n\tonServerTiming?: (timing: ServerTiming, requestId: string) => void,\n\tserverErrorCodes?: ServerErrorCodeMap\n): Promise<any> {\n\tconst responseTime = Math.round(performance.now() - startTime);\n\n\tif (!response.ok) {\n\t\t// Read body once and reuse. `rawBody` stays what came over the wire (stored\n\t\t// on error context); `errorBody` may be rewritten into a friendlier message.\n\t\tconst rawBody = await response.text();\n\t\tlet errorBody = rawBody;\n\n\t\t// Enhanced logging for errors\n\t\tif (debug) {\n\t\t\tlog(\n\t\t\t\t`❌ Request [${requestId}] failed with HTTP ${response.status} in ${responseTime}ms`,\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tlog(`   URL: ${fullUrl}`, true);\n\t\t\tlog(`   Status: ${response.status} ${response.statusText}`, true);\n\t\t\tif (errorBody) {\n\t\t\t\tlog(\n\t\t\t\t\t`   Response body: ${errorBody.substring(0, 500)}${errorBody.length > 500 ? '...' : ''}`,\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// A machine-readable code the server may tag onto its error body. Unlike the\n\t\t// human `message`, it isn't scrubbed in the server's production (non-debug)\n\t\t// mode, so it's the reliable signal for classifying the error (see\n\t\t// throwHttpError → mapServerErrorCode). It can ride any error status, not\n\t\t// just 500, so read it here.\n\t\tlet serverCode: string | undefined;\n\n\t\ttry {\n\t\t\tconst parsedForCode = JSON.parse(errorBody);\n\t\t\tif (typeof parsedForCode?.code === 'string') serverCode = parsedForCode.code;\n\t\t} catch {\n\t\t\t// Non-JSON body — nothing to extract.\n\t\t}\n\n\t\t// Check if it's a valid compute response with errors/warnings\n\t\tif (response.status === 500) {\n\t\t\ttry {\n\t\t\t\tconst parsed = JSON.parse(errorBody);\n\t\t\t\t// If it has values, it's a partial success with errors. Read the fields\n\t\t\t\t// case-insensitively: `values`/`errors`/`warnings` arrive PascalCase from\n\t\t\t\t// stock mcneel servers — a casing miss here would throw the partial\n\t\t\t\t// values away as a hard failure.\n\t\t\t\tconst values = readField(parsed, 'values');\n\t\t\t\tconst solveErrors = readField<unknown[]>(parsed, 'errors');\n\t\t\t\tconst solveWarnings = readField<unknown[]>(parsed, 'warnings');\n\t\t\t\tif (values && (solveErrors || solveWarnings)) {\n\t\t\t\t\tsetResponseWireSize(parsed, errorBody.length);\n\t\t\t\t\tif (debug) {\n\t\t\t\t\t\tlog(\n\t\t\t\t\t\t\t`⚠️ Request [${requestId}] completed with solver errors in ${responseTime}ms`,\n\t\t\t\t\t\t\ttrue\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (solveErrors && solveErrors.length > 0) {\n\t\t\t\t\t\t\tlog(`   Errors: ${JSON.stringify(solveErrors, null, 2)}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (solveWarnings && solveWarnings.length > 0) {\n\t\t\t\t\t\t\tlog(`   Warnings: ${JSON.stringify(solveWarnings, null, 2)}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tfireServerTiming(response, requestId, onServerTiming, debug);\n\t\t\t\t\treturn parsed;\n\t\t\t\t}\n\n\t\t\t\t// Raw server-side exception. The Compute8 server's exception handler\n\t\t\t\t// (compute.geometry Startup.cs) emits:\n\t\t\t\t//   { error: \"Internal Server Error\", message: \"<category>: <detail>\",\n\t\t\t\t//     stackTrace?: string[] }   // stackTrace only when Config.Debug\n\t\t\t\t// The actionable part is `message` — surface it, with the optional\n\t\t\t\t// stack appended for debugging. We prefer `message`/`error` (current\n\t\t\t\t// server) and keep `Message`/`ExceptionType`/`StackTrace` (old\n\t\t\t\t// PascalCase .NET shape) as a back-compat fallback so an older server\n\t\t\t\t// still produces a useful message.\n\t\t\t\tconst serverMessage =\n\t\t\t\t\t(typeof parsed?.message === 'string' && parsed.message) ||\n\t\t\t\t\t(typeof parsed?.Message === 'string' && parsed.Message) ||\n\t\t\t\t\t'';\n\t\t\t\tconst exceptionType =\n\t\t\t\t\t(typeof parsed?.ExceptionType === 'string' && parsed.ExceptionType) || '';\n\t\t\t\tconst stack = parsed?.stackTrace ?? parsed?.StackTrace;\n\t\t\t\tconst stackStr = Array.isArray(stack) ? stack.join('\\n') : stack || '';\n\n\t\t\t\tif (serverMessage) {\n\t\t\t\t\t// Don't repeat the generic \"Internal Server Error\" label when the\n\t\t\t\t\t// message already carries the real detail.\n\t\t\t\t\tconst prefix = exceptionType ? `${exceptionType}: ` : '';\n\t\t\t\t\terrorBody = `${prefix}${serverMessage}${stackStr ? `\\n${stackStr}` : ''}`;\n\t\t\t\t} else if (parsed?.error) {\n\t\t\t\t\terrorBody =\n\t\t\t\t\t\ttypeof parsed.error === 'string' ? parsed.error : JSON.stringify(parsed.error, null, 2);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tif (debug) {\n\t\t\t\t\tlog(`   Failed to parse error body as JSON: ${e}`, true);\n\t\t\t\t}\n\t\t\t\t// Not valid JSON, proceed with HTTP error\n\t\t\t}\n\t\t}\n\n\t\tthrowHttpError(\n\t\t\tresponse,\n\t\t\tfullUrl,\n\t\t\trequestId,\n\t\t\trequestSize,\n\t\t\tserverUrl,\n\t\t\terrorBody,\n\t\t\tserverCode,\n\t\t\trawBody,\n\t\t\tserverErrorCodes\n\t\t);\n\t}\n\n\tlog(`✅ Request [${requestId}] completed in ${responseTime}ms`, debug);\n\n\tfireServerTiming(response, requestId, onServerTiming, debug);\n\n\ttry {\n\t\t// text-then-parse (what `response.json()` does internally) so the body's\n\t\t// wire size rides along with the parsed object — downstream byte-budgeted\n\t\t// caches read it instead of re-serializing a potentially huge tree.\n\t\tconst rawBody = await response.text();\n\t\tconst parsed = JSON.parse(rawBody);\n\t\tsetResponseWireSize(parsed, rawBody.length);\n\t\treturn parsed;\n\t} catch (error) {\n\t\t// Classify by the declared Content-Type (issue 87). A 2xx that DECLARES a\n\t\t// non-JSON body (HTML from a captive portal, a reverse-proxy login page, a\n\t\t// misconfigured endpoint) is deterministic — refetching returns the same\n\t\t// page — so fail immediately with INVALID_RESPONSE (never retried). A body\n\t\t// that fails to parse under a JSON (or absent) Content-Type means the\n\t\t// stream was likely cut mid-body — as transient as any network error — and\n\t\t// keeps the retryable NETWORK_ERROR classification.\n\t\tconst contentType = (response.headers.get('Content-Type') ?? '').toLowerCase();\n\t\tconst declaredNonJson = contentType !== '' && !contentType.includes('json');\n\t\tif (declaredNonJson) {\n\t\t\tthrow new ComputeError(\n\t\t\t\t`Server returned a non-JSON response (Content-Type: ${contentType}) — check the server URL / proxy configuration`,\n\t\t\t\tErrorCodes.INVALID_RESPONSE,\n\t\t\t\t{\n\t\t\t\t\tstatusCode: response.status,\n\t\t\t\t\tcontext: { url: fullUrl, requestId, contentType },\n\t\t\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\tthrow new ComputeError('Failed to parse JSON response', ErrorCodes.NETWORK_ERROR, {\n\t\t\tstatusCode: response.status,\n\t\t\tcontext: {\n\t\t\t\turl: fullUrl,\n\t\t\t\trequestId\n\t\t\t},\n\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t});\n\t}\n}\n","import type { RetryPolicy } from '../types';\n\nconst DEFAULT_RETRY: Required<RetryPolicy> = {\n\tattempts: 0,\n\tbaseDelayMs: 500,\n\tmaxDelayMs: 30_000,\n\tretryOn429: true\n};\n\nexport const RETRYABLE_STATUS = new Set([502, 503, 504]);\n\n/**\n * Absolute ceiling for a server-supplied `Retry-After` wait. The server's\n * stated window wins over `retryPolicy.maxDelayMs` (retrying earlier all but\n * guarantees another 429), but a bad/hostile header must not park the client\n * for minutes — anything above this cap is clamped.\n */\nexport const RETRY_AFTER_CAP_MS = 60_000;\n\nexport function resolveRetryPolicy(policy: RetryPolicy | undefined): Required<RetryPolicy> {\n\tif (!policy) return DEFAULT_RETRY;\n\treturn {\n\t\tattempts: policy.attempts ?? DEFAULT_RETRY.attempts,\n\t\tbaseDelayMs: policy.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,\n\t\tmaxDelayMs: policy.maxDelayMs ?? DEFAULT_RETRY.maxDelayMs,\n\t\tretryOn429: policy.retryOn429 ?? DEFAULT_RETRY.retryOn429\n\t};\n}\n\n/**\n * Parse a Retry-After header value (seconds-int or HTTP-date) into ms.\n * Returns null if the header is missing or unparseable.\n */\nexport function parseRetryAfter(headerValue: string | null): number | null {\n\tif (!headerValue) return null;\n\tconst seconds = Number(headerValue);\n\tif (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;\n\tconst dateMs = Date.parse(headerValue);\n\tif (Number.isFinite(dateMs)) {\n\t\tconst delta = dateMs - Date.now();\n\t\treturn delta > 0 ? delta : 0;\n\t}\n\treturn null;\n}\n\nexport function backoffDelay(attempt: number, policy: Required<RetryPolicy>): number {\n\tconst exponential = policy.baseDelayMs * Math.pow(2, attempt);\n\tconst jitter = Math.random() * policy.baseDelayMs;\n\treturn Math.min(exponential + jitter, policy.maxDelayMs);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new DOMException('Aborted', 'AbortError'));\n\t\t\treturn;\n\t\t}\n\t\tconst id = setTimeout(() => {\n\t\t\tsignal?.removeEventListener('abort', onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(id);\n\t\t\treject(new DOMException('Aborted', 'AbortError'));\n\t\t};\n\t\tsignal?.addEventListener('abort', onAbort, { once: true });\n\t});\n}\n","/**\n * Compose a caller-supplied AbortSignal with an optional timeout. Returns a\n * combined signal, or `undefined` if neither was given.\n *\n * Uses `AbortSignal.timeout` (not setTimeout) so the timer is not throttled\n * when the tab is hidden. Falls back to a manual timer for older runtimes.\n *\n * @internal exported for tests\n */\nexport function composeSignal(\n\tcallerSignal: AbortSignal | undefined,\n\ttimeoutMs: number | undefined\n): { signal: AbortSignal | undefined; cleanup: () => void } {\n\tconst noCleanup = () => {};\n\tconst wantsTimeout = typeof timeoutMs === 'number' && timeoutMs > 0;\n\n\tif (!callerSignal && !wantsTimeout) return { signal: undefined, cleanup: noCleanup };\n\tif (callerSignal && !wantsTimeout) return { signal: callerSignal, cleanup: noCleanup };\n\n\tconst supportsTimeout =\n\t\ttypeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function';\n\n\t// Timeout only: nothing is registered on a caller signal, so there is nothing to clean up on\n\t// the modern path (the pending timer is small and self-expires at timeoutMs).\n\tif (!callerSignal) {\n\t\tif (supportsTimeout) return { signal: AbortSignal.timeout(timeoutMs!), cleanup: noCleanup };\n\t\tconst ctrl = new AbortController();\n\t\tconst id = setTimeout(() => ctrl.abort(), timeoutMs);\n\t\treturn { signal: ctrl.signal, cleanup: () => clearTimeout(id) };\n\t}\n\n\t// Caller signal + timeout: composed manually rather than with AbortSignal.any — `any` offers no\n\t// way to unregister its dependent link on the caller's signal, so an app reusing one long-lived\n\t// signal across many solves accumulates a registration per attempt for the full timeoutMs after\n\t// each response (and forever on Node versions with the known AbortSignal.any leak).\n\tconst ctrl = new AbortController();\n\n\tlet timeoutSignal: AbortSignal;\n\tlet timerId: ReturnType<typeof setTimeout> | undefined;\n\tif (supportsTimeout) {\n\t\ttimeoutSignal = AbortSignal.timeout(timeoutMs!);\n\t} else {\n\t\tconst timeoutCtrl = new AbortController();\n\t\ttimerId = setTimeout(() => timeoutCtrl.abort(), timeoutMs);\n\t\ttimeoutSignal = timeoutCtrl.signal;\n\t}\n\n\tconst sources = [callerSignal, timeoutSignal];\n\t// Forward the source's reason so fetch rejects with the right error name\n\t// ('TimeoutError' vs 'AbortError'), matching AbortSignal.any semantics.\n\tconst onAbort = function (this: AbortSignal) {\n\t\tctrl.abort(this.reason);\n\t};\n\n\tfor (const s of sources) {\n\t\tif (s.aborted) {\n\t\t\tctrl.abort(s.reason);\n\t\t\tbreak;\n\t\t}\n\t\ts.addEventListener('abort', onAbort, { once: true });\n\t}\n\n\treturn {\n\t\tsignal: ctrl.signal,\n\t\tcleanup: () => {\n\t\t\tif (timerId !== undefined) clearTimeout(timerId);\n\t\t\tfor (const s of sources) s.removeEventListener('abort', onAbort);\n\t\t}\n\t};\n}\n","import { ComputeError, ErrorCodes } from '../errors';\nimport { utf8ByteLength } from '../utils/encoding';\nimport { log } from './log';\nimport { buildUrl, buildHeaders, generateRequestId } from './request';\nimport { handleResponse } from './response';\nimport {\n\tRETRYABLE_STATUS,\n\tRETRY_AFTER_CAP_MS,\n\tbackoffDelay,\n\tparseRetryAfter,\n\tresolveRetryPolicy,\n\tsleep\n} from './retry';\nimport { composeSignal } from './signal';\n\nimport type { ComputeConfig, RetryPolicy } from '../types';\n\ninterface AttemptContext {\n\tendpoint: string;\n\tbody: string;\n\trequestSize: number;\n\tfullUrl: string;\n\trequestId: string;\n\theaders: HeadersInit;\n\tconfig: ComputeConfig;\n}\n\ninterface AttemptResult {\n\tok: true;\n\tvalue: any;\n}\n\ninterface AttemptRetry {\n\tok: false;\n\tretry: true;\n\tdelayMs: number;\n\tcause: ComputeError;\n}\n\ninterface AttemptFatal {\n\tok: false;\n\tretry: false;\n\tcause: ComputeError;\n}\n\nasync function attemptFetch(\n\tctx: AttemptContext,\n\tretryPolicy: Required<RetryPolicy>,\n\tattempt: number,\n\ttotalAttempts: number\n): Promise<AttemptResult | AttemptRetry | AttemptFatal> {\n\tconst { signal, cleanup } = composeSignal(ctx.config.signal, ctx.config.timeoutMs);\n\tconst startTime = performance.now();\n\n\ttry {\n\t\tconst response = await fetch(ctx.fullUrl, {\n\t\t\tmethod: 'POST',\n\t\t\tbody: ctx.body,\n\t\t\theaders: ctx.headers,\n\t\t\tsignal\n\t\t});\n\n\t\t// 429 with Retry-After or retryable 5xx → maybe retry\n\t\tconst isRetryableStatus =\n\t\t\tRETRYABLE_STATUS.has(response.status) || (retryPolicy.retryOn429 && response.status === 429);\n\n\t\tif (isRetryableStatus && attempt < totalAttempts - 1) {\n\t\t\tconst retryAfterMs = parseRetryAfter(response.headers.get('Retry-After'));\n\t\t\t// The server's stated Retry-After window wins over maxDelayMs — retrying\n\t\t\t// before it all but guarantees another 429 and burns an attempt. It is\n\t\t\t// only clamped by the absolute RETRY_AFTER_CAP_MS safety cap (or by a\n\t\t\t// caller-configured maxDelayMs that is even larger), so a bad header\n\t\t\t// can't force a pathological sleep. backoffDelay already clamps itself.\n\t\t\tconst delayMs =\n\t\t\t\tretryAfterMs !== null\n\t\t\t\t\t? Math.min(retryAfterMs, Math.max(retryPolicy.maxDelayMs, RETRY_AFTER_CAP_MS))\n\t\t\t\t\t: backoffDelay(attempt, retryPolicy);\n\t\t\t// Drain the body so the connection can be reused on the next attempt.\n\t\t\t// On the *final* attempt we deliberately fall through — handleResponse\n\t\t\t// reads the body itself to surface the error context.\n\t\t\tawait response.text().catch(() => {});\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\tretry: true,\n\t\t\t\tdelayMs,\n\t\t\t\tcause: new ComputeError(\n\t\t\t\t\t`HTTP ${response.status} ${response.statusText} (will retry)`,\n\t\t\t\t\tresponse.status === 429 ? ErrorCodes.RATE_LIMIT : ErrorCodes.NETWORK_ERROR,\n\t\t\t\t\t{ statusCode: response.status, context: { requestId: ctx.requestId } }\n\t\t\t\t)\n\t\t\t};\n\t\t}\n\n\t\tconst value = await handleResponse(\n\t\t\tresponse,\n\t\t\tctx.fullUrl,\n\t\t\tctx.requestId,\n\t\t\tctx.requestSize,\n\t\t\tctx.config.serverUrl,\n\t\t\tstartTime,\n\t\t\tctx.config.debug,\n\t\t\tctx.config.onServerTiming,\n\t\t\tctx.config.serverErrorCodes\n\t\t);\n\t\treturn { ok: true, value };\n\t} catch (error) {\n\t\t// Caller-aborted vs timeout-aborted distinction\n\t\tif (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) {\n\t\t\tconst callerAborted = ctx.config.signal?.aborted === true;\n\n\t\t\tif (callerAborted) {\n\t\t\t\t// Caller cancellation is never retried — propagate immediately\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: false,\n\t\t\t\t\tcause: new ComputeError('Request aborted by caller', ErrorCodes.ABORTED, {\n\t\t\t\t\t\tcontext: {\n\t\t\t\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\t\t\t\trequestId: ctx.requestId,\n\t\t\t\t\t\t\trequestSize: ctx.requestSize\n\t\t\t\t\t\t},\n\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t})\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Only claim a timeout when we actually armed one (issue 88). Without a\n\t\t\t// configured timeoutMs, a non-caller abort came from the runtime itself\n\t\t\t// (e.g. an undici socket teardown) — as transient as any network drop, so\n\t\t\t// it stays retryable, but it is NOT a timeout: report it truthfully\n\t\t\t// instead of \"timed out after undefinedms\" / TIMEOUT_ERROR.\n\t\t\tconst timeoutArmed = typeof ctx.config.timeoutMs === 'number' && ctx.config.timeoutMs > 0;\n\t\t\tconst fatal = timeoutArmed\n\t\t\t\t? new ComputeError(\n\t\t\t\t\t\t`Request timed out after ${ctx.config.timeoutMs}ms`,\n\t\t\t\t\t\tErrorCodes.TIMEOUT_ERROR,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcontext: {\n\t\t\t\t\t\t\t\tserverUrl: ctx.config.serverUrl,\n\t\t\t\t\t\t\t\ttimeoutMs: ctx.config.timeoutMs,\n\t\t\t\t\t\t\t\turl: ctx.fullUrl,\n\t\t\t\t\t\t\t\trequestId: ctx.requestId,\n\t\t\t\t\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\t\t\t\t\trequestSize: ctx.requestSize\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t\t}\n\t\t\t\t\t)\n\t\t\t\t: new ComputeError(\n\t\t\t\t\t\t`Request aborted by the runtime (${error.name}): ${error.message}`,\n\t\t\t\t\t\tErrorCodes.NETWORK_ERROR,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcontext: {\n\t\t\t\t\t\t\t\tserverUrl: ctx.config.serverUrl,\n\t\t\t\t\t\t\t\turl: ctx.fullUrl,\n\t\t\t\t\t\t\t\trequestId: ctx.requestId,\n\t\t\t\t\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\t\t\t\t\trequestSize: ctx.requestSize\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t\t}\n\t\t\t\t\t);\n\t\t\tif (attempt < totalAttempts - 1) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: true,\n\t\t\t\t\tdelayMs: backoffDelay(attempt, retryPolicy),\n\t\t\t\t\tcause: fatal\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { ok: false, retry: false, cause: fatal };\n\t\t}\n\n\t\t// Network error (TypeError) — retryable (issue 90).\n\t\t//\n\t\t// Duplicate-POST caveat: a connection reset can strike after the body was\n\t\t// sent, in which case the server may already have executed this POST and a\n\t\t// retry runs it again. Compute solves are deterministic, so a duplicate is\n\t\t// wasted work rather than corruption; RetryPolicy documents the risk and\n\t\t// defaults to attempts: 0.\n\t\t//\n\t\t// In a real browser, a fetch TypeError with no response is most often a\n\t\t// CORS misconfiguration (the browser hides the details by design), so it is\n\t\t// classified CORS_ERROR there to stop callers chasing phantom network\n\t\t// failures. Retries are kept in both environments: the same TypeError is\n\t\t// also what a flaky/offline network produces, and there is no way to tell\n\t\t// them apart.\n\t\tif (error instanceof TypeError) {\n\t\t\tconst inBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';\n\t\t\tconst errorContext = {\n\t\t\t\tserverUrl: ctx.config.serverUrl,\n\t\t\t\turl: ctx.fullUrl,\n\t\t\t\trequestId: ctx.requestId,\n\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\trequestSize: ctx.requestSize\n\t\t\t};\n\t\t\tconst fatal = inBrowser\n\t\t\t\t? new ComputeError(\n\t\t\t\t\t\t`Request failed: ${error.message} — in a browser this usually means a CORS misconfiguration on the server (or a network failure; the browser does not distinguish them)`,\n\t\t\t\t\t\tErrorCodes.CORS_ERROR,\n\t\t\t\t\t\t{ context: errorContext, originalError: error }\n\t\t\t\t\t)\n\t\t\t\t: new ComputeError(`Network error: ${error.message}`, ErrorCodes.NETWORK_ERROR, {\n\t\t\t\t\t\tcontext: errorContext,\n\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t});\n\t\t\tif (attempt < totalAttempts - 1) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: true,\n\t\t\t\t\tdelayMs: backoffDelay(attempt, retryPolicy),\n\t\t\t\t\tcause: fatal\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { ok: false, retry: false, cause: fatal };\n\t\t}\n\t\t// ComputeError thrown from handleResponse — already has full context.\n\t\t// Retryable only if it carries a retryable status code.\n\t\tif (error instanceof ComputeError) {\n\t\t\tconst status = error.statusCode;\n\t\t\t// A 2xx whose body failed to parse UNDER A JSON CONTENT-TYPE\n\t\t\t// (NETWORK_ERROR from handleResponse) means the stream was cut mid-body —\n\t\t\t// as transient as any network error. A 2xx that declared a non-JSON body\n\t\t\t// arrives here as INVALID_RESPONSE (deterministic — captive portal /\n\t\t\t// login page) and deliberately does NOT match: it is never retried.\n\t\t\tconst isTruncatedSuccess =\n\t\t\t\terror.code === ErrorCodes.NETWORK_ERROR &&\n\t\t\t\tstatus !== undefined &&\n\t\t\t\tstatus >= 200 &&\n\t\t\t\tstatus < 300;\n\t\t\tconst retryable =\n\t\t\t\tisTruncatedSuccess ||\n\t\t\t\t(status !== undefined &&\n\t\t\t\t\t(RETRYABLE_STATUS.has(status) || (retryPolicy.retryOn429 && status === 429)));\n\t\t\tif (retryable && attempt < totalAttempts - 1) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: true,\n\t\t\t\t\tdelayMs: backoffDelay(attempt, retryPolicy),\n\t\t\t\t\tcause: error\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { ok: false, retry: false, cause: error };\n\t\t}\n\n\t\t// Unknown — wrap and don't retry\n\t\treturn {\n\t\t\tok: false,\n\t\t\tretry: false,\n\t\t\tcause: new ComputeError(\n\t\t\t\terror instanceof Error ? error.message : String(error),\n\t\t\t\tErrorCodes.UNKNOWN_ERROR,\n\t\t\t\t{\n\t\t\t\t\tcontext: { endpoint: ctx.endpoint, requestId: ctx.requestId },\n\t\t\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t\t\t}\n\t\t\t)\n\t\t};\n\t} finally {\n\t\tcleanup();\n\t}\n}\n\n/**\n * Generic Rhino Compute fetch function.\n * Sends a POST request to any Compute endpoint with pre-prepared arguments.\n *\n * Use this for advanced, low-level control over compute requests. For most use cases, prefer higher-level APIs.\n *\n * The transport is response-type-agnostic: it does not know which response a\n * given endpoint returns. Callers supply the response type via `R` (defaulting\n * to `unknown`, which forces an explicit narrowing before use).\n *\n * Timeout semantics: `config.timeoutMs` is a PER-ATTEMPT timeout, re-armed for\n * every retry. With `retry: { attempts: N }` the worst-case wall clock is\n * `(N + 1) × timeoutMs` plus backoff sleeps (and a server `Retry-After` can\n * stretch a single sleep up to 60s). For a hard overall deadline, pass\n * `config.signal` (e.g. `AbortSignal.timeout(totalMs)`) — a caller abort wins\n * immediately, including during backoff.\n *\n * Retry caveats: requests are POSTs, so a retry after a mid-flight connection\n * loss may re-execute a request the server already ran (see {@link RetryPolicy}).\n * A 2xx response that declares a non-JSON `Content-Type` (captive portal,\n * proxy login page) fails immediately with `INVALID_RESPONSE` and is never\n * retried; a body that fails to parse under a JSON content-type is treated as\n * a truncated stream and is retried.\n *\n * @typeParam R - The expected response shape. The caller names it at the call site.\n * @param endpoint - The Compute API endpoint (e.g., 'grasshopper', 'io', 'mesh').\n * @param args - Pre-prepared arguments for the request body.\n * @param config - Compute configuration (server URL, API key, timeout, debug, retry, signal).\n * @returns The parsed JSON response from the server, typed as `R`.\n *\n * @example\n * // Basic usage for the Grasshopper endpoint:\n * const response = await fetchCompute(\n *   'grasshopper',\n *   { ... },\n *   {\n *     serverUrl: 'https://my-server.com',\n *     debug: true,\n *     timeoutMs: 30_000,\n *     retry: { attempts: 2 },\n *     signal: controller.signal,\n *   }\n * );\n */\nexport async function fetchCompute<R = unknown>(\n\tendpoint: string,\n\targs: Record<string, any>,\n\tconfig: ComputeConfig\n): Promise<R> {\n\tconst requestId = generateRequestId();\n\t// A circular or BigInt-containing payload makes JSON.stringify throw a raw\n\t// TypeError; surface it as the INVALID_INPUT ComputeError this function's\n\t// contract promises, before anything touches the network.\n\tlet body: string;\n\ttry {\n\t\tbody = JSON.stringify(args);\n\t} catch (error) {\n\t\tthrow new ComputeError(\n\t\t\t`Request body is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\tErrorCodes.INVALID_INPUT,\n\t\t\t{\n\t\t\t\tcontext: { endpoint, requestId },\n\t\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t\t}\n\t\t);\n\t}\n\t// Wire size in UTF-8 bytes — `body.length` counts UTF-16 code units and\n\t// undercounts non-ASCII payloads (matters for the 413 message and size logs).\n\tconst requestSize = utf8ByteLength(body);\n\tconst fullUrl = buildUrl(endpoint, config.serverUrl);\n\tconst headers = buildHeaders(requestId, config);\n\tconst retryPolicy = resolveRetryPolicy(config.retry);\n\tconst totalAttempts = retryPolicy.attempts + 1;\n\n\tif (config.debug) {\n\t\tconst sizeKb = (requestSize / 1024).toFixed(2);\n\t\tconst emoji = requestSize > 100000 ? '⚠️' : '🚀';\n\t\tlog(`${emoji} Starting compute request [${requestId}]: ${endpoint} (${sizeKb}KB)`, true);\n\t}\n\n\tconst ctx: AttemptContext = {\n\t\tendpoint,\n\t\tbody,\n\t\trequestSize,\n\t\tfullUrl,\n\t\trequestId,\n\t\theaders,\n\t\tconfig\n\t};\n\n\t// Every iteration ends in `return` or `throw` (issue 105): attemptFetch never\n\t// asks to retry on the final attempt (all its retry branches are gated on\n\t// `attempt < totalAttempts - 1`), so the exhausted-retries error is the final\n\t// attempt's own `result.cause` — there is no post-loop fallback to reach.\n\tfor (let attempt = 0; ; attempt++) {\n\t\tconst result = await attemptFetch(ctx, retryPolicy, attempt, totalAttempts);\n\n\t\tif (result.ok) return result.value as R;\n\n\t\tif (!result.retry) throw result.cause;\n\n\t\tif (config.debug) {\n\t\t\tlog(\n\t\t\t\t`🔁 Request [${requestId}] retrying after ${result.delayMs}ms (attempt ${attempt + 2}/${totalAttempts}): ${result.cause.message}`,\n\t\t\t\ttrue\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tawait sleep(result.delayMs, config.signal);\n\t\t} catch {\n\t\t\t// Caller cancelled during backoff\n\t\t\tthrow new ComputeError('Request aborted by caller', ErrorCodes.ABORTED, {\n\t\t\t\tcontext: { endpoint, requestId, requestSize },\n\t\t\t\toriginalError: result.cause\n\t\t\t});\n\t\t}\n\t}\n}\n","/**\n * By-reference definition form for solves.\n *\n * Lets a caller that already knows a definition's identity (e.g. a stored\n * version's UUID) schedule solves without materializing the multi-MB bytes:\n * cache keys and the server-pointer map are derived from `key` alone, and\n * `load()` is only called when an upload is genuinely unavoidable (first solve\n * of a definition, or a server-side pointer miss).\n */\n\n/**\n * A definition identified by a stable key, with bytes materialized on demand.\n *\n * **Immutability contract — read this before constructing one.** `key` must\n * identify IMMUTABLE bytes: every `load()` for a given `key` must return the\n * same content, forever. All caching (the scheduler's result cache, the\n * server-pointer map, and any durable cache built on these keys) trusts the\n * key as the definition's identity WITHOUT looking at the bytes. If two\n * different byte contents ever share a key, cached solves from one are served\n * for the other — silent cache poisoning with no diagnostic. Use an identity\n * that can never be reused for different content (e.g. a version UUID), never\n * a mutable name or path.\n */\nexport interface DefinitionRef {\n\t/**\n\t * Identity of immutable bytes (e.g. a version UUID). Two different byte\n\t * contents must never share a key — cache poisoning otherwise.\n\t */\n\tkey: string;\n\t/**\n\t * Materialize the bytes. Called ONLY when an upload is unavoidable — inside\n\t * the solve execution, so it counts toward the solve's abort semantics.\n\t */\n\tload: () => Promise<Uint8Array>;\n}\n\n/**\n * Every definition form accepted by the solve entry points:\n * - a URL, base64 string, or plain string (content-hashed for caching)\n * - raw `.gh` bytes (content-hashed)\n * - a {@link DefinitionRef} (identity-keyed; bytes loaded lazily)\n */\nexport type SolveDefinition = string | Uint8Array | DefinitionRef;\n\n/** Narrow a {@link SolveDefinition} to the by-reference form. */\nexport function isDefinitionRef(definition: SolveDefinition): definition is DefinitionRef {\n\treturn (\n\t\ttypeof definition === 'object' &&\n\t\t!(definition instanceof Uint8Array) &&\n\t\ttypeof definition.key === 'string' &&\n\t\ttypeof definition.load === 'function'\n\t);\n}\n","import type { FileData } from './types';\n\n/**\n * The `Sub Folder` convention, as authored on Selva's Grasshopper file components.\n *\n * `::` nests, matching Rhino's layer separator (`ROOT::Panels`), and `/` and `\\` are accepted\n * because people type them out of habit. The plugin normalizes to `/` before sending, so these\n * helpers also cover payloads from an older plugin that still emit `::`.\n *\n * The first segment is the **root**, which names the archive rather than becoming a folder inside\n * it: `ROOT::Panels` downloads as `ROOT.zip` containing `Panels/…`. Files sharing a root travel\n * together; distinct roots produce separate archives.\n */\n\n/**\n * Split a `Sub Folder` value into folder segments; `/`, `\\` and `::` all separate.\n *\n * Exported because anything rendering a folder tree has to agree with the archive on where the\n * boundaries are — splitting on `/` alone leaves `Main::Panels` as one literal segment and shows\n * a folder named after the separator.\n */\nexport const subFolderSegments = (subFolder: string | undefined): string[] =>\n\t(subFolder ?? '')\n\t\t.replace(/::/g, '/')\n\t\t.replace(/\\\\/g, '/')\n\t\t.split('/')\n\t\t.map((segment) => segment.trim())\n\t\t.filter((segment) => segment !== '');\n\n/** First `Sub Folder` segment, or `''` when the file sits at the top level. */\nexport const rootOf = (file: Pick<FileData, 'subFolder'>): string =>\n\tsubFolderSegments(file.subFolder)[0] ?? '';\n\n/**\n * Everything below the root, as a `/`-joined path. The root names the archive, so repeating it\n * inside would nest it twice.\n */\nexport const pathBelowRoot = (file: Pick<FileData, 'subFolder'>): string =>\n\tsubFolderSegments(file.subFolder).slice(1).join('/');\n\n/**\n * Group files by their `Sub Folder` root, preserving encounter order.\n *\n * Rootless files group under `''` — a caller naming archives supplies its own fallback for that\n * bucket. Useful beyond downloading: a consumer writing to disk gets the same grouping without\n * touching the DOM.\n */\nexport const groupFilesByRoot = <T extends Pick<FileData, 'subFolder'>>(\n\tfiles: readonly T[]\n): Array<{ root: string; files: T[] }> => {\n\tconst groups = new Map<string, T[]>();\n\n\tfor (const file of files) {\n\t\tconst root = rootOf(file);\n\t\tconst bucket = groups.get(root);\n\t\tif (bucket) bucket.push(file);\n\t\telse groups.set(root, [file]);\n\t}\n\n\treturn Array.from(groups, ([root, grouped]) => ({ root, files: grouped }));\n};\n\n/**\n * Make a root usable as a download filename: path separators and the characters Windows forbids\n * would otherwise reach the download attribute verbatim and produce a broken or misdirected save.\n */\nexport const toArchiveName = (root: string): string => {\n\tconst safe = root\n\t\t.replace(/[/\\\\:*?\"<>|]/g, '_')\n\t\t.replace(/\\s+/g, ' ')\n\t\t.trim()\n\t\t.replace(/^\\.+/, '');\n\n\treturn safe === '' ? 'files' : safe;\n};\n","import { ComputeError, ErrorCodes } from '@/core/errors';\nimport { getLogger } from '@/core/utils/logger';\nimport { decodeBase64ToBinary } from '@/core/utils/encoding';\nimport { readField } from '@/core/utils/read-field';\n\nimport { FileBaseInfo, FileData, ProcessedFile } from './types';\nimport { groupFilesByRoot, pathBelowRoot, toArchiveName } from './sub-folder';\n\n/**\n * Extracts and processes files from compute response data without downloading\n * them. Never throws: undecodable/unnamed items and failed external fetches are\n * logged and dropped per-file (see {@link fetchRemoteFiles}).\n */\nexport const extractFilesFromComputeResponse = async (\n\tdownloadableFiles: FileData[],\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null = null\n): Promise<ProcessedFile[]> => {\n\treturn processFiles(downloadableFiles, additionalFiles);\n};\n\n/** Downloads files from a compute response as a ZIP archive. */\nexport const downloadFileData = async (\n\tdownloadableFiles: FileData[],\n\tfileFoldername: string,\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null = null\n): Promise<void> => {\n\t// Check if we're in a browser environment\n\tif (typeof document === 'undefined' || typeof Blob === 'undefined') {\n\t\tthrow new ComputeError(\n\t\t\t'File download functionality is only available in browser environments. This function requires the DOM API (document, Blob).',\n\t\t\tErrorCodes.BROWSER_ONLY,\n\t\t\t{\n\t\t\t\tcontext: {\n\t\t\t\t\tenvironment: typeof window !== 'undefined' ? 'browser (SSR)' : 'Node.js',\n\t\t\t\t\tdocumentAvailable: typeof document !== 'undefined',\n\t\t\t\t\tblobAvailable: typeof Blob !== 'undefined'\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t}\n\n\ttry {\n\t\tconst processedFiles = await processFiles(downloadableFiles, additionalFiles);\n\t\tawait createAndDownloadZip(processedFiles, fileFoldername);\n\t} catch (err) {\n\t\t// Re-throw if it's already a ComputeError\n\t\tif (err instanceof ComputeError) {\n\t\t\tthrow err;\n\t\t}\n\t\tthrow new ComputeError(\n\t\t\t'Failed to download files from compute response',\n\t\t\tErrorCodes.INVALID_STATE,\n\t\t\t{\n\t\t\t\tcontext: { originalError: err instanceof Error ? err.message : String(err) },\n\t\t\t\toriginalError: err instanceof Error ? err : undefined\n\t\t\t}\n\t\t);\n\t}\n};\n\n/**\n * Download files as one archive per `Sub Folder` root.\n *\n * `ROOT::Panels` and `OTHERROOT::Panels` produce `ROOT.zip` and `OTHERROOT.zip`, each containing\n * `Panels/…` — the root names the archive instead of nesting inside it. Files with no root fall\n * back to `fallbackName`, so a definition that never sets `Sub Folder` downloads exactly as before.\n *\n * Archives are saved one at a time: browsers discard concurrent downloads issued in the same tick,\n * and these are user-initiated saves rather than a throughput-bound batch. Note that saving more\n * than one file per gesture may prompt for permission.\n *\n * @param downloadableFiles - `FileData` items from the compute response.\n * @param fallbackName - Archive name for files with no `Sub Folder` root.\n * @param additionalFiles - Extra files to package; they carry no root and join the fallback archive.\n */\nexport const downloadFileDataByRoot = async (\n\tdownloadableFiles: FileData[],\n\tfallbackName: string,\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null = null\n): Promise<void> => {\n\tconst groups = groupFilesByRoot(downloadableFiles);\n\n\t// Extras alone still deserve an archive; without this they'd be dropped for having no root.\n\tif (groups.length === 0) {\n\t\tawait downloadFileData([], fallbackName, additionalFiles);\n\t\treturn;\n\t}\n\n\t// Extras belong to the definition rather than to any one root, so they ride with the rootless\n\t// archive — or with the first one when every file is rooted, rather than being dropped.\n\tconst extrasRoot = (groups.find((group) => group.root === '') ?? groups[0]).root;\n\n\tfor (const { root, files } of groups) {\n\t\tawait downloadFileData(\n\t\t\troot === '' ? files : files.map((file) => ({ ...file, subFolder: pathBelowRoot(file) })),\n\t\t\troot === '' ? fallbackName : toArchiveName(root),\n\t\t\troot === extrasRoot ? additionalFiles : null\n\t\t);\n\t}\n};\n\n/**\n * Reduce a server-controlled path field to safe relative segments for use inside the zip\n * (zip-slip defense): backslashes normalize to `/`, and empty, `.`, `..`, and drive-letter\n * segments are dropped so no entry can escape the extraction directory via traversal or an\n * absolute path. Returns '' when nothing safe remains.\n *\n * `::` nests, matching the `Sub Folder` input's Rhino-layer syntax. The plugin already\n * normalizes it, so this is for payloads from an older one — without it those land in a\n * literal folder named `ROOT::Panels`.\n */\nconst sanitizeArchivePath = (raw: string): string =>\n\traw\n\t\t.replace(/::/g, '/')\n\t\t.replace(/\\\\/g, '/')\n\t\t.split('/')\n\t\t.map((segment) => segment.trim())\n\t\t.filter(\n\t\t\t(segment) =>\n\t\t\t\tsegment !== '' && segment !== '.' && segment !== '..' && !/^[a-zA-Z]:$/.test(segment)\n\t\t)\n\t\t.join('/');\n\n/**\n * Lenient read of the `isBase64Encoded` wire flag: some server branches serialize\n * booleans as strings (`\"true\"`/`\"True\"`), which must still count as base64 rather\n * than silently dropping the file (issue 95).\n */\nconst isBase64Flag = (flag: unknown): boolean =>\n\tflag === true || (typeof flag === 'string' && flag.trim().toLowerCase() === 'true');\n\n/**\n * Decode the inline files carried in a compute response into `ProcessedFile`s.\n *\n * Pure and synchronous: base64 items are decoded to binary, plain-text items are\n * passed through, and the archive path is derived from `subFolder` + name.\n * Degrades per-file like {@link fetchRemoteFiles}: an item with no usable `data`\n * or with undecodable base64 is logged and skipped, never aborting the batch.\n * This is the half of file handling that both public entry points share; it\n * never touches the network and never throws.\n *\n * Wire fields are read case-insensitively via {@link readField}: mcneel-branch\n * servers serialize PascalCase (`FileName`, `Data`, `IsBase64Encoded`, …) while\n * the VektorNode fork uses camelCase — both must decode (issue 95).\n *\n * @param dataItems - `FileData` items from the compute response.\n * @returns The decoded files.\n */\nconst decodeResponseFiles = (dataItems: FileData[]): ProcessedFile[] => {\n\tconst processedFiles: ProcessedFile[] = [];\n\n\tdataItems.forEach((item) => {\n\t\tconst rawName = readField<string>(item, 'fileName') ?? '';\n\t\tconst rawType = readField<string>(item, 'fileType') ?? '';\n\t\tconst fileName = sanitizeArchivePath(`${rawName}${rawType}`);\n\t\tif (fileName === '') {\n\t\t\tgetLogger().warn(\n\t\t\t\t`Skipping file with unusable name \"${rawName}${rawType}\": no safe archive path remains after sanitization.`\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst rawSubFolder = readField<string>(item, 'subFolder');\n\t\tconst subFolder = rawSubFolder ? sanitizeArchivePath(rawSubFolder) : '';\n\t\tconst filePath = subFolder !== '' ? `${subFolder}/${fileName}` : fileName;\n\n\t\t// Only a genuinely absent/empty body is unusable — the flag's exact type\n\t\t// must never decide that (issue 95).\n\t\tconst data = readField<unknown>(item, 'data');\n\t\tif (typeof data !== 'string' || data === '') {\n\t\t\tgetLogger().warn(`Skipping file \"${filePath}\": item carries no usable data.`);\n\t\t\treturn;\n\t\t}\n\n\t\t// GH-authored, not interpreted here — passed through so a consumer that\n\t\t// stores files rather than zipping them keeps the authoring context.\n\t\tconst metadata = readField<Record<string, string>>(item, 'metadata');\n\n\t\tif (isBase64Flag(readField<unknown>(item, 'isBase64Encoded'))) {\n\t\t\t// `decodeBase64ToBinary` already returns a correctly-bounded view;\n\t\t\t// re-wrapping `.buffer` would discard its byteOffset/byteLength and\n\t\t\t// expose the whole (possibly pooled) backing buffer as corrupt content.\n\t\t\ttry {\n\t\t\t\tprocessedFiles.push({\n\t\t\t\t\tfileName,\n\t\t\t\t\tcontent: decodeBase64ToBinary(data),\n\t\t\t\t\tpath: filePath,\n\t\t\t\t\tsubFolder,\n\t\t\t\t\t...(metadata ? { metadata } : {})\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tgetLogger().warn(`Skipping file \"${filePath}\": base64 decode failed.`, err);\n\t\t\t}\n\t\t} else {\n\t\t\tprocessedFiles.push({\n\t\t\t\tfileName,\n\t\t\t\tcontent: data,\n\t\t\t\tpath: filePath,\n\t\t\t\tsubFolder,\n\t\t\t\t...(metadata ? { metadata } : {})\n\t\t\t});\n\t\t}\n\t});\n\n\treturn processedFiles;\n};\n\n/** Abort a hung external-file fetch — one dead URL must degrade the batch, not stall it forever. */\nconst REMOTE_FILE_TIMEOUT_MS = 30_000;\n\n/**\n * Fetch externally-referenced files over HTTP into `ProcessedFile`s.\n *\n * Async and fallible by nature. A failed fetch (network error, non-OK status,\n * or timeout after {@link REMOTE_FILE_TIMEOUT_MS}) is logged and that file is\n * dropped — the rest still resolve — so one dead URL degrades the result rather\n * than aborting the whole batch. This swallow is deliberate and pinned by\n * tests; callers receive only the files that succeeded.\n *\n * @param refs - External file references to fetch.\n * @returns The successfully-fetched files (failures omitted).\n */\nconst fetchRemoteFiles = async (refs: FileBaseInfo[]): Promise<ProcessedFile[]> => {\n\tconst fetched = await Promise.all(\n\t\trefs.map(async (file) => {\n\t\t\ttry {\n\t\t\t\tconst signal =\n\t\t\t\t\ttypeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'\n\t\t\t\t\t\t? AbortSignal.timeout(REMOTE_FILE_TIMEOUT_MS)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tconst response = await fetch(file.filePath, { signal });\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tgetLogger().warn(`Failed to fetch additional file from URL: ${file.filePath}`);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\t// One step, no intermediate Blob allocation (issue 111).\n\t\t\t\tconst arrayBuffer = await response.arrayBuffer();\n\t\t\t\t// Same zip-slip defense as decodeResponseFiles — the name lands in the archive.\n\t\t\t\tconst safeName = sanitizeArchivePath(file.fileName);\n\t\t\t\tif (safeName === '') {\n\t\t\t\t\tgetLogger().warn(`Skipping fetched file with unusable name: ${file.fileName}`);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst subFolder = file.subFolder ? sanitizeArchivePath(file.subFolder) : '';\n\t\t\t\treturn {\n\t\t\t\t\tfileName: safeName,\n\t\t\t\t\tcontent: new Uint8Array(arrayBuffer),\n\t\t\t\t\tpath: subFolder !== '' ? `${subFolder}/${safeName}` : safeName,\n\t\t\t\t\t// No `metadata`: an external URL carries no GH authoring context.\n\t\t\t\t\tsubFolder\n\t\t\t\t} as ProcessedFile;\n\t\t\t} catch (error) {\n\t\t\t\tgetLogger().error(`Error fetching additional file from URL: ${file.filePath}`, error);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t})\n\t);\n\n\treturn fetched.filter((f): f is ProcessedFile => f !== null);\n};\n\n/**\n * Compose the decoded response files with any fetched external files.\n *\n * Archive paths are de-duplicated here — on the path both public entry points\n * share — so `extractFilesFromComputeResponse` consumers keying by `path` never\n * lose files silently, matching the zip path's rename behavior (issue 111).\n *\n * @param dataItems - `FileData` items from the compute response.\n * @param additionalFiles - Optional external file references to fetch and include.\n * @returns A Promise resolving to the combined `ProcessedFile` list.\n */\nconst processFiles = async (\n\tdataItems: FileData[],\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null\n): Promise<ProcessedFile[]> => {\n\tconst processedFiles = decodeResponseFiles(dataItems);\n\n\tif (additionalFiles) {\n\t\tconst filesArray = Array.isArray(additionalFiles) ? additionalFiles : [additionalFiles];\n\t\tprocessedFiles.push(...(await fetchRemoteFiles(filesArray)));\n\t}\n\n\tconst taken = new Set<string>();\n\treturn processedFiles.map((file) => {\n\t\tconst path = uniqueArchivePath(file.path, taken);\n\t\ttaken.add(path);\n\t\tif (path === file.path) return file;\n\t\tgetLogger().warn(`Duplicate archive path \"${file.path}\" — storing as \"${path}\".`);\n\t\treturn { ...file, path, fileName: path.slice(path.lastIndexOf('/') + 1) };\n\t});\n};\n\n/** Creates a ZIP archive from processed files and triggers a browser download. */\nasync function createAndDownloadZip(files: ProcessedFile[], zipName: string): Promise<void> {\n\tconst { zip, strToU8 } = await import('fflate');\n\n\t// Convert files to fflate format. Zip entries are keyed by path, so two\n\t// files with the same path would silently overwrite each other — rename\n\t// collisions (\"model.txt\" → \"model-2.txt\") instead of losing data.\n\t// (processFiles already de-duplicates; this is a cheap second line of\n\t// defense in case callers construct ProcessedFiles themselves.)\n\tconst zipData: Record<string, Uint8Array> = {};\n\tconst taken = new Set<string>();\n\tfiles.forEach((file) => {\n\t\tconst path = uniqueArchivePath(file.path, taken);\n\t\ttaken.add(path);\n\t\tif (path !== file.path) {\n\t\t\tgetLogger().warn(`Duplicate archive path \"${file.path}\" — storing as \"${path}\".`);\n\t\t}\n\t\tzipData[path] = typeof file.content === 'string' ? strToU8(file.content) : file.content;\n\t});\n\n\t// Async `zip` deflates on a worker thread instead of blocking the main thread\n\t// like `zipSync` — keeps the UI responsive for large geometry exports.\n\tconst zipped = await new Promise<Uint8Array>((resolve, reject) => {\n\t\tzip(zipData, { level: 6 }, (err, data) => (err ? reject(err) : resolve(data)));\n\t});\n\n\tconst blob = new Blob([zipped as BlobPart], { type: 'application/zip' });\n\tsaveFile(blob, `${zipName}.zip`);\n}\n\n/**\n * First archive path not already taken in `taken`, disambiguating with a\n * numeric suffix before the extension: `dir/model.txt` → `dir/model-2.txt`.\n */\nfunction uniqueArchivePath(path: string, taken: ReadonlySet<string>): string {\n\tif (!taken.has(path)) return path;\n\tconst slash = path.lastIndexOf('/');\n\tconst dot = path.lastIndexOf('.');\n\tconst stemEnd = dot > slash ? dot : path.length;\n\tconst stem = path.slice(0, stemEnd);\n\tconst ext = path.slice(stemEnd);\n\tfor (let i = 2; ; i++) {\n\t\tconst candidate = `${stem}-${i}${ext}`;\n\t\tif (!taken.has(candidate)) return candidate;\n\t}\n}\n\n/** Saves a Blob object as a file in the user's browser. */\nfunction saveFile(blob: Blob, filename: string) {\n\tif (typeof document === 'undefined') {\n\t\tthrow new ComputeError(\n\t\t\t'saveFile requires a browser environment with DOM API access.',\n\t\t\tErrorCodes.BROWSER_ONLY,\n\t\t\t{\n\t\t\t\tcontext: { function: 'saveFile', requiredAPI: 'document' }\n\t\t\t}\n\t\t);\n\t}\n\n\tconst url = URL.createObjectURL(blob);\n\tconst a = document.createElement('a');\n\ta.href = url;\n\ta.download = filename;\n\t// Firefox requires the anchor to be in the DOM for the click to download.\n\tdocument.body.appendChild(a);\n\ta.click();\n\ta.remove();\n\t// Revoking synchronously can abort the download in some browsers — the\n\t// browser only pins the blob once the download has actually started.\n\tsetTimeout(() => URL.revokeObjectURL(url), 10_000);\n}\n"],"mappings":"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"}