@selvajs/solve 2.0.0-beta.8 → 2.0.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.
- package/README.md +17 -11
- package/dist/abort-CNJ8Sn4g.js +13 -0
- package/dist/abort-CNJ8Sn4g.js.map +1 -0
- package/dist/client.d.ts +34 -135
- package/dist/client.js +968 -1
- package/dist/client.js.map +1 -1
- package/dist/{outcome-DsNmx27P.d.ts → outcome-Bk9mnMi0.d.ts} +9 -28
- package/dist/outcome-DNrffAXp.js +169 -0
- package/dist/outcome-DNrffAXp.js.map +1 -0
- package/dist/server.d.ts +98 -230
- package/dist/server.js +984 -1
- package/dist/server.js.map +1 -1
- package/dist/shared.d.ts +2 -2
- package/dist/shared.js +1 -1
- package/package.json +16 -41
- package/dist/client.cjs +0 -2
- package/dist/client.cjs.map +0 -1
- package/dist/client.d.cts +0 -429
- package/dist/outcome-BOrAeRAQ.js +0 -2
- package/dist/outcome-BOrAeRAQ.js.map +0 -1
- package/dist/outcome-DUHQoZqq.cjs +0 -2
- package/dist/outcome-DUHQoZqq.cjs.map +0 -1
- package/dist/outcome-DsNmx27P.d.cts +0 -92
- package/dist/server.cjs +0 -2
- package/dist/server.cjs.map +0 -1
- package/dist/server.d.cts +0 -419
- package/dist/shared.cjs +0 -1
- package/dist/shared.d.cts +0 -3
- package/dist/solve-input-CUjVGYPC.d.cts +0 -11
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["isDefinitionRef","isDefinitionRef$1","buildSolveInputTree","TreeBuilder","inputs","input","values","gzipAsync","promisify","gzip","COMPUTE_CONTRACT_VERSION","COMPUTE_VERSION_HEADER","runSolvePipeline","args","treeBuildStart","inputTree","treeBuildMs","rhino","result","settle","solveStart","detail","client","t","err","durationMs","isAbort","ComputeError","ErrorCodes","isDeadline","signal","solveMs","serializeStart","serialized","serializeMs","worthSendingGzip","compressed","gzipMs","gzipStart","serverTotalMs","headers","buildServerTiming","metrics","envelope","adaptEnvelopeToEncoding","waiterWantsGzip","acceptEncoding","json","gunzipSync","header","parts","decode","solve","encode","computeLinkMs","label","ms","byteRef","verdict","serverIdentity","server","FAILED_BUILD_TTL_MS","createClientCache","maxWarmComputeServers","config","cache","pending","failed","enableDebugLogging","debugLog","message","build","debugVerbose","clientConfig","defCache","solveCache","ran","definitionGuid","GrasshopperClient","maxConcurrent","lastProbeAt","probing","refreshConcurrency","children","next","scheduler","errs","errCount","cacheNote","key","existing","inFlight","recentFailure","buildPromise","entry","opts","oldestKey","id","total","s","createDefinitionByteCache","budget","maxBytes","retainedBytes","stats","touch","evictToFit","oldest","store","bytes","outcome","versionId","hit","load","createSolveCacheSingleFlight","inflight","options","flight","onWaiterJoined","p","work","isByteCacheRef","source","isVersionLoadPair","SolveEngine","logger","NoopLogger","resolvedSource","coalesceKey","identity","stableStringify","abortController","hasWaiters","onCallerAbort","wire","fail","status","body","r","ref","isDefinitionRef"],"sources":["../src/server/transform-input.ts","../src/server/solve-pipeline.ts","../src/server/client-cache.ts","../src/server/definition-byte-cache.ts","../src/server/solve-cache-single-flight.ts","../src/server/solve-engine.ts"],"sourcesContent":["import { TreeBuilder, type DataTree } from '@selvajs/compute/grasshopper';\nimport type { SolveInput } from '../shared/solve-input.js';\n\n/** The input tree a solve sends: each input's value, or its schema default when it has none. */\nexport function buildSolveInputTree(\n\tinputs: SolveInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromValues(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input) => ({\n\t\t\t\tparamName: input.nickname,\n\t\t\t\tparamType: input.paramType,\n\t\t\t\tvalue: values[input.id] ?? input.default,\n\t\t\t\tminimum: input.minimum,\n\t\t\t\tmaximum: input.maximum\n\t\t\t}))\n\t);\n}\n","/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * solve and returns a discriminated {@link SolveOutcome} instead of throwing\n * for expected failures. Auth, the database, share tokens, rate limits, and\n * metric sinks are the calling route's job, not this file's.\n */\n\nimport {\n\tErrorCodes,\n\tComputeError,\n\ttype ServerTiming,\n\ttype SolveDefinition\n} from '@selvajs/compute/core';\nimport type {\n\tDataTree,\n\tGrasshopperComputeResponse,\n\tSelvaEventTarget\n} from '@selvajs/compute/grasshopper';\nimport type { SolveInput } from '../shared/solve-input.js';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { buildSolveInputTree } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SolveInput;\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/** Mutable load outcome for a `DefinitionRef` source, so the `def_bytes` Server-Timing verdict can be emitted. Omit for raw-bytes solves. */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}; skips the\n\t * pipeline's own build and solves this object directly.\n\t *\n\t * A caller coalescing concurrent solves needs this: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Only phrases the timeout message — the scheduler enforces the deadline itself. */\n\tsolveDeadlineMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/** Per-solve live-event callback the compute server hands to the definition; absent when the caller opened no stream. */\n\tselvaEvents?: SelvaEventTarget;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also distinguishes a client disconnect\n\t * from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/** Wall-clock origin (`performance.now()` at the top of the request) so `load`/`total` phase timings line up with the caller's pre-solve prep. */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/** Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*` Server-Timing entries. */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure the\n * calling route maps to a status code (timeout→504, client_abort→499,\n * too_large→413, shed→503+Retry-After, compute_error→generic 500/503).\n * `durationMs` on error variants is the solve wall time up to the failure, for\n * the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** Scheduler backpressure rejected the solve before it ran: queue was full (`queue_full`) or the wait exceeded the deadline (`queue_timeout`). Retryable — `retryAfterSeconds` is a backoff hint. */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\tlet rhino: ServerTiming | null = null;\n\tlet result: GrasshopperComputeResponse;\n\tlet settle: CacheVerdict;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tconst detail = await client.scheduler.solveDetailed(args.definitionSource, inputTree, {\n\t\t\tsignal,\n\t\t\tselvaevents: args.selvaEvents,\n\t\t\tonServerTiming: (t) => {\n\t\t\t\trhino = t;\n\t\t\t}\n\t\t});\n\t\tresult = detail.response;\n\t\tsettle = { fromCache: detail.fromCache, definitionReuploaded: detail.definitionReuploaded };\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// Abort with the request signal NOT aborted means the scheduler's own\n\t\t// deadline timer fired, not a client disconnect. The scheduler normalizes\n\t\t// aborts into ComputeError (code ABORTED / TIMEOUT_ERROR) — a raw\n\t\t// AbortError only reaches here from a custom executor, so match both\n\t\t// shapes or real disconnects and deadlines fall through to compute_error\n\t\t// and read as 500s.\n\t\tconst isAbort =\n\t\t\t(err instanceof Error && err.name === 'AbortError') ||\n\t\t\t(err instanceof ComputeError && err.code === ErrorCodes.ABORTED);\n\t\tconst isDeadline = err instanceof ComputeError && err.code === ErrorCodes.TIMEOUT_ERROR;\n\t\tif (isAbort || isDeadline) {\n\t\t\tif (isAbort && signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.solveDeadlineMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tif (err instanceof ComputeError) {\n\t\t\tif (err.code === ErrorCodes.RESPONSE_TOO_LARGE) return { kind: 'too_large' };\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t// here. Treat it the same as the byte cap: too_large.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// Buffered, not streamed, so Content-Length is known upfront and a connection\n\t// cut mid-transfer fails hard instead of silently truncating the JSON. Below\n\t// 1 KB gzip isn't worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope},\n * baked from the FIRST caller's `Accept-Encoding`. A later waiter with a\n * different `Accept-Encoding` would otherwise get a body labelled with the\n * wrong encoding — `Vary` can't help here, since it's one shared object, not a\n * cache lookup. Encoding is deliberately left out of the coalesce key so mixed\n * clients still coalesce; this adapts the shared result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// Only mismatch that corrupts the response: gzip body, non-gzip waiter.\n\t// Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface CacheVerdict {\n\tfromCache: boolean;\n\tdefinitionReuploaded?: boolean;\n}\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own timing\n * (VektorNode fork), `rhino_*` is time on the compute server and `compute_link`\n * is everything between it (network + queue wait). `rhino_cache` is 1 when the\n * compute server answered from its own result cache.\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst decode = rhino.decode ?? 0;\n\t\tconst solve = rhino.solve ?? 0;\n\t\tconst encode = rhino.encode ?? 0;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - decode - solve - encode);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}` +\n\t\t\t`, rhino_cache;dur=${rhino.cacheHit ? 1 : 0}`;\n\t\tif (rhino.gh !== undefined) header += `, rhino_gh;dur=${rhino.gh.toFixed(1)}`;\n\t\tif (rhino.recomputed) {\n\t\t\theader += `, rhino_recomputed;desc=\"${rhino.recomputed.ran}/${rhino.recomputed.total}\"`;\n\t\t}\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\tif (settle.definitionReuploaded !== undefined) {\n\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t}\n\n\t// skipped = scheduler never materialized bytes (pointer-known solve, the\n\t// whole point of this path); hit = warm byte cache; miss = fell through to\n\t// storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import {\n\tGrasshopperClient,\n\ttype GrasshopperComputeConfig,\n\ttype SolveScheduler\n} from '@selvajs/compute/grasshopper';\nimport { enableDebugLogging } from '@selvajs/compute/core';\n\n// ============================================================================\n// Shared compute client cache\n// ============================================================================\n//\n// One warm `GrasshopperClient` (+ its `SolveScheduler`) per distinct compute\n// server, keyed by the server's `id`, never its URL — a rotated URL/apiKey\n// keeps the same key with stale connection details, so the config-write path\n// MUST call `evict(id)` when a server's config changes.\n//\n// Per-server LRU, not a single shared client: definitions can pin different\n// servers, so two definitions on two servers keep two warm clients, with\n// churn from one-off servers bounded by `maxWarmComputeServers`.\n//\n// Both hot paths share this cache — the solve endpoint uses the entry's\n// `scheduler`; the definition-viewer render path uses the entry's `client`\n// for `getIO` — so a definition that was just solved renders from the same\n// warm client instead of re-handshaking Rhino.Compute per page load.\n\n/**\n * Opaque identity of a compute server — construct only via `serverIdentity()`.\n * Kept opaque so \"a server is now a pool of URLs behind one id\" stays additive.\n */\nexport type ServerIdentity = string & { readonly __brand: 'ServerIdentity' };\n\n/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */\nexport interface ResolvedServer {\n\tid: string;\n\tserverUrl: string;\n\t/** Sent as the `RhinoComputeKey` header. */\n\tapiKey?: string;\n}\n\nexport function serverIdentity(server: Pick<ResolvedServer, 'id'>): ServerIdentity {\n\treturn server.id as ServerIdentity;\n}\n\nexport interface CachedClient {\n\tclient: GrasshopperClient;\n\tscheduler: SolveScheduler;\n}\n\n/**\n * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs\n * (`onDebugLog`), `'verbose'` also enables full lib-level request/response\n * dumps (incl. geometry) — a superset of `true`, not an independent setting.\n */\nexport type ClientCacheDebug = boolean | 'verbose';\n\n/** Config injected by the consuming app, so this module stays env-agnostic and testable. */\nexport interface ClientCacheConfig {\n\t/** Per-solve timeout forwarded to the scheduler (`ComputeLimits.solveDeadlineMs`). */\n\tsolveDeadlineMs: number;\n\tcachesolve: boolean;\n\t/** Only meaningful with `cachesolve`. */\n\tcacheerroredsolves: boolean;\n\t/** Reference large definitions by server cache key instead of re-uploading. */\n\treuseServerDefinitionCache: boolean;\n\t/**\n\t * Max solves that may wait in the FIFO queue, excluding in-flight solves\n\t * (capped at `maxConcurrent`, itself driven by the server's probed child\n\t * count). `0` = unbounded (`ComputeLimits.computeMaxQueueDepth`); a full\n\t * queue rejects new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Max ms a solve may sit queued before executing; `0` = no deadline\n\t * (`ComputeLimits.computeQueueWaitMs`). Too long a wait rejects with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache. Applies per warm\n\t * client — worst-case heap is this × `maxWarmComputeServers`. `0` disables\n\t * the cache (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\t/**\n\t * Largest compute response body read; past it the download is cancelled (`too_large`).\n\t * Default: no limit.\n\t */\n\tmaxResponseBytes?: number;\n\tdebug: ClientCacheDebug;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */\n\tmaxWarmComputeServers?: number;\n\t/** Sink for the debug lines. `onDebugLog` only fires when `debug` is not `false`. */\n\tonDebugLog?: (message: string) => void;\n}\n\nexport interface ClientCache {\n\t/**\n\t * Get (or create) the warm client + scheduler for a resolved compute server,\n\t * keyed by its `id`. `definitionGuid`, when present, is stamped as the\n\t * `X-Selva-Definition` header on this client's outbound solve/IO requests —\n\t * inert routing/telemetry metadata until a pool router exists.\n\t */\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient>;\n\t/** Dispose and drop the warm client for `id`, so the next request rebuilds against fresh connection details. */\n\tevict(id: string | ServerIdentity): void;\n\t/** Solve-cache counters summed across every warm client. Counters die with the client that owns them, so totals can fall over time. */\n\tsolveCacheStats(): SolveCacheStats;\n\t/**\n\t * Drop every retained solve result, keeping the warm clients (and their\n\t * connections and server-side definition pointers) intact. Nothing expires\n\t * these on its own — the byte budget is the only other pressure — so this is\n\t * the operator's release valve when a definition's inputs no longer describe\n\t * its output, e.g. it reads an external source that has since changed.\n\t */\n\tclearSolveCaches(): void;\n\t/** Dispose every warm client. Test seam / shutdown hook. */\n\tdisposeAll(): void;\n}\n\nexport interface SolveCacheStats {\n\twarmClients: number;\n\tentries: number;\n\tbytes: number;\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped under size/byte pressure (not replacement or manual clears). */\n\tevictions: number;\n}\n\nconst DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;\n\n/**\n * Fallback concurrency while the first probe is still in flight, and the value\n * kept whenever a probe fails or reports 0 — never send more than a single\n * solve to a server whose real capacity is unknown.\n */\nconst FALLBACK_CONCURRENCY = 1;\n\n/**\n * How stale the compute server's child count may get before a solve triggers a\n * re-read. Short enough that a resized pool is picked up within a few solves,\n * long enough that a busy server is probed rarely — the probe rides on solve\n * completion, so an idle server is never polled at all.\n */\nconst CONCURRENCY_PROBE_INTERVAL_MS = 5 * 60 * 1000;\n\n/**\n * How long a failed build is remembered before the next `getClient` retries the\n * handshake. Without this, every solve against a down server pays the full probe\n * ladder again, so a user clicking Solve twice waits twice.\n *\n * Short on purpose: this is a convenience for the rapid-retry case, not a\n * circuit breaker. An operator who starts the compute VM should not have to wait\n * out a long penalty window before the app notices.\n */\nconst FAILED_BUILD_TTL_MS = 5000;\n\n/**\n * A build failure worth replaying to the next caller, with the time it happened\n * so {@link FAILED_BUILD_TTL_MS} can expire it.\n */\ninterface FailedBuild {\n\terror: unknown;\n\tat: number;\n}\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxWarmComputeServers = config.maxWarmComputeServers ?? DEFAULT_MAX_WARM_COMPUTE_SERVERS;\n\tconst cache = new Map<string, CachedClient>();\n\t// Builds in flight, keyed like `cache`. Concurrent `getClient` calls for the\n\t// same id must share ONE build: without this, both miss, both handshake, and\n\t// the loser's entry is overwritten in the map without ever being disposed.\n\tconst pending = new Map<string, Promise<CachedClient>>();\n\t// Recent build failures, keyed like `cache`. Cleared by `evict` so a config\n\t// fix takes effect immediately rather than waiting out the TTL.\n\tconst failed = new Map<string, FailedBuild>();\n\n\tif (config.debug === 'verbose') enableDebugLogging();\n\n\tconst debugLog = (message: string) => {\n\t\tif (config.debug) config.onDebugLog?.(message);\n\t};\n\n\tasync function build(server: ResolvedServer, definitionGuid?: string): Promise<CachedClient> {\n\t\tconst debugVerbose = config.debug === 'verbose';\n\n\t\tconst clientConfig: GrasshopperComputeConfig = {\n\t\t\tserverUrl: server.serverUrl,\n\t\t\tapiKey: server.apiKey,\n\t\t\t// Lib-level debug is the verbose one (full payload logs); keep it on the\n\t\t\t// verbose flag so the concise cache logs can run without the noise.\n\t\t\tdebug: debugVerbose,\n\t\t\tcachesolve: config.cachesolve,\n\t\t\tcacheerroredsolves: config.cacheerroredsolves,\n\t\t\tmaxResponseBytes: config.maxResponseBytes\n\t\t};\n\n\t\tif (config.debug) {\n\t\t\tclientConfig.onServerTiming = (t) => {\n\t\t\t\tif (t.cacheHit) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t'[Compute/rhino-cache] solve-cache (cachesolve): HIT (cached result, solve skipped)'\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst decode = t.decode ?? 0;\n\t\t\t\tconst solve = t.solve ?? 0;\n\t\t\t\tconst encode = t.encode ?? 0;\n\t\t\t\t// Heuristic threshold: a genuine parse is many ms; a reused definition is\n\t\t\t\t// sub-millisecond. Only meaningful relative to the first (cold) solve.\n\t\t\t\tconst defCache =\n\t\t\t\t\tdecode <= 1 ? 'HIT (definition reused, no re-upload/parse)' : 'miss (full parse)';\n\t\t\t\tconst solveCache = config.cachesolve ? 'miss (solved fresh)' : 'off';\n\t\t\t\tconst ran = t.recomputed ? ` | ran ${t.recomputed.ran}/${t.recomputed.total} objects` : '';\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/rhino-cache] decode=${decode}ms solve=${solve}ms encode=${encode}ms ` +\n\t\t\t\t\t\t`| definition-cache: ${defCache} | solve-cache (cachesolve): ${solveCache}${ran}`\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\n\t\t// Baked at client-create time because the scheduler has no per-request\n\t\t// header hook.\n\t\tif (definitionGuid) {\n\t\t\tclientConfig.headers = { 'X-Selva-Definition': definitionGuid };\n\t\t}\n\n\t\tconst client = await GrasshopperClient.create(clientConfig);\n\n\t\t// rhino.compute hands out children in strict rotation and never reports \"busy\",\n\t\t// so nothing downstream enforces a concurrency cap — send more than it has\n\t\t// children and requests silently double up on one. Rhino.Compute is the only\n\t\t// source of truth for its own child count, so this always probes rather than\n\t\t// trusting an operator-supplied guess.\n\t\t//\n\t\t// The count is not static: children exit on their own under `-idlespan`, and\n\t\t// `/launch-child` + `/recycle-children` resize the pool at runtime. So the probe\n\t\t// re-runs after solves (see `refreshConcurrency`) rather than only at connect.\n\t\tlet maxConcurrent = FALLBACK_CONCURRENCY;\n\t\tlet lastProbeAt = 0;\n\t\tlet probing = false;\n\n\t\t/**\n\t\t * Best-effort re-read of the server's child count. Never awaited by a caller:\n\t\t * a probe must not add a round-trip to the solve path, so a changed count takes\n\t\t * effect on the NEXT solve rather than the one that triggered the probe.\n\t\t *\n\t\t * `initialize: false` keeps this from spawning children on an idle server —\n\t\t * which also means a cold server honestly reports 0. Both a failed probe and a\n\t\t * 0 result fall back to `FALLBACK_CONCURRENCY` (1): an unknown-capacity server\n\t\t * is safer under-used than oversent.\n\t\t */\n\t\tasync function refreshConcurrency(scheduler?: { setMaxConcurrent(n: number): void }) {\n\t\t\tif (probing) return;\n\t\t\tprobing = true;\n\t\t\ttry {\n\t\t\t\tconst children = await Promise.resolve(\n\t\t\t\t\tclient.serverStats?.getActiveChildren({ initialize: false })\n\t\t\t\t).catch(() => null);\n\t\t\t\tlastProbeAt = Date.now();\n\t\t\t\tconst next = !children || children <= 0 ? FALLBACK_CONCURRENCY : children;\n\t\t\t\tif (next === maxConcurrent) return;\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/client-cache] concurrency ${maxConcurrent} → ${next} (server child count)`\n\t\t\t\t);\n\t\t\t\tmaxConcurrent = next;\n\t\t\t\tscheduler?.setMaxConcurrent(next);\n\t\t\t} finally {\n\t\t\t\tprobing = false;\n\t\t\t}\n\t\t}\n\n\t\t// One blocking probe at connect so the first solves already use the right cap.\n\t\tawait refreshConcurrency();\n\n\t\tconst scheduler = client.createScheduler({\n\t\t\tmode: 'queue',\n\t\t\t// Queue mode defaults maxConcurrent to 1, serializing every solve on\n\t\t\t// this server through a single slot while the compute VM's other\n\t\t\t// compute.geometry children idle — always pass it explicitly.\n\t\t\tmaxConcurrent,\n\t\t\t// The scheduler treats undefined as unbounded/no-deadline, so map our\n\t\t\t// `0`-means-off convention to undefined rather than passing 0 (which\n\t\t\t// would reject EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: config.solveDeadlineMs,\n\t\t\t// The byte budget is the only eviction pressure. A solve is a pure\n\t\t\t// function of (definition, inputs), both immutable, so a retained\n\t\t\t// result can never go stale — expiring one only forces a paid re-solve\n\t\t\t// of the identical answer.\n\t\t\tcache: config.responseCacheMaxBytes > 0 ? { maxBytes: config.responseCacheMaxBytes } : false,\n\t\t\treuseServerDefinitionCache: config.reuseServerDefinitionCache,\n\t\t\tonSettle: (_ctx, result) => {\n\t\t\t\tif (result.status !== 'success') return;\n\t\t\t\t// Piggyback the pool re-read on real traffic: only servers actually\n\t\t\t\t// being used get probed, at most once per window, and never on the\n\t\t\t\t// request path. A cache hit means we never reached the server, so it\n\t\t\t\t// says nothing about the pool — don't spend a probe on it.\n\t\t\t\tif (\n\t\t\t\t\tresult.fromCache !== true &&\n\t\t\t\t\tDate.now() - lastProbeAt >= CONCURRENCY_PROBE_INTERVAL_MS\n\t\t\t\t) {\n\t\t\t\t\t// Safe despite being referenced from inside its own initializer:\n\t\t\t\t\t// onSettle only ever fires during a solve, long after binding.\n\t\t\t\t\tvoid refreshConcurrency(scheduler);\n\t\t\t\t}\n\t\t\t\tif (!config.debug) return;\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/selva-cache] ${result.fromCache ? 'HIT — served from cache (no compute call)' : 'miss — went to Rhino.Compute'} (${Math.round(result.durationMs)}ms)`\n\t\t\t\t);\n\t\t\t\tif (!result.fromCache) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\tresult.definitionReuploaded === true\n\t\t\t\t\t\t\t? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'\n\t\t\t\t\t\t\t: result.definitionReuploaded === false\n\t\t\t\t\t\t\t\t? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'\n\t\t\t\t\t\t\t\t: '[Compute/def-cache] n/a — reuse disabled or non-reusable definition'\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!result.fromCache) {\n\t\t\t\t\tconst errs = (result.response as { errors?: unknown[] })?.errors;\n\t\t\t\t\tconst errCount = Array.isArray(errs) ? errs.length : 0;\n\t\t\t\t\tif (errCount > 0) {\n\t\t\t\t\t\tconst cacheNote = config.cacheerroredsolves\n\t\t\t\t\t\t\t? 'cached (cacheerroredsolves on)'\n\t\t\t\t\t\t\t: 'not cached (cacheerroredsolves off) — re-run until fixed';\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/rhino-cache] ${errCount} GH error(s); ${cacheNote}. First: ${JSON.stringify((errs as unknown[])[0])}`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { client, scheduler };\n\t}\n\n\treturn {\n\t\tasync getClient(server, opts): Promise<CachedClient> {\n\t\t\tconst key = server.id;\n\t\t\tconst existing = cache.get(key);\n\t\t\tif (existing) {\n\t\t\t\t// Refresh LRU position.\n\t\t\t\tcache.delete(key);\n\t\t\t\tcache.set(key, existing);\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst inFlight = pending.get(key);\n\t\t\tif (inFlight) return inFlight;\n\n\t\t\t// Replay a recent failure instead of re-running the probe ladder. The\n\t\t\t// original error is rethrown verbatim so the caller still sees the real\n\t\t\t// cause (refused / unauthorized / timeout), not a generic cache miss.\n\t\t\tconst recentFailure = failed.get(key);\n\t\t\tif (recentFailure) {\n\t\t\t\tif (Date.now() - recentFailure.at < FAILED_BUILD_TTL_MS) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t`[Compute/client-cache] server ${key} failed to build <${FAILED_BUILD_TTL_MS}ms ago — replaying error`\n\t\t\t\t\t);\n\t\t\t\t\tthrow recentFailure.error;\n\t\t\t\t}\n\t\t\t\tfailed.delete(key);\n\t\t\t}\n\n\t\t\t// A build is a Rhino.Compute handshake — expensive, and repeated builds for\n\t\t\t// the same id mean churn (LRU thrash or config-rotation evictions).\n\t\t\tdebugLog(`[Compute/client-cache] miss — building warm client for server ${key}`);\n\t\t\tconst buildPromise = (async () => {\n\t\t\t\tlet entry: CachedClient;\n\t\t\t\ttry {\n\t\t\t\t\tentry = await build(server, opts?.definitionGuid);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tfailed.set(key, { error: err, at: Date.now() });\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tif (cache.size >= maxWarmComputeServers) {\n\t\t\t\t\tconst oldestKey = cache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/client-cache] LRU evicted warm client for server ${oldestKey} (cap ${maxWarmComputeServers})`\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcache.get(oldestKey)?.scheduler.dispose();\n\t\t\t\t\t\tcache.delete(oldestKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcache.set(key, entry);\n\t\t\t\treturn entry;\n\t\t\t})().finally(() => pending.delete(key));\n\t\t\tpending.set(key, buildPromise);\n\t\t\treturn buildPromise;\n\t\t},\n\n\t\tevict(id): void {\n\t\t\tconst key = id as string;\n\t\t\t// Drop any remembered failure too: `evict` means the connection details\n\t\t\t// changed, so the reason the last build failed no longer applies.\n\t\t\tfailed.delete(key);\n\t\t\tconst entry = cache.get(key);\n\t\t\tif (entry) {\n\t\t\t\tdebugLog(`[Compute/client-cache] evicted warm client for server ${key} (config change)`);\n\t\t\t\tentry.scheduler.dispose();\n\t\t\t\tcache.delete(key);\n\t\t\t}\n\t\t},\n\n\t\tsolveCacheStats(): SolveCacheStats {\n\t\t\tconst total: SolveCacheStats = {\n\t\t\t\twarmClients: cache.size,\n\t\t\t\tentries: 0,\n\t\t\t\tbytes: 0,\n\t\t\t\thits: 0,\n\t\t\t\tmisses: 0,\n\t\t\t\tevictions: 0\n\t\t\t};\n\t\t\tfor (const entry of cache.values()) {\n\t\t\t\tconst s = entry.scheduler.cacheStats();\n\t\t\t\ttotal.entries += s.entries;\n\t\t\t\ttotal.bytes += s.bytes;\n\t\t\t\ttotal.hits += s.hits;\n\t\t\t\ttotal.misses += s.misses;\n\t\t\t\ttotal.evictions += s.evictions;\n\t\t\t}\n\t\t\treturn total;\n\t\t},\n\n\t\tclearSolveCaches(): void {\n\t\t\tdebugLog(`[Compute/client-cache] cleared solve caches on ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.clearCache();\n\t\t},\n\n\t\tdisposeAll(): void {\n\t\t\tdebugLog(`[Compute/client-cache] disposing all ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.dispose();\n\t\t\tcache.clear();\n\t\t\tfailed.clear();\n\t\t}\n\t};\n}\n","/**\n * In-process cache of `.gh` definition bytes, keyed by immutable **version id**.\n *\n * Turns an eager `storage.get(fileKey)` on every solve request into a lazy\n * `DefinitionRef.load()`: the scheduler calls `load()` only when an upload is\n * unavoidable (a pointer-known re-solve never touches storage or this cache).\n *\n * The key MUST be the version id, never a `fileKey` — a delete-latest-then-\n * reupload can reuse a `fileKey` for different content, which would silently\n * serve one version's bytes for another with no diagnostic. Callers pass\n * `version.id`, and the returned `DefinitionRef.key` is that same id, so the\n * scheduler's own result/pointer cache keys on the same immutable identity.\n *\n * Eviction is LRU by total byte budget, not entry count — definitions range\n * from tens of KB to hundreds of MB, so a count cap would either pin\n * gigabytes or evict uselessly. `COMPUTE_DEFINITION_CACHE_MB` sets the\n * budget (see `createDefinitionByteCache`). No TTL: version ids are\n * immutable, so a cached entry can never go stale. An entry larger than the\n * whole budget is served but never retained.\n */\n\n/**\n * Per-`getOrLoad` outcome, observable after the scheduler has (or hasn't)\n * called `load()` — lets the solve pipeline emit a `def_bytes` Server-Timing\n * verdict (`skipped` / `hit` / `miss`).\n */\nexport interface ByteRefOutcome {\n\tloaded: boolean;\n\tfromCache: boolean;\n}\n\n/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */\nexport interface ByteCacheRef {\n\tkey: string;\n\tload: () => Promise<Uint8Array>;\n\toutcome: ByteRefOutcome;\n}\n\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\tgetOrLoad(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\tclear(): void;\n}\n\ninterface Entry {\n\tbytes: Uint8Array;\n}\n\n/**\n * @param maxBytes total retained-byte budget; `0` (or negative) disables caching\n * entirely — every `load()` calls the loader and nothing is retained.\n */\nexport function createDefinitionByteCache(maxBytes: number): DefinitionByteCache {\n\tconst budget = Math.max(0, Math.floor(maxBytes));\n\t// Map iteration order is insertion order, so it doubles as LRU order:\n\t// re-inserting a key on access moves it to the most-recently-used end.\n\tconst cache = new Map<string, Entry>();\n\tlet retainedBytes = 0;\n\tconst stats = { hits: 0, misses: 0, evictions: 0 };\n\n\tfunction touch(key: string, entry: Entry): void {\n\t\tcache.delete(key);\n\t\tcache.set(key, entry);\n\t}\n\n\tfunction evictToFit(): void {\n\t\twhile (retainedBytes > budget && cache.size > 0) {\n\t\t\tconst oldestKey = cache.keys().next().value as string;\n\t\t\tconst oldest = cache.get(oldestKey)!;\n\t\t\tcache.delete(oldestKey);\n\t\t\tretainedBytes -= oldest.bytes.byteLength;\n\t\t\tstats.evictions += 1;\n\t\t}\n\t}\n\n\tfunction store(key: string, bytes: Uint8Array): void {\n\t\tif (budget === 0) return;\n\t\t// Caching an entry bigger than the budget would evict everything\n\t\t// (including itself) on the next evictToFit — serve it, cache nothing.\n\t\tif (bytes.byteLength > budget) return;\n\t\tconst existing = cache.get(key);\n\t\tif (existing) retainedBytes -= existing.bytes.byteLength;\n\t\tcache.set(key, { bytes });\n\t\tretainedBytes += bytes.byteLength;\n\t\tevictToFit();\n\t}\n\n\treturn {\n\t\tgetOrLoad(versionId, load) {\n\t\t\tconst outcome: ByteRefOutcome = { loaded: false, fromCache: false };\n\t\t\treturn {\n\t\t\t\tkey: versionId,\n\t\t\t\toutcome,\n\t\t\t\tload: async () => {\n\t\t\t\t\tconst hit = cache.get(versionId);\n\t\t\t\t\tif (hit) {\n\t\t\t\t\t\ttouch(versionId, hit);\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn hit.bytes;\n\t\t\t\t\t}\n\t\t\t\t\tstats.misses += 1;\n\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\toutcome.fromCache = false;\n\t\t\t\t\tconst bytes = await load();\n\t\t\t\t\tstore(versionId, bytes);\n\t\t\t\t\treturn bytes;\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tstats() {\n\t\t\treturn {\n\t\t\t\thits: stats.hits,\n\t\t\t\tmisses: stats.misses,\n\t\t\t\tevictions: stats.evictions,\n\t\t\t\tentries: cache.size,\n\t\t\t\tbytes: retainedBytes\n\t\t\t};\n\t\t},\n\t\tclear() {\n\t\t\tcache.clear();\n\t\t\tretainedBytes = 0;\n\t\t}\n\t};\n}\n","/**\n * In-process single-flight — dogpile protection for every solve.\n *\n * Always on, not gated behind a result cache being configured: the dogpile is worst\n * when nothing else is caching, since N concurrent identical solves each pay a full\n * Rhino round trip (the scheduler doesn't coalesce in-flight requests itself, and\n * Rhino's `cachesolve` still costs a round trip per repeat). An earlier version\n * gated this on cache config and so disabled it in exactly the deployments most\n * exposed to a cold-key stampede — hot public definition plus a deploy.\n *\n * Per app instance, above `ISolveResultCache`. No cross-instance lease (Redis\n * `SET NX`) yet.\n *\n * The shared promise resolves to one value for every waiter, so `work` must return\n * something safe to share by reference — the solve pipeline's envelope qualifies\n * because the app serializes it per response.\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Coalesces concurrent calls under the same key: the first caller runs `work`,\n\t * later callers for that key await the same promise, and the key frees as soon\n\t * as it settles.\n\t *\n\t * `onWaiterJoined` fires only on the owner's call, only while `work` is still\n\t * running, each time another caller joins. Ownership changes what the owner's\n\t * abort signal should mean: a solo run can cancel on its own client's\n\t * disconnect, but a shared run can't without 499-ing every waiter.\n\t */\n\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T>;\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\tonJoin?: (key: string) => void;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\tinterface Flight {\n\t\tpromise: Promise<unknown>;\n\t\tnotifyOwner?: () => void;\n\t}\n\tconst inflight = new Map<string, Flight>();\n\n\treturn {\n\t\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T> {\n\t\t\tconst existing = inflight.get(key);\n\t\t\tif (existing) {\n\t\t\t\toptions.onJoin?.(key);\n\t\t\t\texisting.notifyOwner?.();\n\t\t\t\treturn existing.promise as Promise<T>;\n\t\t\t}\n\n\t\t\t// Wrapping in an async IIFE means a synchronous throw in `work` still\n\t\t\t// rejects `p` instead of escaping `run` directly.\n\t\t\tconst flight: Flight = { notifyOwner: onWaiterJoined, promise: undefined! };\n\t\t\tconst p = (async () => work())().finally(() => {\n\t\t\t\t// Clear it here so a join after `work` settles can't fire it late.\n\t\t\t\tflight.notifyOwner = undefined;\n\t\t\t\t// Guard against deleting a key a later caller has already re-taken.\n\t\t\t\tif (inflight.get(key) === flight) inflight.delete(key);\n\t\t\t});\n\t\t\tflight.promise = p;\n\t\t\tinflight.set(key, flight);\n\t\t\treturn p;\n\t\t},\n\t\tinFlight(): number {\n\t\t\treturn inflight.size;\n\t\t}\n\t};\n}\n","/**\n * Facade over the warm-client cache, definition-byte cache, single-flight\n * coalescing, and the solve pipeline — the primitives an app route would\n * otherwise wire up by hand.\n *\n * Doesn't read env itself: `@selvajs/server` owns `resolveComputeLimits` and\n * depends on `@selvajs/solve`, so calling it from here would be a circular\n * import. Callers resolve their own limits and pass in the fields below.\n *\n * Stays out of auth, DB reads, share tokens, rate limiting, metrics, and\n * compute-server selection — all app policy, supplied per-call via\n * `SolveEngineSolveArgs.server`.\n */\n\nimport { isDefinitionRef, type DefinitionRef, type SolveDefinition } from '@selvajs/compute/core';\nimport { stableStringify, type SelvaEventTarget } from '@selvajs/compute/grasshopper';\nimport { NoopLogger, type ILogger } from '@selvajs/platform';\nimport {\n\tcreateClientCache,\n\ttype CachedClient,\n\ttype ClientCache,\n\ttype ClientCacheDebug,\n\ttype ResolvedServer,\n\ttype SolveCacheStats\n} from './client-cache.js';\nimport {\n\tcreateDefinitionByteCache,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype DefinitionByteCache\n} from './definition-byte-cache.js';\nimport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight\n} from './solve-cache-single-flight.js';\nimport { buildSolveInputTree } from './transform-input.js';\nimport {\n\tadaptEnvelopeToEncoding,\n\trunSolvePipeline,\n\ttype PipelineInput,\n\ttype SolveOutcome\n} from './solve-pipeline.js';\n\n/**\n * Subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs.\n * Pass the resolved object through as-is — extra fields are ignored.\n */\nexport interface SolveEngineLimits {\n\tsolveDeadlineMs: number;\n\tcomputeResponseMaxBytes: number;\n\tcomputeReuseDefinitionCache: boolean;\n\tcomputeServerCachesolve: boolean;\n\tcomputeCacheErroredSolves: boolean;\n\tcomputeMaxQueueDepth: number;\n\tcomputeQueueWaitMs: number;\n\tcomputeDefinitionCacheBytes: number;\n\tcomputeSolveCacheBytes: number;\n}\n\nexport interface SolveEngineOptions {\n\tlimits: SolveEngineLimits;\n\tlogger?: ILogger;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */\n\tmaxWarmComputeServers?: number;\n\t/** Concise cache/timing logs when truthy; `'verbose'` also dumps full lib-level requests/responses. Default off. */\n\tdebug?: ClientCacheDebug;\n\tonDebugLog?: (message: string) => void;\n\t/** Fired when a caller joins an already-in-flight solve instead of running its own. */\n\tonSolveCoalesced?: (key: string) => void;\n}\n\n/**\n * Accepts every form `runSolvePipeline` does, plus `{versionId, load}` sugar\n * that builds (and caches) a `ByteCacheRef` internally. A `ByteCacheRef`\n * obtained ahead of time from `engine.definitionRef()` (e.g. to read bytes for\n * schema extraction before solving) is passed through as-is — detected by its\n * `outcome` field, which a plain `DefinitionRef` never has.\n */\nexport type SolveEngineDefinitionSource =\n\t| Uint8Array\n\t| string\n\t| DefinitionRef\n\t| ByteCacheRef\n\t| { versionId: string; load: () => Promise<Uint8Array> };\n\nexport interface SolveEngineSolveArgs {\n\tserver: ResolvedServer;\n\tdefinitionSource: SolveEngineDefinitionSource;\n\t/**\n\t * Coalesce-key identity for a raw `Uint8Array`/`string` source, which has\n\t * no natural identity the way a `DefinitionRef`'s `.key` does. Required in\n\t * that case — `solve()` throws rather than silently hashing the bytes.\n\t * Ignored for the other source forms.\n\t */\n\tdefinitionKey?: string;\n\tinputs: PipelineInput[];\n\tvalues: Record<string, unknown>;\n\tsignal: AbortSignal;\n\tacceptEncoding?: string;\n\t/** Stamped as `X-Selva-Definition` on this solve's client. */\n\tdefinitionGuid?: string;\n\tloadStartMs?: number;\n\tdefLoadMs?: number;\n\tprepMarks?: [string, number][];\n\t/**\n\t * Per-solve live-event callback. A coalesced waiter shares the first caller's\n\t * compute call, so only that caller's target reaches the definition; the\n\t * others still get the server-emitted start/end events on their own stream.\n\t */\n\tselvaEvents?: SelvaEventTarget;\n}\n\nexport interface FrameworkAgnosticResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string | Uint8Array;\n}\n\nexport interface SolveEngineStats {\n\tclient: SolveCacheStats;\n\tdefinitionBytes: ByteCacheStats;\n\tcoalescing: { inFlight: number };\n}\n\nfunction isByteCacheRef(source: SolveEngineDefinitionSource): source is ByteCacheRef {\n\treturn (\n\t\ttypeof source === 'object' &&\n\t\tsource !== null &&\n\t\t!(source instanceof Uint8Array) &&\n\t\t'outcome' in source\n\t);\n}\n\nfunction isVersionLoadPair(\n\tsource: SolveEngineDefinitionSource\n): source is { versionId: string; load: () => Promise<Uint8Array> } {\n\treturn (\n\t\ttypeof source === 'object' &&\n\t\tsource !== null &&\n\t\t!(source instanceof Uint8Array) &&\n\t\t!('outcome' in source) &&\n\t\t!('key' in source) &&\n\t\t'versionId' in source\n\t);\n}\n\nexport class SolveEngine {\n\tprivate readonly limits: SolveEngineLimits;\n\tprivate readonly clientCache: ClientCache;\n\tprivate readonly byteCache: DefinitionByteCache;\n\tprivate readonly singleFlight: SolveCacheSingleFlight;\n\n\tconstructor(options: SolveEngineOptions) {\n\t\tthis.limits = options.limits;\n\t\tconst logger = options.logger ?? new NoopLogger();\n\n\t\tthis.clientCache = createClientCache({\n\t\t\tsolveDeadlineMs: this.limits.solveDeadlineMs,\n\t\t\tcachesolve: this.limits.computeServerCachesolve,\n\t\t\tcacheerroredsolves: this.limits.computeCacheErroredSolves,\n\t\t\treuseServerDefinitionCache: this.limits.computeReuseDefinitionCache,\n\t\t\tmaxQueueDepth: this.limits.computeMaxQueueDepth,\n\t\t\tqueueWaitMs: this.limits.computeQueueWaitMs,\n\t\t\tresponseCacheMaxBytes: this.limits.computeSolveCacheBytes,\n\t\t\tmaxResponseBytes: this.limits.computeResponseMaxBytes,\n\t\t\tdebug: options.debug ?? false,\n\t\t\tmaxWarmComputeServers: options.maxWarmComputeServers,\n\t\t\tonDebugLog:\n\t\t\t\toptions.onDebugLog ??\n\t\t\t\t((message) => logger.debug(message, { component: 'Compute/client-cache' }))\n\t\t});\n\n\t\tthis.byteCache = createDefinitionByteCache(this.limits.computeDefinitionCacheBytes);\n\n\t\tthis.singleFlight = createSolveCacheSingleFlight({\n\t\t\tonJoin: options.onSolveCoalesced\n\t\t});\n\t}\n\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient> {\n\t\treturn this.clientCache.getClient(server, opts);\n\t}\n\n\tdefinitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef {\n\t\treturn this.byteCache.getOrLoad(versionId, load);\n\t}\n\n\tevictServer(id: string): void {\n\t\tthis.clientCache.evict(id);\n\t}\n\n\tclearSolveCaches(): void {\n\t\tthis.clientCache.clearSolveCaches();\n\t}\n\n\t/**\n\t * Drop every retained `.gh` byte entry. Safe at any time: entries key on an\n\t * immutable version id, so clearing costs a storage re-read and nothing else.\n\t */\n\tclearDefinitionCache(): void {\n\t\tthis.byteCache.clear();\n\t}\n\n\tstats(): SolveEngineStats {\n\t\treturn {\n\t\t\tclient: this.clientCache.solveCacheStats(),\n\t\t\tdefinitionBytes: this.byteCache.stats(),\n\t\t\tcoalescing: { inFlight: this.singleFlight.inFlight() }\n\t\t};\n\t}\n\n\tasync solve(args: SolveEngineSolveArgs): Promise<SolveOutcome> {\n\t\tconst {\n\t\t\tdefinitionSource: resolvedSource,\n\t\t\tbyteRefOutcome,\n\t\t\tidentity\n\t\t} = this.resolveDefinitionSource(args);\n\n\t\tconst client = await this.clientCache.getClient(args.server, {\n\t\t\tdefinitionGuid: args.definitionGuid\n\t\t});\n\t\tconst inputTree = buildSolveInputTree(args.inputs, args.values);\n\t\tconst coalesceKey = `${identity}:${args.server.id}:${stableStringify(inputTree)}`;\n\n\t\t// A coalesced solve is shared, so one caller's disconnect can't 499 every\n\t\t// other waiter. Only propagate abort if no one else has joined yet.\n\t\tconst abortController = new AbortController();\n\t\tlet hasWaiters = false;\n\t\tconst onCallerAbort = () => {\n\t\t\tif (!hasWaiters) abortController.abort();\n\t\t};\n\t\t// `once: true` doesn't detach on a normal completion, and the caller's signal\n\t\t// outlives this call — a request signal fires `abort` after the response is\n\t\t// consumed, and a session-scoped one never settles at all. Without the\n\t\t// `finally` below, every solve leaks a listener onto it.\n\t\targs.signal.addEventListener('abort', onCallerAbort, { once: true });\n\n\t\tconst acceptEncoding = args.acceptEncoding ?? '';\n\t\tlet outcome: SolveOutcome;\n\t\ttry {\n\t\t\toutcome = await this.singleFlight.run(\n\t\t\t\tcoalesceKey,\n\t\t\t\t() =>\n\t\t\t\t\trunSolvePipeline({\n\t\t\t\t\t\tdefinitionSource: resolvedSource,\n\t\t\t\t\t\tbyteRefOutcome,\n\t\t\t\t\t\tinputs: args.inputs,\n\t\t\t\t\t\tvalues: args.values,\n\t\t\t\t\t\tinputTree,\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tresponseMaxBytes: this.limits.computeResponseMaxBytes,\n\t\t\t\t\t\tsolveDeadlineMs: this.limits.solveDeadlineMs,\n\t\t\t\t\t\tacceptEncoding,\n\t\t\t\t\t\tselvaEvents: args.selvaEvents,\n\t\t\t\t\t\tsignal: abortController.signal,\n\t\t\t\t\t\tloadStartMs: args.loadStartMs ?? performance.now(),\n\t\t\t\t\t\tdefLoadMs: args.defLoadMs ?? 0,\n\t\t\t\t\t\tprepMarks: args.prepMarks\n\t\t\t\t\t}),\n\t\t\t\t() => {\n\t\t\t\t\thasWaiters = true;\n\t\t\t\t}\n\t\t\t);\n\t\t} finally {\n\t\t\targs.signal.removeEventListener('abort', onCallerAbort);\n\t\t}\n\n\t\tif (outcome.kind !== 'ok') return outcome;\n\n\t\t// The envelope may be shared across waiters; re-key it to this caller's\n\t\t// own Accept-Encoding so each one gets the right wire form.\n\t\tconst wire = adaptEnvelopeToEncoding(outcome.envelope, acceptEncoding);\n\t\treturn {\n\t\t\t...outcome,\n\t\t\tenvelope: { ...outcome.envelope, body: wire.body, headers: wire.headers }\n\t\t};\n\t}\n\n\ttoResponse(\n\t\toutcome: SolveOutcome,\n\t\topts?: { onError?: (status: number, body: { message: string; retryAfter?: number }) => never }\n\t): FrameworkAgnosticResponse {\n\t\tconst fail = (\n\t\t\tstatus: number,\n\t\t\tbody: { message: string; retryAfter?: number }\n\t\t): FrameworkAgnosticResponse => {\n\t\t\tif (opts?.onError) opts.onError(status, body);\n\t\t\tconst headers: Record<string, string> = { 'Content-Type': 'application/json' };\n\t\t\tif (body.retryAfter !== undefined) headers['Retry-After'] = String(body.retryAfter);\n\t\t\treturn { status, headers, body: JSON.stringify(body) };\n\t\t};\n\n\t\tswitch (outcome.kind) {\n\t\t\tcase 'ok':\n\t\t\t\treturn { status: 200, headers: outcome.envelope.headers, body: outcome.envelope.body };\n\t\t\tcase 'timeout':\n\t\t\t\treturn fail(504, { message: outcome.message });\n\t\t\tcase 'client_abort':\n\t\t\t\treturn fail(499, { message: 'Client closed request' });\n\t\t\tcase 'too_large':\n\t\t\t\treturn fail(413, {\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'Solve result is too large to return. This usually means a file output exceeds the supported size.'\n\t\t\t\t});\n\t\t\tcase 'shed':\n\t\t\t\treturn fail(503, { message: outcome.message, retryAfter: outcome.retryAfterSeconds });\n\t\t\tcase 'compute_error':\n\t\t\t\tthrow outcome.error;\n\t\t}\n\t}\n\n\ttoWebResponse(outcome: SolveOutcome): Response {\n\t\tconst r = this.toResponse(outcome);\n\t\treturn new Response(typeof r.body === 'string' ? r.body : new Uint8Array(r.body), {\n\t\t\tstatus: r.status,\n\t\t\theaders: r.headers\n\t\t});\n\t}\n\n\tprivate resolveDefinitionSource(args: SolveEngineSolveArgs): {\n\t\tdefinitionSource: SolveDefinition;\n\t\tbyteRefOutcome?: ByteCacheRef['outcome'];\n\t\tidentity: string;\n\t} {\n\t\tconst source = args.definitionSource;\n\n\t\tif (isVersionLoadPair(source)) {\n\t\t\tconst ref = this.byteCache.getOrLoad(source.versionId, source.load);\n\t\t\treturn { definitionSource: ref, byteRefOutcome: ref.outcome, identity: ref.key };\n\t\t}\n\n\t\tif (isByteCacheRef(source)) {\n\t\t\treturn { definitionSource: source, byteRefOutcome: source.outcome, identity: source.key };\n\t\t}\n\n\t\tif (isDefinitionRef(source)) {\n\t\t\treturn { definitionSource: source, identity: source.key };\n\t\t}\n\n\t\t// Raw Uint8Array | string — no natural identity; require the caller's own.\n\t\tif (!args.definitionKey) {\n\t\t\tthrow new Error(\n\t\t\t\t'SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.'\n\t\t\t);\n\t\t}\n\t\treturn { definitionSource: source, identity: args.definitionKey };\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","names":["undiciFetch"],"sources":["../src/server/concurrency-probe.ts","../src/server/long-fetch.ts","../src/server/server-limits.ts","../src/server/client-cache.ts","../src/server/definition-byte-cache.ts","../src/server/response-encoding.ts","../src/server/server-timing.ts","../src/server/solve-cache-single-flight.ts","../src/server/transform-input.ts","../src/server/solve-pipeline.ts","../src/server/solve-engine.ts"],"sourcesContent":["import type { GrasshopperClient } from '@selvajs/compute/grasshopper';\n\n/**\n * Fallback concurrency while the first probe is still in flight, and the value\n * kept whenever a probe fails or reports 0 — never send more than a single\n * solve to a server whose real capacity is unknown.\n */\nconst FALLBACK_CONCURRENCY = 1;\n\n/**\n * How stale the compute server's child count may get before a solve triggers a\n * re-read. Short enough that a resized pool is picked up within a few solves,\n * long enough that a busy server is probed rarely — the probe rides on solve\n * completion, so an idle server is never polled at all.\n */\nconst CONCURRENCY_PROBE_INTERVAL_MS = 5 * 60 * 1000;\n\nexport interface ConcurrencyProbe {\n\treadonly maxConcurrent: number;\n\t/** One blocking read at connect, so the first solves already use the right cap. */\n\trefresh(): Promise<void>;\n\t/** Piggybacks on real traffic: at most once per window, and never on the request path. */\n\trefreshIfDue(scheduler: { setMaxConcurrent(n: number): void }): void;\n}\n\n/**\n * rhino.compute hands out children in strict rotation and never reports \"busy\", so nothing\n * downstream enforces a concurrency cap — send more than it has children and requests silently\n * double up on one. Rhino.Compute is the only source of truth for its own child count, so this\n * always probes rather than trusting an operator-supplied guess.\n *\n * The count is not static: children exit on their own under `-idlespan`, and `/launch-child` +\n * `/recycle-children` resize the pool at runtime. So the probe re-runs after solves rather than\n * only at connect.\n */\nexport function createConcurrencyProbe(\n\tclient: Pick<GrasshopperClient, 'serverStats'>,\n\tdebugLog: (message: string) => void\n): ConcurrencyProbe {\n\tlet maxConcurrent = FALLBACK_CONCURRENCY;\n\tlet lastProbeAt = 0;\n\tlet probing = false;\n\n\t/** `initialize: false` keeps this from spawning children on an idle server, so a cold one reports 0. */\n\tasync function refresh(scheduler?: { setMaxConcurrent(n: number): void }) {\n\t\tif (probing) return;\n\t\tprobing = true;\n\t\ttry {\n\t\t\tconst children = await Promise.resolve(\n\t\t\t\tclient.serverStats?.getActiveChildren({ initialize: false })\n\t\t\t).catch(() => null);\n\t\t\tlastProbeAt = Date.now();\n\t\t\tconst next = !children || children <= 0 ? FALLBACK_CONCURRENCY : children;\n\t\t\tif (next === maxConcurrent) return;\n\t\t\tdebugLog(\n\t\t\t\t`[Compute/client-cache] concurrency ${maxConcurrent} → ${next} (server child count)`\n\t\t\t);\n\t\t\tmaxConcurrent = next;\n\t\t\tscheduler?.setMaxConcurrent(next);\n\t\t} finally {\n\t\t\tprobing = false;\n\t\t}\n\t}\n\n\treturn {\n\t\tget maxConcurrent() {\n\t\t\treturn maxConcurrent;\n\t\t},\n\t\trefresh: () => refresh(),\n\t\trefreshIfDue(scheduler) {\n\t\t\tif (Date.now() - lastProbeAt >= CONCURRENCY_PROBE_INTERVAL_MS) void refresh(scheduler);\n\t\t}\n\t};\n}\n","import { Agent, fetch as undiciFetch } from 'undici';\n\n/** undici's default `headersTimeout` and `bodyTimeout`, which `timeoutMs` can't extend. */\nconst UNDICI_DEFAULT_LIMIT_MS = 300_000;\n\n/** Both off, not raised: the scheduler's `timeoutMs` stays the one deadline on a solve. */\nconst unlimited = new Agent({ headersTimeout: 0, bodyTimeout: 0 });\n\n// npm undici's own fetch, not the global one: a dispatcher has to match the undici that calls it,\n// and Node's bundled copy is a different version.\n//\n// A call with no signal has no deadline of its own (`getIO` is one), so it keeps undici's limit:\n// switching the timeouts off for it would let a hung server hang a page load forever.\nconst unlimitedFetch = ((input: string, init?: { signal?: AbortSignal }) =>\n\tundiciFetch(input, {\n\t\t...init,\n\t\tsignal: init?.signal ?? AbortSignal.timeout(UNDICI_DEFAULT_LIMIT_MS),\n\t\tdispatcher: unlimited\n\t})) as unknown as typeof fetch;\n\n/** The fetch for a solve deadline: `undefined` means the global one reaches it. */\nexport function fetchForDeadline(deadlineMs: number): typeof fetch | undefined {\n\treturn deadlineMs >= UNDICI_DEFAULT_LIMIT_MS ? unlimitedFetch : undefined;\n}\n","import type { ServerLimits } from '@selvajs/compute/grasshopper';\n\n/** Rhino.Compute's own default `--timeout`, for a server that can't report its own. */\nexport const DEFAULT_SOLVE_DEADLINE_MS = 100_000;\n\n/**\n * Ceiling on any solve deadline, whatever the server reports. Bounds how long a request, its\n * live-event token and a delegated session stay open; past an hour a solve belongs in a job.\n */\nexport const MAX_SOLVE_DEADLINE_MS = 60 * 60_000;\n\n/** Selva waits this much past the server's timeout, so Compute's own error arrives first. */\nconst DEADLINE_SLACK_MS = 5_000;\n\n/** What Selva holds a solve to on one compute server. */\nexport interface SolveLimits {\n\tsolveDeadlineMs: number;\n\t/** `null` when the server didn't say: its own 413 is then the only check. */\n\tmaxRequestBytes: number | null;\n\t/**\n\t * Send a pointer to the server's cached definition instead of the `.gh`. Only on a server that\n\t * answers a stale pointer with `definition_not_cached`: one that doesn't returns empty geometry.\n\t */\n\treuseDefinitionCache: boolean;\n}\n\nexport function solveLimitsFor(reported: ServerLimits | null): SolveLimits {\n\tif (!reported) {\n\t\treturn {\n\t\t\tsolveDeadlineMs: DEFAULT_SOLVE_DEADLINE_MS,\n\t\t\tmaxRequestBytes: null,\n\t\t\treuseDefinitionCache: false\n\t\t};\n\t}\n\treturn {\n\t\tsolveDeadlineMs: Math.min(\n\t\t\treported.timeoutSeconds * 1000 + DEADLINE_SLACK_MS,\n\t\t\tMAX_SOLVE_DEADLINE_MS\n\t\t),\n\t\tmaxRequestBytes: reported.maxRequestBytes,\n\t\treuseDefinitionCache: reported.signalsStaleDefinition\n\t};\n}\n\nexport function sameSolveLimits(a: SolveLimits, b: SolveLimits): boolean {\n\treturn (\n\t\ta.solveDeadlineMs === b.solveDeadlineMs &&\n\t\ta.maxRequestBytes === b.maxRequestBytes &&\n\t\ta.reuseDefinitionCache === b.reuseDefinitionCache\n\t);\n}\n","import {\n\tGrasshopperClient,\n\ttype GrasshopperComputeConfig,\n\ttype SolveScheduler\n} from '@selvajs/compute/grasshopper';\nimport { ComputeError, enableDebugLogging, ErrorCodes } from '@selvajs/compute/core';\n\nimport { createConcurrencyProbe } from './concurrency-probe';\nimport { fetchForDeadline } from './long-fetch';\nimport {\n\tMAX_SOLVE_DEADLINE_MS,\n\tsameSolveLimits,\n\tsolveLimitsFor,\n\ttype SolveLimits\n} from './server-limits';\n\n// ============================================================================\n// Shared compute client cache\n// ============================================================================\n//\n// One warm `GrasshopperClient` (+ its `SolveScheduler`) per distinct compute\n// server, keyed by the server's `id`, never its URL — a rotated URL/apiKey\n// keeps the same key with stale connection details, so the config-write path\n// MUST call `evict(id)` when a server's config changes.\n//\n// Per-server LRU, not a single shared client: definitions can pin different\n// servers, so two definitions on two servers keep two warm clients, with\n// churn from one-off servers bounded by `maxWarmComputeServers`.\n//\n// Both hot paths share this cache — the solve endpoint uses the entry's\n// `scheduler`; the definition-viewer render path uses the entry's `client`\n// for `getIO` — so a definition that was just solved renders from the same\n// warm client instead of re-handshaking Rhino.Compute per page load.\n\n/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */\nexport interface ResolvedServer {\n\tid: string;\n\tserverUrl: string;\n\t/** Sent as the `RhinoComputeKey` header. */\n\tapiKey?: string;\n}\n\nexport interface CachedClient {\n\tclient: GrasshopperClient;\n\tscheduler: SolveScheduler;\n\t/** Read from the server when the client was built; a changed server rebuilds the client. */\n\tlimits: SolveLimits;\n}\n\n/**\n * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs\n * (`onDebugLog`), `'verbose'` also enables full lib-level request/response\n * dumps (incl. geometry) — a superset of `true`, not an independent setting.\n */\nexport type ClientCacheDebug = boolean | 'verbose';\n\n/** Config injected by the consuming app, so this module stays env-agnostic and testable. */\nexport interface ClientCacheConfig {\n\tcachesolve: boolean;\n\t/**\n\t * Max solves that may wait in the FIFO queue, excluding in-flight solves\n\t * (capped at `maxConcurrent`, itself driven by the server's probed child\n\t * count). `0` = unbounded (`ComputeLimits.computeMaxQueueDepth`); a full\n\t * queue rejects new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Max ms a solve may sit queued before executing; `0` = no deadline\n\t * (`ComputeLimits.computeQueueWaitMs`). Too long a wait rejects with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache. Applies per warm\n\t * client — worst-case heap is this × `maxWarmComputeServers`. `0` disables\n\t * the cache (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\t/**\n\t * Largest compute response body read; past it the download is cancelled (`too_large`).\n\t * Default: no limit.\n\t */\n\tmaxResponseBytes?: number;\n\tdebug: ClientCacheDebug;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. */\n\tmaxWarmComputeServers?: number;\n\t/** Sink for the debug lines. `onDebugLog` only fires when `debug` is not `false`. */\n\tonDebugLog?: (message: string) => void;\n}\n\nexport interface ClientCache {\n\t/**\n\t * Get (or create) the warm client + scheduler for a resolved compute server, keyed by its `id`.\n\t *\n\t * @throws {ComputeError} `ABORTED` when `evict` or `disposeAll` overtakes the build.\n\t */\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient>;\n\t/**\n\t * Drops the warm client for `id`, so the next request rebuilds, and disposes it once idle. A\n\t * build in flight for `id` is abandoned.\n\t */\n\tevict(id: string): void;\n\t/** Solve-cache counters summed across every warm client. Counters die with the client that owns them, so totals can fall over time. */\n\tsolveCacheStats(): SolveCacheStats;\n\t/**\n\t * Drop every retained solve result, keeping the warm clients (and their\n\t * connections and server-side definition pointers) intact. Nothing expires\n\t * these on its own — the byte budget is the only other pressure — so this is\n\t * the operator's release valve when a definition's inputs no longer describe\n\t * its output, e.g. it reads an external source that has since changed.\n\t */\n\tclearSolveCaches(): void;\n\t/** Dispose every warm client now, running solves included. Test seam / shutdown hook. */\n\tdisposeAll(): void;\n}\n\nexport interface SolveCacheStats {\n\twarmClients: number;\n\tentries: number;\n\tbytes: number;\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped under size/byte pressure (not replacement or manual clears). */\n\tevictions: number;\n}\n\nconst DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;\n\n/**\n * How often a warm client re-reads its server's limits, riding on solve completion like the\n * concurrency probe. An operator who changes `--timeout` sees it within this window.\n */\nconst LIMITS_RECHECK_MS = 5 * 60 * 1000;\n\n/**\n * How long a failed build is remembered before the next `getClient` retries the\n * handshake. Without this, every solve against a down server pays the full probe\n * ladder again, so a user clicking Solve twice waits twice.\n *\n * Short on purpose: this is a convenience for the rapid-retry case, not a\n * circuit breaker. An operator who starts the compute VM should not have to wait\n * out a long penalty window before the app notices.\n */\nconst FAILED_BUILD_TTL_MS = 5000;\n\n/**\n * A build failure worth replaying to the next caller, with the time it happened\n * so {@link FAILED_BUILD_TTL_MS} can expire it.\n */\ninterface FailedBuild {\n\terror: unknown;\n\tat: number;\n}\n\ninterface PendingBuild {\n\tpromise: Promise<CachedClient>;\n\t/** Set when the connection details this build started from stopped being true. */\n\ttoken: { stale: boolean };\n}\n\ninterface SettledSolve {\n\tfromCache?: boolean;\n\tdurationMs: number;\n\tdefinitionReuploaded?: boolean;\n\tresponse?: { errors?: unknown };\n}\n\nfunction describeSettledSolve(result: SettledSolve): string[] {\n\tconst verdict = result.fromCache\n\t\t? 'HIT — served from cache (no compute call)'\n\t\t: 'miss — went to Rhino.Compute';\n\tconst lines = [`[Compute/selva-cache] ${verdict} (${Math.round(result.durationMs)}ms)`];\n\tif (result.fromCache) return lines;\n\n\tlines.push(\n\t\tresult.definitionReuploaded === true\n\t\t\t? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'\n\t\t\t: result.definitionReuploaded === false\n\t\t\t\t? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'\n\t\t\t\t: '[Compute/def-cache] n/a — reuse disabled or non-reusable definition'\n\t);\n\tconst errors = result.response?.errors;\n\tif (Array.isArray(errors) && errors.length > 0) {\n\t\t// A count: solver text can quote user input, and logs are out of erasure's reach.\n\t\tlines.push(`[Compute/rhino-cache] ${errors.length} GH error(s); not cached by Rhino.Compute.`);\n\t}\n\treturn lines;\n}\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxWarmComputeServers = config.maxWarmComputeServers ?? DEFAULT_MAX_WARM_COMPUTE_SERVERS;\n\tconst cache = new Map<string, CachedClient>();\n\t// Builds in flight, keyed like `cache`. Concurrent `getClient` calls for the\n\t// same id must share ONE build: without this, both miss, both handshake, and\n\t// the loser's entry is overwritten in the map without ever being disposed.\n\tconst pending = new Map<string, PendingBuild>();\n\t// Recent build failures, keyed like `cache`. Cleared by `evict` so a config\n\t// fix takes effect immediately rather than waiting out the TTL.\n\tconst failed = new Map<string, FailedBuild>();\n\t// Out of `cache`, waiting for their running solves to finish.\n\tconst draining = new Set<CachedClient>();\n\n\tif (config.debug === 'verbose') enableDebugLogging();\n\n\tconst debugLog = (message: string) => {\n\t\tif (config.debug) config.onDebugLog?.(message);\n\t};\n\n\tfunction isIdle({ scheduler }: CachedClient) {\n\t\treturn scheduler.inFlightCount === 0 && scheduler.queueDepth === 0;\n\t}\n\n\t/** Disposing a busy scheduler aborts its solves, so a config write would fail them all. */\n\tfunction retire(entry: CachedClient) {\n\t\tif (isIdle(entry)) entry.scheduler.dispose();\n\t\telse draining.add(entry);\n\t}\n\n\tfunction finishDraining(entry: CachedClient) {\n\t\tif (!draining.has(entry) || !isIdle(entry)) return;\n\t\tdraining.delete(entry);\n\t\tentry.scheduler.dispose();\n\t}\n\n\tasync function build(server: ResolvedServer, definitionGuid?: string): Promise<CachedClient> {\n\t\tconst debugVerbose = config.debug === 'verbose';\n\t\t// The deadline comes from the server, read only once the client exists: pick the fetch\n\t\t// for the longest one. Calls without a signal keep undici's limits either way.\n\t\tconst longFetch = fetchForDeadline(MAX_SOLVE_DEADLINE_MS);\n\n\t\tconst clientConfig: GrasshopperComputeConfig = {\n\t\t\tserverUrl: server.serverUrl,\n\t\t\tapiKey: server.apiKey,\n\t\t\t// Lib-level debug is the verbose one; keep it on the verbose flag so the\n\t\t\t// concise cache logs can run without the noise.\n\t\t\tdebug: debugVerbose,\n\t\t\tcachesolve: config.cachesolve,\n\t\t\tmaxResponseBytes: config.maxResponseBytes,\n\t\t\t...(longFetch && { fetch: longFetch })\n\t\t};\n\n\t\tif (config.debug) {\n\t\t\tclientConfig.onServerTiming = (t) => {\n\t\t\t\tif (t.cacheHit) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t'[Compute/rhino-cache] solve-cache (cachesolve): HIT (cached result, solve skipped)'\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst decode = t.decode ?? 0;\n\t\t\t\tconst solve = t.solve ?? 0;\n\t\t\t\tconst encode = t.encode ?? 0;\n\t\t\t\t// Heuristic threshold: a genuine parse is many ms; a reused definition is\n\t\t\t\t// sub-millisecond. Only meaningful relative to the first (cold) solve.\n\t\t\t\tconst defCache =\n\t\t\t\t\tdecode <= 1 ? 'HIT (definition reused, no re-upload/parse)' : 'miss (full parse)';\n\t\t\t\tconst solveCache = config.cachesolve ? 'miss (solved fresh)' : 'off';\n\t\t\t\tconst ran = t.recomputed ? ` | ran ${t.recomputed.ran}/${t.recomputed.total} objects` : '';\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/rhino-cache] decode=${decode}ms solve=${solve}ms encode=${encode}ms ` +\n\t\t\t\t\t\t`| definition-cache: ${defCache} | solve-cache (cachesolve): ${solveCache}${ran}`\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\n\t\t// Baked at client-create time because the scheduler has no per-request\n\t\t// header hook.\n\t\tif (definitionGuid) {\n\t\t\tclientConfig.headers = { 'X-Selva-Definition': definitionGuid };\n\t\t}\n\n\t\tconst client = await GrasshopperClient.create(clientConfig);\n\n\t\tconst probe = createConcurrencyProbe(client, debugLog);\n\t\tconst readLimits = async () =>\n\t\t\tsolveLimitsFor(\n\t\t\t\t(await Promise.resolve(client.serverStats?.getLimits?.()).catch(() => null)) ?? null\n\t\t\t);\n\t\tconst [limits] = await Promise.all([readLimits(), probe.refresh()]);\n\t\tdebugLog(\n\t\t\t`[Compute/client-cache] server ${server.id}: deadline ${limits.solveDeadlineMs}ms, ` +\n\t\t\t\t`request cap ${limits.maxRequestBytes ?? 'not reported'}, pointer reuse ${limits.reuseDefinitionCache}`\n\t\t);\n\t\tlet limitsCheckedAt = Date.now();\n\t\tlet checkingLimits = false;\n\t\tconst recheckLimitsIfDue = () => {\n\t\t\tif (checkingLimits || Date.now() - limitsCheckedAt < LIMITS_RECHECK_MS) return;\n\t\t\tcheckingLimits = true;\n\t\t\tvoid readLimits()\n\t\t\t\t.then((next) => {\n\t\t\t\t\tlimitsCheckedAt = Date.now();\n\t\t\t\t\tif (sameSolveLimits(next, limits)) return;\n\t\t\t\t\tdebugLog(`[Compute/client-cache] server ${server.id} limits changed — rebuilding`);\n\t\t\t\t\tif (cache.get(server.id) === entry) evictEntry(server.id);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tcheckingLimits = false;\n\t\t\t\t});\n\t\t};\n\n\t\tconst scheduler = client.createScheduler({\n\t\t\tmode: 'queue',\n\t\t\t// Queue mode defaults maxConcurrent to 1, serializing every solve on\n\t\t\t// this server through a single slot while the compute VM's other\n\t\t\t// compute.geometry children idle — always pass it explicitly.\n\t\t\tmaxConcurrent: probe.maxConcurrent,\n\t\t\t// The scheduler treats undefined as unbounded/no-deadline, so map our\n\t\t\t// `0`-means-off convention to undefined rather than passing 0 (which\n\t\t\t// would reject EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: limits.solveDeadlineMs,\n\t\t\t// The byte budget is the only eviction pressure. A solve is a pure\n\t\t\t// function of (definition, inputs), both immutable, so a retained\n\t\t\t// result can never go stale — expiring one only forces a paid re-solve\n\t\t\t// of the identical answer.\n\t\t\tcache: config.responseCacheMaxBytes > 0 ? { maxBytes: config.responseCacheMaxBytes } : false,\n\t\t\treuseServerDefinitionCache: limits.reuseDefinitionCache,\n\t\t\tonSettle: (_ctx, result) => {\n\t\t\t\t// Deferred: this hook fires before the scheduler drops the solve from its in-flight set.\n\t\t\t\tqueueMicrotask(() => finishDraining(entry));\n\t\t\t\tif (result.status !== 'success') return;\n\t\t\t\t// A cache hit means we never reached the server, so it says nothing about the pool.\n\t\t\t\t// `scheduler` is safe despite being referenced from inside its own initializer:\n\t\t\t\t// onSettle only ever fires during a solve, long after binding.\n\t\t\t\tif (result.fromCache !== true) {\n\t\t\t\t\tprobe.refreshIfDue(scheduler);\n\t\t\t\t\trecheckLimitsIfDue();\n\t\t\t\t}\n\t\t\t\tif (config.debug) describeSettledSolve(result).forEach(debugLog);\n\t\t\t}\n\t\t});\n\t\tconst entry: CachedClient = { client, scheduler, limits };\n\t\treturn entry;\n\t}\n\n\tfunction evictEntry(id: string) {\n\t\tconst entry = cache.get(id);\n\t\tif (!entry) return;\n\t\tcache.delete(id);\n\t\tretire(entry);\n\t}\n\n\treturn {\n\t\tasync getClient(server, opts): Promise<CachedClient> {\n\t\t\tconst key = server.id;\n\t\t\tconst existing = cache.get(key);\n\t\t\tif (existing) {\n\t\t\t\t// Refresh LRU position.\n\t\t\t\tcache.delete(key);\n\t\t\t\tcache.set(key, existing);\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst inFlight = pending.get(key);\n\t\t\tif (inFlight) return inFlight.promise;\n\n\t\t\t// Replay a recent failure instead of re-running the probe ladder. The\n\t\t\t// original error is rethrown verbatim so the caller still sees the real\n\t\t\t// cause (refused / unauthorized / timeout), not a generic cache miss.\n\t\t\tconst recentFailure = failed.get(key);\n\t\t\tif (recentFailure) {\n\t\t\t\tif (Date.now() - recentFailure.at < FAILED_BUILD_TTL_MS) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t`[Compute/client-cache] server ${key} failed to build <${FAILED_BUILD_TTL_MS}ms ago — replaying error`\n\t\t\t\t\t);\n\t\t\t\t\tthrow recentFailure.error;\n\t\t\t\t}\n\t\t\t\tfailed.delete(key);\n\t\t\t}\n\n\t\t\t// A build is a Rhino.Compute handshake — expensive, and repeated builds for\n\t\t\t// the same id mean churn (LRU thrash or config-rotation evictions).\n\t\t\tdebugLog(`[Compute/client-cache] miss — building warm client for server ${key}`);\n\t\t\tconst token = { stale: false };\n\t\t\tconst promise = (async () => {\n\t\t\t\tlet entry: CachedClient;\n\t\t\t\ttry {\n\t\t\t\t\tentry = await build(server, opts?.definitionGuid);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (!token.stale) failed.set(key, { error: err, at: Date.now() });\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\t// Installing it would carry the old URL or key past the eviction, with nothing left to\n\t\t\t\t// evict it again. Its caller holds a client built from those same details, so it fails too.\n\t\t\t\tif (token.stale) {\n\t\t\t\t\tentry.scheduler.dispose();\n\t\t\t\t\tthrow new ComputeError(\n\t\t\t\t\t\t`Compute server ${key} changed while connecting.`,\n\t\t\t\t\t\tErrorCodes.ABORTED\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (cache.size >= maxWarmComputeServers) {\n\t\t\t\t\tconst oldestKey = cache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/client-cache] LRU evicted warm client for server ${oldestKey} (cap ${maxWarmComputeServers})`\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst oldest = cache.get(oldestKey);\n\t\t\t\t\t\tcache.delete(oldestKey);\n\t\t\t\t\t\tif (oldest) retire(oldest);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcache.set(key, entry);\n\t\t\t\treturn entry;\n\t\t\t})().finally(() => {\n\t\t\t\tif (pending.get(key)?.token === token) pending.delete(key);\n\t\t\t});\n\t\t\tpending.set(key, { promise, token });\n\t\t\treturn promise;\n\t\t},\n\n\t\tevict(id): void {\n\t\t\t// Drop any remembered failure too: `evict` means the connection details\n\t\t\t// changed, so the reason the last build failed no longer applies.\n\t\t\tfailed.delete(id);\n\t\t\tconst build = pending.get(id);\n\t\t\tif (build) {\n\t\t\t\tbuild.token.stale = true;\n\t\t\t\tpending.delete(id);\n\t\t\t}\n\t\t\tif (cache.has(id)) {\n\t\t\t\tdebugLog(`[Compute/client-cache] evicted warm client for server ${id} (config change)`);\n\t\t\t\tevictEntry(id);\n\t\t\t}\n\t\t},\n\n\t\tsolveCacheStats(): SolveCacheStats {\n\t\t\tconst total: SolveCacheStats = {\n\t\t\t\twarmClients: cache.size,\n\t\t\t\tentries: 0,\n\t\t\t\tbytes: 0,\n\t\t\t\thits: 0,\n\t\t\t\tmisses: 0,\n\t\t\t\tevictions: 0\n\t\t\t};\n\t\t\tfor (const entry of cache.values()) {\n\t\t\t\tconst s = entry.scheduler.cacheStats();\n\t\t\t\ttotal.entries += s.entries;\n\t\t\t\ttotal.bytes += s.bytes;\n\t\t\t\ttotal.hits += s.hits;\n\t\t\t\ttotal.misses += s.misses;\n\t\t\t\ttotal.evictions += s.evictions;\n\t\t\t}\n\t\t\treturn total;\n\t\t},\n\n\t\tclearSolveCaches(): void {\n\t\t\tdebugLog(`[Compute/client-cache] cleared solve caches on ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.clearCache();\n\t\t},\n\n\t\tdisposeAll(): void {\n\t\t\tdebugLog(`[Compute/client-cache] disposing all ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of [...cache.values(), ...draining]) entry.scheduler.dispose();\n\t\t\tfor (const build of pending.values()) build.token.stale = true;\n\t\t\tpending.clear();\n\t\t\tcache.clear();\n\t\t\tdraining.clear();\n\t\t\tfailed.clear();\n\t\t}\n\t};\n}\n","/**\n * In-process cache of `.gh` definition bytes, keyed by immutable **version id**.\n *\n * Turns an eager `storage.get(fileKey)` on every solve request into a lazy\n * `DefinitionRef.load()`: the scheduler calls `load()` only when an upload is\n * unavoidable (a pointer-known re-solve never touches storage or this cache).\n *\n * The key MUST be the version id, never a `fileKey` — a delete-latest-then-\n * reupload can reuse a `fileKey` for different content, which would silently\n * serve one version's bytes for another with no diagnostic. Callers pass\n * `version.id`, and the returned `DefinitionRef.key` is that same id, so the\n * scheduler's own result/pointer cache keys on the same immutable identity.\n *\n * Eviction is LRU by total byte budget, not entry count — definitions range\n * from tens of KB to hundreds of MB, so a count cap would either pin\n * gigabytes or evict uselessly. `COMPUTE_DEFINITION_CACHE_MB` sets the\n * budget (see `createDefinitionByteCache`). No TTL: version ids are\n * immutable, so a cached entry can never go stale. An entry larger than the\n * whole budget is served but never retained.\n */\n\nimport { raceAbort } from '../shared/abort.js';\n\n/**\n * Per-`getOrLoad` outcome, observable after the scheduler has (or hasn't)\n * called `load()` — lets the solve pipeline emit a `def_bytes` Server-Timing\n * verdict (`skipped` / `hit` / `miss`).\n */\nexport interface ByteRefOutcome {\n\tloaded: boolean;\n\tfromCache: boolean;\n}\n\n/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */\nexport interface ByteCacheRef {\n\tkey: string;\n\tload: (signal?: AbortSignal) => Promise<Uint8Array>;\n\toutcome: ByteRefOutcome;\n}\n\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\t/** `load` gets a signal that fires once every caller waiting on this read has gone. */\n\tgetOrLoad(versionId: string, load: (signal: AbortSignal) => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\tclear(): void;\n}\n\n/** A `ByteCacheRef` carries `outcome`, which a plain `DefinitionRef` never has. */\nexport function isByteCacheRef(source: unknown): source is ByteCacheRef {\n\treturn typeof source === 'object' && source !== null && 'outcome' in source;\n}\n\n/** One storage read, shared by every caller that misses while it runs. */\ninterface Loading {\n\tpromise: Promise<Uint8Array>;\n\tcontroller: AbortController;\n\twaiters: number;\n}\n\n/**\n * @param maxBytes total retained-byte budget; `0` (or negative) disables caching\n * entirely — every `load()` calls the loader and nothing is retained.\n */\nexport function createDefinitionByteCache(maxBytes: number): DefinitionByteCache {\n\tconst budget = Math.max(0, Math.floor(maxBytes));\n\t// Map iteration order is insertion order, so it doubles as LRU order:\n\t// re-inserting a key on access moves it to the most-recently-used end.\n\tconst cache = new Map<string, Uint8Array>();\n\tconst loading = new Map<string, Loading>();\n\tlet retainedBytes = 0;\n\tconst stats = { hits: 0, misses: 0, evictions: 0 };\n\n\tfunction evictToFit(): void {\n\t\tfor (const [key, bytes] of cache) {\n\t\t\tif (retainedBytes <= budget) break;\n\t\t\tcache.delete(key);\n\t\t\tretainedBytes -= bytes.byteLength;\n\t\t\tstats.evictions += 1;\n\t\t}\n\t}\n\n\tfunction store(key: string, bytes: Uint8Array): void {\n\t\tif (budget === 0) return;\n\t\t// Caching an entry bigger than the budget would evict everything\n\t\t// (including itself) on the next evictToFit — serve it, cache nothing.\n\t\tif (bytes.byteLength > budget) return;\n\t\tcache.set(key, bytes);\n\t\tretainedBytes += bytes.byteLength;\n\t\tevictToFit();\n\t}\n\n\tfunction startLoad(key: string, load: (signal: AbortSignal) => Promise<Uint8Array>): Loading {\n\t\tconst controller = new AbortController();\n\t\tconst entry: Loading = {\n\t\t\tcontroller,\n\t\t\twaiters: 0,\n\t\t\tpromise: (async () => {\n\t\t\t\tconst bytes = await load(controller.signal);\n\t\t\t\tstore(key, bytes);\n\t\t\t\treturn bytes;\n\t\t\t})().finally(() => {\n\t\t\t\tif (loading.get(key) === entry) loading.delete(key);\n\t\t\t})\n\t\t};\n\t\tloading.set(key, entry);\n\t\treturn entry;\n\t}\n\n\t/**\n\t * A caller gone stops waiting, but the read runs on for whoever else is on it. A caller with no\n\t * signal can't leave, so it stays counted and the read is never aborted under it.\n\t */\n\tasync function wait(entry: Loading, signal?: AbortSignal): Promise<Uint8Array> {\n\t\tentry.waiters += 1;\n\t\tif (!signal) return entry.promise;\n\t\ttry {\n\t\t\treturn await raceAbort(entry.promise, signal);\n\t\t} finally {\n\t\t\tentry.waiters -= 1;\n\t\t\tif (entry.waiters === 0 && signal.aborted) entry.controller.abort();\n\t\t}\n\t}\n\n\treturn {\n\t\tgetOrLoad(versionId, load) {\n\t\t\tconst outcome: ByteRefOutcome = { loaded: false, fromCache: false };\n\t\t\treturn {\n\t\t\t\tkey: versionId,\n\t\t\t\toutcome,\n\t\t\t\tload: async function read(signal): Promise<Uint8Array> {\n\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\tconst hit = cache.get(versionId);\n\t\t\t\t\tif (hit) {\n\t\t\t\t\t\tcache.delete(versionId);\n\t\t\t\t\t\tcache.set(versionId, hit);\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn hit;\n\t\t\t\t\t}\n\t\t\t\t\tconst running = loading.get(versionId);\n\t\t\t\t\tif (running?.controller.signal.aborted) {\n\t\t\t\t\t\t// Abandoned, and a store that ignores its signal may never finish. Waiting it out\n\t\t\t\t\t\t// rather than starting a second read keeps a hung store to one read per version.\n\t\t\t\t\t\tconst woundDown = running.promise.then(\n\t\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t\t\t() => undefined\n\t\t\t\t\t\t);\n\t\t\t\t\t\tawait (signal ? raceAbort(woundDown, signal) : woundDown);\n\t\t\t\t\t\treturn read(signal);\n\t\t\t\t\t}\n\t\t\t\t\tif (running) {\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn wait(running, signal);\n\t\t\t\t\t}\n\t\t\t\t\tstats.misses += 1;\n\t\t\t\t\treturn wait(startLoad(versionId, load), signal);\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tstats() {\n\t\t\treturn {\n\t\t\t\thits: stats.hits,\n\t\t\t\tmisses: stats.misses,\n\t\t\t\tevictions: stats.evictions,\n\t\t\t\tentries: cache.size,\n\t\t\t\tbytes: retainedBytes\n\t\t\t};\n\t\t},\n\t\tclear() {\n\t\t\tcache.clear();\n\t\t\tretainedBytes = 0;\n\t\t}\n\t};\n}\n","/**\n * Turns one solve's response into each caller's wire form. Runs after single-flight, not inside\n * it: coalesced callers can want different encodings, and each reports its own timing.\n */\n\nimport { gzip } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport type { ServerTiming } from '@selvajs/compute/core';\nimport type { GrasshopperComputeResponse } from '@selvajs/compute/grasshopper';\nimport type { CacheVerdict, DefBytesVerdict } from './server-timing.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n/**\n * Display output is base64 of already-deflated bytes, where higher levels find nothing more:\n * level 1 lands within 1% of the default's size, in up to a fifth of the time.\n */\nconst GZIP_LEVEL = 1;\n\n/** Below this gzip isn't worth the CPU for the wire savings. */\nconst GZIP_MIN_BYTES = 1024;\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Envelope\n// ============================================================================\n\n/** What the `Server-Timing` header carries for one caller, as data: read this, don't parse that. */\nexport interface SolvePhaseMetrics {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n\t/** The compute server's own timing; null when it reports none. */\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** Absent for raw-bytes solves. */\n\tdefBytes?: DefBytesVerdict;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\nexport interface EncodedBody {\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\tserializedBytes: number;\n\tserializeMs: number;\n\tgzipMs: number;\n}\n\nexport type TooLarge = 'too_large';\n\nexport function buildEnvelope(\n\tencoded: EncodedBody,\n\tresult: GrasshopperComputeResponse,\n\tserverTiming: string,\n\tmetrics: SolvePhaseMetrics\n): SolveEnvelope {\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\tconst { body, encoding } = encoded;\n\tif (encoding === 'gzip') {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t}\n\theaders['Content-Length'] = String(\n\t\ttypeof body === 'string' ? Buffer.byteLength(body) : body.byteLength\n\t);\n\treturn { body, ...(encoding ? { encoding } : {}), headers, result, metrics };\n}\n\n// ============================================================================\n// Encoding\n// ============================================================================\n\n/** Whether an `Accept-Encoding` header takes gzip. `gzip;q=0` is a client refusing it. */\nexport function acceptsGzip(acceptEncoding: string): boolean {\n\treturn acceptEncoding.split(',').some((part) => {\n\t\tconst [coding, ...params] = part.split(';').map((s) => s.trim().toLowerCase());\n\t\tif (coding !== 'gzip') return false;\n\t\tconst q = params.find((p) => p.startsWith('q='));\n\t\treturn q === undefined || Number(q.slice(2)) > 0;\n\t});\n}\n\ninterface GzippedBody {\n\tbody: Uint8Array;\n\tserializedBytes: number;\n}\n\n/**\n * A scheduler cache hit returns the same response object, so a repeat solve reuses its bytes rather\n * than paying to serialize and compress them again. Entries live exactly as long as the response.\n * Only the gzip form is kept: browsers always take it, and the JSON string would double the memory.\n * Holds the promise, so callers arriving during the compression share it.\n */\nconst gzippedBodies = new WeakMap<object, Promise<GzippedBody | TooLarge>>();\n\nfunction gzipOnce(response: object, json: string, serializedBytes: number) {\n\tconst pending = gzipAsync(Buffer.from(json), { level: GZIP_LEVEL }).then((body) => ({\n\t\tbody,\n\t\tserializedBytes\n\t}));\n\tgzippedBodies.set(response, pending);\n\t// A failed compression must not stay memoized while the response is cached.\n\tpending.catch(() => gzippedBodies.delete(response));\n\treturn pending;\n}\n\nexport interface ResponseEncoder {\n\t/** Buffered, not streamed, so Content-Length is known upfront and a connection cut mid-transfer fails hard instead of silently truncating the JSON. */\n\tencode(acceptEncoding: string): Promise<EncodedBody | TooLarge>;\n}\n\n/**\n * One per solve. The JSON is kept here rather than beside the gzip body: it only has to outlive the\n * callers of this flight, who would otherwise each stringify the same response and hold a copy.\n */\nexport function createResponseEncoder(response: object, maxBytes: number): ResponseEncoder {\n\tlet serialized: { json: string; bytes: number } | TooLarge | undefined;\n\n\tfunction serialize() {\n\t\tif (serialized !== undefined) return serialized;\n\t\ttry {\n\t\t\tconst json = JSON.stringify(response);\n\t\t\tconst bytes = Buffer.byteLength(json);\n\t\t\tserialized = bytes > maxBytes ? 'too_large' : { json, bytes };\n\t\t} catch (err) {\n\t\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t\t// here. Treat it the same as the byte cap.\n\t\t\tif (!(err instanceof RangeError)) throw err;\n\t\t\tserialized = 'too_large';\n\t\t}\n\t\treturn serialized;\n\t}\n\n\treturn {\n\t\tasync encode(acceptEncoding) {\n\t\t\tconst wantsGzip = acceptsGzip(acceptEncoding);\n\t\t\tlet gzipped = wantsGzip ? gzippedBodies.get(response) : undefined;\n\t\t\tlet serializeMs = 0;\n\n\t\t\tif (!gzipped) {\n\t\t\t\tconst serializeStart = performance.now();\n\t\t\t\tconst plain = serialize();\n\t\t\t\tserializeMs = performance.now() - serializeStart;\n\t\t\t\tif (plain === 'too_large') {\n\t\t\t\t\tif (wantsGzip) gzippedBodies.set(response, Promise.resolve('too_large'));\n\t\t\t\t\treturn 'too_large';\n\t\t\t\t}\n\t\t\t\tif (!wantsGzip || plain.bytes <= GZIP_MIN_BYTES) {\n\t\t\t\t\treturn { body: plain.json, serializedBytes: plain.bytes, serializeMs, gzipMs: 0 };\n\t\t\t\t}\n\t\t\t\tgzipped = gzipOnce(response, plain.json, plain.bytes);\n\t\t\t}\n\n\t\t\tconst gzipStart = performance.now();\n\t\t\tconst verdict = await gzipped;\n\t\t\tif (verdict === 'too_large') return verdict;\n\t\t\treturn {\n\t\t\t\tbody: verdict.body,\n\t\t\t\tencoding: 'gzip',\n\t\t\t\tserializedBytes: verdict.serializedBytes,\n\t\t\t\tserializeMs,\n\t\t\t\tgzipMs: performance.now() - gzipStart\n\t\t\t};\n\t\t}\n\t};\n}\n","import type { ServerTiming } from '@selvajs/compute/core';\n\n/** A pre-solve prep sub-phase: `[label, ms]`, surfaced as a `p_*` Server-Timing entry. */\nexport type PrepMark = [string, number];\n\nexport interface CacheVerdict {\n\tfromCache: boolean;\n\tdefinitionReuploaded?: boolean;\n}\n\n/**\n * skipped = scheduler never materialized bytes (pointer-known solve, the whole point of this\n * path); hit = warm byte cache; miss = fell through to storage.\n */\nexport type DefBytesVerdict = 'skipped' | 'hit' | 'miss';\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** Absent for raw-bytes solves. */\n\tdefBytes?: DefBytesVerdict;\n\tprepMarks: PrepMark[];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own timing\n * (VektorNode fork), `rhino_*` is time on the compute server and `compute_link`\n * is everything between it (network + queue wait). `rhino_cache` is 1 when the\n * compute server answered from its own result cache.\n */\nexport function buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst decode = rhino.decode ?? 0;\n\t\tconst solve = rhino.solve ?? 0;\n\t\tconst encode = rhino.encode ?? 0;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - decode - solve - encode);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}` +\n\t\t\t`, rhino_cache;dur=${rhino.cacheHit ? 1 : 0}`;\n\t\tif (rhino.gh !== undefined) header += `, rhino_gh;dur=${rhino.gh.toFixed(1)}`;\n\t\tif (rhino.recomputed) {\n\t\t\theader += `, rhino_recomputed;desc=\"${rhino.recomputed.ran}/${rhino.recomputed.total}\"`;\n\t\t}\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\tif (settle.definitionReuploaded !== undefined) {\n\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t}\n\n\tif (parts.defBytes) header += `, def_bytes;desc=${parts.defBytes}`;\n\n\treturn header;\n}\n","/**\n * In-process single-flight — dogpile protection for every solve.\n *\n * Always on, not gated behind a result cache being configured: the dogpile is worst\n * when nothing else is caching, since N concurrent identical solves each pay a full\n * Rhino round trip (the scheduler doesn't coalesce in-flight requests itself, and\n * Rhino's `cachesolve` still costs a round trip per repeat).\n *\n * Per app instance. No cross-instance lease (Redis `SET NX`) yet.\n *\n * The shared promise resolves to one value for every caller, so `work` must return\n * something safe to share by reference.\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Coalesces concurrent calls under the same key: the first caller runs `work`, later\n\t * callers await the same promise, and the key frees as soon as it settles.\n\t *\n\t * The flight belongs to all its callers. It aborts `work`'s signal when the last one's\n\t * `callerSignal` fires, and frees the key right then, so a caller arriving after that starts\n\t * a fresh flight instead of joining one that is dying. A caller that leaves earlier still\n\t * resolves with the flight: check its own signal before using the result.\n\t */\n\trun<T>(\n\t\tkey: string,\n\t\twork: (signal: AbortSignal) => Promise<T>,\n\t\tcallerSignal: AbortSignal\n\t): Promise<T>;\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\tonJoin?: (key: string) => void;\n}\n\ninterface Flight {\n\tpromise: Promise<unknown>;\n\tcontroller: AbortController;\n\tcallers: number;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\tconst inflight = new Map<string, Flight>();\n\n\tfunction free(key: string, flight: Flight) {\n\t\t// Guard against deleting a key a later caller has already re-taken.\n\t\tif (inflight.get(key) === flight) inflight.delete(key);\n\t}\n\n\tfunction attend<T>(key: string, flight: Flight, signal: AbortSignal): Promise<T> {\n\t\tflight.callers += 1;\n\t\t// `once: true` doesn't detach on a normal completion, and the caller's signal outlives\n\t\t// this call, so without the `finally` every solve leaks a listener onto it.\n\t\tconst leave = () => {\n\t\t\tflight.callers -= 1;\n\t\t\tif (flight.callers > 0) return;\n\t\t\tfree(key, flight);\n\t\t\tflight.controller.abort();\n\t\t};\n\t\tsignal.addEventListener('abort', leave, { once: true });\n\t\treturn (flight.promise as Promise<T>).finally(() => signal.removeEventListener('abort', leave));\n\t}\n\n\treturn {\n\t\trun<T>(key: string, work: (signal: AbortSignal) => Promise<T>, callerSignal: AbortSignal) {\n\t\t\tconst existing = inflight.get(key);\n\t\t\tif (existing) {\n\t\t\t\ttry {\n\t\t\t\t\toptions.onJoin?.(key);\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Telemetry must not fail a solve that is otherwise fine.\n\t\t\t\t\tconsole.error('[Solve/single-flight] onJoin threw:', err);\n\t\t\t\t}\n\t\t\t\treturn attend<T>(key, existing, callerSignal);\n\t\t\t}\n\n\t\t\tconst controller = new AbortController();\n\t\t\tconst flight: Flight = {\n\t\t\t\tcontroller,\n\t\t\t\tcallers: 0,\n\t\t\t\t// Wrapped so a synchronous throw in `work` still rejects instead of escaping `run`.\n\t\t\t\tpromise: (async () => work(controller.signal))().finally(() => free(key, flight))\n\t\t\t};\n\t\t\tinflight.set(key, flight);\n\t\t\treturn attend<T>(key, flight, callerSignal);\n\t\t},\n\t\tinFlight(): number {\n\t\t\treturn inflight.size;\n\t\t}\n\t};\n}\n","import { TreeBuilder, type DataTree } from '@selvajs/compute/grasshopper';\nimport { getInputItems, type UISchema } from '@selvajs/schemas';\nimport type { SolveInput } from '../shared/solve-input.js';\n\n/**\n * What `SolveEngine.solve` takes as `inputs`, from a schema the host stored. The bounds live on\n * the number widgets in the layout, not on `schema.inputs`, so without them nothing clamps.\n */\nexport function solveInputsFromSchema(schema: UISchema): SolveInput[] {\n\tconst bounds = new Map<string, { minimum?: number; maximum?: number }>();\n\tfor (const item of getInputItems(schema)) {\n\t\tif (item.widgetType === 'number' && item.config) bounds.set(item.paramId, item.config);\n\t}\n\treturn schema.inputs.map((input) => {\n\t\tconst widget = bounds.get(input.id);\n\t\treturn widget ? { ...input, minimum: widget.minimum, maximum: widget.maximum } : input;\n\t});\n}\n\n/** The input tree a solve sends: each input's value, or its schema default when it has none. */\nexport function buildSolveInputTree(\n\tinputs: SolveInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromValues(\n\t\tinputs\n\t\t\t// Stored schemas are validated at upload only, so an older one can lack `paramType`.\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input) => ({\n\t\t\t\tparamName: input.nickname,\n\t\t\t\tparamType: input.paramType,\n\t\t\t\t// `hasOwn`: an input id of `constructor` would read the inherited function.\n\t\t\t\tvalue: (Object.hasOwn(values, input.id) ? values[input.id] : undefined) ?? input.default,\n\t\t\t\tminimum: input.minimum,\n\t\t\t\tmaximum: input.maximum\n\t\t\t}))\n\t);\n}\n","/**\n * Runs one solve against a warm scheduler and classifies how it ended: a solved response, or an\n * expected failure as a value instead of a throw. This is the part single-flight shares, so\n * nothing here depends on who is asking.\n */\n\nimport {\n\tErrorCodes,\n\tComputeError,\n\ttype ServerTiming,\n\ttype SolveDefinition\n} from '@selvajs/compute/core';\nimport type {\n\tDataTree,\n\tGrasshopperComputeResponse,\n\tSelvaEventTarget\n} from '@selvajs/compute/grasshopper';\nimport type { CachedClient } from './client-cache.js';\nimport { isByteCacheRef } from './definition-byte-cache.js';\nimport {\n\tcreateResponseEncoder,\n\ttype ResponseEncoder,\n\ttype SolveEnvelope\n} from './response-encoding.js';\nimport type { CacheVerdict, DefBytesVerdict } from './server-timing.js';\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\tinputTree: DataTree[];\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Per-solve live-event callback the compute server hands to the definition; absent when the caller opened no stream. */\n\tselvaEvents?: SelvaEventTarget;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also tells that apart from an abort nobody asked for.\n\t */\n\tsignal: AbortSignal;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure:\n * `SolveEngine.toWebResponse` maps each to a status. `durationMs` on error variants is the solve\n * wall time up to the failure, for the metric record.\n */\nexport type SolveEngineOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| SolveFailure;\n\nexport type SolveFailure =\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** The compute server refused the request body (its `RHINO_COMPUTE_MAX_REQUEST_SIZE`). */\n\t| { kind: 'request_too_large'; durationMs: number; message: string }\n\t/**\n\t * Retryable: no result, through no fault of the request, and `retryAfterSeconds` is a backoff\n\t * hint. Scheduler backpressure rejected the solve before it ran (`queue_full`,\n\t * `queue_timeout`), or its client was shut down under it (`interrupted`).\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout' | 'interrupted';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n/** One finished solve, shared by every caller coalesced onto it. */\nexport interface SolvedFlight {\n\tkind: 'solved';\n\tresponse: GrasshopperComputeResponse;\n\tencoder: ResponseEncoder;\n\tsolveMs: number;\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** The verdict of the ref that ran: a coalesced caller's own ref never loads anything. */\n\tdefBytes?: DefBytesVerdict;\n}\n\nfunction shed(\n\tdurationMs: number,\n\treason: 'queue_full' | 'queue_timeout' | 'interrupted',\n\tmessage: string\n): SolveFailure {\n\treturn { kind: 'shed', durationMs, reason, retryAfterSeconds: 1, message };\n}\n\n/** The solve lost its client, and no caller asked for that: a config write or a shutdown. */\nexport function interrupted(durationMs: number): SolveFailure {\n\treturn shed(durationMs, 'interrupted', 'The solve was interrupted. Retry shortly.');\n}\n\nfunction classifyFailure(err: unknown, durationMs: number, args: SolvePipelineArgs): SolveFailure {\n\tif (!(err instanceof ComputeError)) return { kind: 'compute_error', durationMs, error: err };\n\tif (err.statusCode === 413) {\n\t\tconst cap = args.client.limits.maxRequestBytes;\n\t\treturn {\n\t\t\tkind: 'request_too_large',\n\t\t\tdurationMs,\n\t\t\tmessage:\n\t\t\t\t`The inputs are larger than the compute server accepts` +\n\t\t\t\t(cap ? ` (${Math.round(cap / (1024 * 1024))} MB)` : '') +\n\t\t\t\t'. Its admin can raise RHINO_COMPUTE_MAX_REQUEST_SIZE.'\n\t\t};\n\t}\n\tswitch (err.code) {\n\t\tcase ErrorCodes.TIMEOUT_ERROR:\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.client.limits.solveDeadlineMs / 1000)}s deadline set by the compute server.`\n\t\t\t};\n\t\tcase ErrorCodes.ABORTED:\n\t\t\treturn args.signal.aborted ? { kind: 'client_abort', durationMs } : interrupted(durationMs);\n\t\tcase ErrorCodes.RESPONSE_TOO_LARGE:\n\t\t\treturn { kind: 'too_large' };\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tcase ErrorCodes.QUEUE_FULL:\n\t\t\treturn shed(durationMs, 'queue_full', 'Compute server is at capacity. Retry shortly.');\n\t\tcase ErrorCodes.QUEUE_TIMEOUT:\n\t\t\treturn shed(\n\t\t\t\tdurationMs,\n\t\t\t\t'queue_timeout',\n\t\t\t\t'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t);\n\t\tdefault:\n\t\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n}\n\nfunction defBytesVerdict(source: SolveDefinition): DefBytesVerdict | undefined {\n\tif (!isByteCacheRef(source)) return undefined;\n\tconst { loaded, fromCache } = source.outcome;\n\treturn !loaded ? 'skipped' : fromCache ? 'hit' : 'miss';\n}\n\nexport async function runSolvePipeline(\n\targs: SolvePipelineArgs\n): Promise<SolvedFlight | SolveFailure> {\n\tlet rhino: ServerTiming | null = null;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tconst detail = await args.client.scheduler.solveDetailed(\n\t\t\targs.definitionSource,\n\t\t\targs.inputTree,\n\t\t\t{\n\t\t\t\tsignal: args.signal,\n\t\t\t\tselvaevents: args.selvaEvents,\n\t\t\t\tonServerTiming: (t) => {\n\t\t\t\t\trhino = t;\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tconst defBytes = defBytesVerdict(args.definitionSource);\n\t\treturn {\n\t\t\tkind: 'solved',\n\t\t\tresponse: detail.response,\n\t\t\tencoder: createResponseEncoder(detail.response, args.responseMaxBytes),\n\t\t\tsolveMs: performance.now() - solveStart,\n\t\t\trhino,\n\t\t\tsettle: { fromCache: detail.fromCache, definitionReuploaded: detail.definitionReuploaded },\n\t\t\t...(defBytes ? { defBytes } : {})\n\t\t};\n\t} catch (err) {\n\t\treturn classifyFailure(err, performance.now() - solveStart, args);\n\t}\n}\n","/**\n * Facade over the warm-client cache, definition-byte cache, single-flight\n * coalescing, and the solve pipeline — the primitives an app route would\n * otherwise wire up by hand.\n *\n * Doesn't read env itself: `@selvajs/server` owns `resolveComputeLimits`, and\n * this package stays independent of it. Callers resolve their own limits and\n * pass in the fields below.\n *\n * Stays out of auth, DB reads, share tokens, rate limiting, metrics, and\n * compute-server selection — all app policy, supplied per-call via\n * `SolveEngineSolveArgs.server`.\n */\n\nimport {\n\tComputeError,\n\tErrorCodes,\n\tisDefinitionRef,\n\ttype DefinitionRef,\n\ttype SolveDefinition\n} from '@selvajs/compute/core';\nimport { stableStringify, type SelvaEventTarget } from '@selvajs/compute/grasshopper';\nimport {\n\tcreateClientCache,\n\ttype CachedClient,\n\ttype ClientCache,\n\ttype ClientCacheDebug,\n\ttype ResolvedServer,\n\ttype SolveCacheStats\n} from './client-cache.js';\nimport {\n\tcreateDefinitionByteCache,\n\tisByteCacheRef,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype DefinitionByteCache\n} from './definition-byte-cache.js';\nimport { buildEnvelope } from './response-encoding.js';\nimport { buildServerTiming, type PrepMark } from './server-timing.js';\nimport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight\n} from './solve-cache-single-flight.js';\nimport { buildSolveInputTree } from './transform-input.js';\nimport { interrupted, runSolvePipeline, type SolveEngineOutcome } from './solve-pipeline.js';\nimport type { SolveInput } from '../shared/solve-input.js';\n\n/**\n * Subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs.\n * Pass the resolved object through as-is — extra fields are ignored.\n */\nexport interface SolveEngineLimits {\n\tcomputeResponseMaxBytes: number;\n\tcomputeServerCachesolve: boolean;\n\tcomputeMaxQueueDepth: number;\n\tcomputeQueueWaitMs: number;\n\tcomputeDefinitionCacheBytes: number;\n\tcomputeSolveCacheBytes: number;\n}\n\n/** A deadline of 0 means no deadline, and a `NaN` budget retains without limit, both silently. */\nfunction assertValidLimits(limits: SolveEngineLimits): void {\n\tconst check = (name: keyof SolveEngineLimits, min: number) => {\n\t\tconst value = limits[name] as number;\n\t\tif (!Number.isFinite(value) || value < min) {\n\t\t\tthrow new RangeError(`SolveEngine: limits.${name} must be a number >= ${min}, got ${value}.`);\n\t\t}\n\t};\n\tcheck('computeResponseMaxBytes', 1);\n\tcheck('computeMaxQueueDepth', 0);\n\tcheck('computeQueueWaitMs', 0);\n\tcheck('computeDefinitionCacheBytes', 0);\n\tcheck('computeSolveCacheBytes', 0);\n}\n\nexport interface SolveEngineOptions {\n\tlimits: SolveEngineLimits;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. */\n\tmaxWarmComputeServers?: number;\n\t/** Concise cache/timing logs when truthy; `'verbose'` also dumps full lib-level requests/responses. Default off. */\n\tdebug?: ClientCacheDebug;\n\t/** Sink for the debug lines. Without it they go nowhere. */\n\tonDebugLog?: (message: string) => void;\n\t/** Fired when a caller joins an already-in-flight solve instead of running its own. */\n\tonSolveCoalesced?: (key: string) => void;\n}\n\n/**\n * A `ByteCacheRef` obtained ahead of time from `engine.definitionRef()` (e.g. to\n * read bytes for schema extraction before solving) is passed through as-is.\n */\nexport type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef;\n\nexport interface SolveEngineSolveArgs {\n\tserver: ResolvedServer;\n\tdefinitionSource: SolveEngineDefinitionSource;\n\t/**\n\t * Coalesce-key identity for a raw `Uint8Array`/`string` source, which has\n\t * no natural identity the way a `DefinitionRef`'s `.key` does. Required in\n\t * that case — `solve()` throws rather than silently hashing the bytes.\n\t * Ignored for the other source forms.\n\t */\n\tdefinitionKey?: string;\n\t/**\n\t * The definition's inputs, from the schema you stored, never from the request: `id`, `nickname`,\n\t * `paramType` and the bounds decide what reaches the definition, so a body that supplies them\n\t * can drop a limit or fill a slot your host reserved.\n\t */\n\tinputs: SolveInput[];\n\tvalues: Record<string, unknown>;\n\tsignal: AbortSignal;\n\tacceptEncoding?: string;\n\t/**\n\t * Stamped as `X-Selva-Definition` when this solve builds the server's warm client. The header\n\t * is fixed per client, so later solves on that server carry the first definition's guid.\n\t */\n\tdefinitionGuid?: string;\n\tloadStartMs?: number;\n\tdefLoadMs?: number;\n\tprepMarks?: PrepMark[];\n\t/** Per-solve live-event callback. A solve carrying one runs its own compute call, never a shared one. */\n\tselvaEvents?: SelvaEventTarget;\n}\n\nexport interface SolveEngineStats {\n\tclient: SolveCacheStats;\n\tdefinitionBytes: ByteCacheStats;\n\tcoalescing: { inFlight: number };\n}\n\nexport class SolveEngine {\n\tprivate readonly limits: SolveEngineLimits;\n\tprivate readonly clientCache: ClientCache;\n\tprivate readonly byteCache: DefinitionByteCache;\n\tprivate readonly singleFlight: SolveCacheSingleFlight;\n\tprivate readonly clientGenerations = new WeakMap<CachedClient, number>();\n\tprivate lastClientGeneration = 0;\n\n\tconstructor(options: SolveEngineOptions) {\n\t\tassertValidLimits(options.limits);\n\t\tthis.limits = options.limits;\n\n\t\tthis.clientCache = createClientCache({\n\t\t\tcachesolve: this.limits.computeServerCachesolve,\n\t\t\tmaxQueueDepth: this.limits.computeMaxQueueDepth,\n\t\t\tqueueWaitMs: this.limits.computeQueueWaitMs,\n\t\t\tresponseCacheMaxBytes: this.limits.computeSolveCacheBytes,\n\t\t\tmaxResponseBytes: this.limits.computeResponseMaxBytes,\n\t\t\tdebug: options.debug ?? false,\n\t\t\tmaxWarmComputeServers: options.maxWarmComputeServers,\n\t\t\tonDebugLog: options.onDebugLog\n\t\t});\n\n\t\tthis.byteCache = createDefinitionByteCache(this.limits.computeDefinitionCacheBytes);\n\n\t\tthis.singleFlight = createSolveCacheSingleFlight({\n\t\t\tonJoin: options.onSolveCoalesced\n\t\t});\n\t}\n\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient> {\n\t\treturn this.clientCache.getClient(server, opts);\n\t}\n\n\tdefinitionRef(\n\t\tversionId: string,\n\t\tload: (signal: AbortSignal) => Promise<Uint8Array>\n\t): ByteCacheRef {\n\t\treturn this.byteCache.getOrLoad(versionId, load);\n\t}\n\n\t/**\n\t * Drops the warm client for `id`; call it when the server's URL or key changes. Solves already\n\t * running finish on the old connection details first, so a revoked key stays in use until then.\n\t */\n\tevictServer(id: string): void {\n\t\tthis.clientCache.evict(id);\n\t}\n\n\tclearSolveCaches(): void {\n\t\tthis.clientCache.clearSolveCaches();\n\t}\n\n\t/**\n\t * Drop every retained `.gh` byte entry. Safe at any time: entries key on an\n\t * immutable version id, so clearing costs a storage re-read and nothing else.\n\t */\n\tclearDefinitionCache(): void {\n\t\tthis.byteCache.clear();\n\t}\n\n\tstats(): SolveEngineStats {\n\t\treturn {\n\t\t\tclient: this.clientCache.solveCacheStats(),\n\t\t\tdefinitionBytes: this.byteCache.stats(),\n\t\t\tcoalescing: { inFlight: this.singleFlight.inFlight() }\n\t\t};\n\t}\n\n\tasync solve(args: SolveEngineSolveArgs): Promise<SolveEngineOutcome> {\n\t\tconst { definitionSource, identity } = this.resolveDefinitionSource(args);\n\t\tconst startedAt = performance.now();\n\t\tconst elapsed = () => performance.now() - startedAt;\n\n\t\t// `addEventListener` never fires for a signal that already aborted, so a client gone during\n\t\t// the host's own awaits would otherwise still pay for a solve.\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: 0 };\n\n\t\tlet client: CachedClient;\n\t\ttry {\n\t\t\tclient = await this.clientCache.getClient(args.server, {\n\t\t\t\tdefinitionGuid: args.definitionGuid\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\t\t\tif (error instanceof ComputeError && error.code === ErrorCodes.ABORTED) {\n\t\t\t\treturn interrupted(elapsed());\n\t\t\t}\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\n\t\tconst treeBuildStart = performance.now();\n\t\tlet inputTree;\n\t\ttry {\n\t\t\tinputTree = buildSolveInputTree(args.inputs, args.values);\n\t\t} catch (error) {\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t\t// A live solve never shares a flight: the request carries its owner's callback, through which\n\t\t// that owner's Esc aborts the Grasshopper solve. Shared, one user's cancel would abort everyone\n\t\t// who joined, and the joiners would get no progress events.\n\t\tconst live = args.selvaEvents ? `:live:${args.selvaEvents.solveId}` : '';\n\t\tconst flight = await this.singleFlight.run(\n\t\t\t`${identity}:${args.server.id}:${this.clientGeneration(client)}:${stableStringify(inputTree)}${live}`,\n\t\t\t(signal) =>\n\t\t\t\trunSolvePipeline({\n\t\t\t\t\tdefinitionSource,\n\t\t\t\t\tinputTree,\n\t\t\t\t\tclient,\n\t\t\t\t\tresponseMaxBytes: this.limits.computeResponseMaxBytes,\n\t\t\t\t\tselvaEvents: args.selvaEvents,\n\t\t\t\t\tsignal\n\t\t\t\t}),\n\t\t\targs.signal\n\t\t);\n\n\t\t// Before encoding: a caller that left a shared flight must not stringify for nobody.\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\t\tif (flight.kind !== 'solved') return flight;\n\n\t\tlet encoded;\n\t\ttry {\n\t\t\tencoded = await flight.encoder.encode(args.acceptEncoding ?? '');\n\t\t} catch (error) {\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tif (encoded === 'too_large') return { kind: 'too_large' };\n\n\t\tconst { body, serializedBytes, serializeMs, gzipMs } = encoded;\n\t\tconst { solveMs, response } = flight;\n\t\tconst serverTotalMs = performance.now() - (args.loadStartMs ?? startedAt);\n\t\tconst timing = {\n\t\t\tdefLoadMs: args.defLoadMs ?? 0,\n\t\t\ttreeBuildMs,\n\t\t\tsolveMs,\n\t\t\tserializeMs,\n\t\t\tgzipMs,\n\t\t\tserverTotalMs,\n\t\t\trhino: flight.rhino,\n\t\t\tsettle: flight.settle,\n\t\t\t...(flight.defBytes ? { defBytes: flight.defBytes } : {})\n\t\t};\n\t\tconst serverTiming = buildServerTiming({ ...timing, prepMarks: args.prepMarks ?? [] });\n\n\t\treturn {\n\t\t\tkind: 'ok',\n\t\t\tenvelope: buildEnvelope(encoded, response, serverTiming, {\n\t\t\t\t...timing,\n\t\t\t\tserializedBytes,\n\t\t\t\tcompressedBytes: encoded.encoding === 'gzip' ? (body as Uint8Array).byteLength : null\n\t\t\t}),\n\t\t\tsolveMs,\n\t\t\terrorCount: response.errors?.length ?? 0,\n\t\t\twarningCount: response.warnings?.length ?? 0\n\t\t};\n\t}\n\n\t/**\n\t * The response for an outcome; the mapping from outcome to status lives here. Throws\n\t * `compute_error`'s error: what it means for the caller is the host's to decide.\n\t */\n\ttoWebResponse(outcome: SolveEngineOutcome): Response {\n\t\tconst fail = (status: number, body: { message: string; retryAfter?: number }) =>\n\t\t\tnew Response(JSON.stringify(body), {\n\t\t\t\tstatus,\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t...(body.retryAfter !== undefined ? { 'Retry-After': String(body.retryAfter) } : {})\n\t\t\t\t}\n\t\t\t});\n\n\t\tswitch (outcome.kind) {\n\t\t\tcase 'ok':\n\t\t\t\t// The cast is a TS gap: `Uint8Array<ArrayBufferLike>` is not a body type, though it works.\n\t\t\t\treturn new Response(outcome.envelope.body as ConstructorParameters<typeof Response>[0], {\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: outcome.envelope.headers\n\t\t\t\t});\n\t\t\tcase 'timeout':\n\t\t\t\treturn fail(504, { message: outcome.message });\n\t\t\tcase 'client_abort':\n\t\t\t\treturn fail(499, { message: 'Client closed request' });\n\t\t\tcase 'too_large':\n\t\t\t\treturn fail(413, {\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'Solve result is too large to return. This usually means a file output exceeds the supported size.'\n\t\t\t\t});\n\t\t\tcase 'request_too_large':\n\t\t\t\treturn fail(413, { message: outcome.message });\n\t\t\tcase 'shed':\n\t\t\t\treturn fail(503, { message: outcome.message, retryAfter: outcome.retryAfterSeconds });\n\t\t\tcase 'compute_error':\n\t\t\t\tthrow outcome.error;\n\t\t}\n\t}\n\n\t/**\n\t * Part of the coalesce key: after an eviction the id stays and the client changes, and a new\n\t * caller joining a draining flight would get an answer from the old URL or key.\n\t */\n\tprivate clientGeneration(client: CachedClient): number {\n\t\tlet generation = this.clientGenerations.get(client);\n\t\tif (generation === undefined) {\n\t\t\tgeneration = ++this.lastClientGeneration;\n\t\t\tthis.clientGenerations.set(client, generation);\n\t\t}\n\t\treturn generation;\n\t}\n\n\tprivate resolveDefinitionSource(args: SolveEngineSolveArgs): {\n\t\tdefinitionSource: SolveDefinition;\n\t\tidentity: string;\n\t} {\n\t\tconst source = args.definitionSource;\n\n\t\tif (isByteCacheRef(source) || isDefinitionRef(source)) {\n\t\t\treturn { definitionSource: source, identity: source.key };\n\t\t}\n\n\t\t// Raw Uint8Array | string — no natural identity; require the caller's own.\n\t\tif (!args.definitionKey) {\n\t\t\tthrow new Error(\n\t\t\t\t'SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.'\n\t\t\t);\n\t\t}\n\t\treturn { definitionSource: source, identity: args.definitionKey };\n\t}\n}\n"],"mappings":"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"}
|
package/dist/shared.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as SolveFn, i as SolveDiagnostic, n as decodeOutcome, o as SolveResult, r as finalizeResult, t as SolveOutcome } from "./outcome-Bk9mnMi0.js";
|
|
2
2
|
import { t as SolveInput } from "./solve-input-CUjVGYPC.js";
|
|
3
|
-
export {
|
|
3
|
+
export { type SolveDiagnostic, type SolveFn, type SolveInput, type SolveOutcome, type SolveResult, decodeOutcome, finalizeResult };
|
package/dist/shared.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{
|
|
1
|
+
export { n as decodeOutcome, r as finalizeResult } from "./outcome-DNrffAXp.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/solve",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "One owner for the Selva solve flow — input change to solve result, on both sides of the wire, with no transport and no UI",
|
|
5
5
|
"author": "VektorNode",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,60 +23,35 @@
|
|
|
23
23
|
],
|
|
24
24
|
"exports": {
|
|
25
25
|
"./client": {
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"default": "./dist/client.js"
|
|
29
|
-
},
|
|
30
|
-
"require": {
|
|
31
|
-
"types": "./dist/client.d.cts",
|
|
32
|
-
"default": "./dist/client.cjs"
|
|
33
|
-
}
|
|
26
|
+
"types": "./dist/client.d.ts",
|
|
27
|
+
"default": "./dist/client.js"
|
|
34
28
|
},
|
|
35
29
|
"./server": {
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"default": "./dist/server.js"
|
|
39
|
-
},
|
|
40
|
-
"require": {
|
|
41
|
-
"types": "./dist/server.d.cts",
|
|
42
|
-
"default": "./dist/server.cjs"
|
|
43
|
-
}
|
|
30
|
+
"types": "./dist/server.d.ts",
|
|
31
|
+
"default": "./dist/server.js"
|
|
44
32
|
},
|
|
45
33
|
"./shared": {
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
"default": "./dist/shared.js"
|
|
49
|
-
},
|
|
50
|
-
"require": {
|
|
51
|
-
"types": "./dist/shared.d.cts",
|
|
52
|
-
"default": "./dist/shared.cjs"
|
|
53
|
-
}
|
|
34
|
+
"types": "./dist/shared.d.ts",
|
|
35
|
+
"default": "./dist/shared.js"
|
|
54
36
|
}
|
|
55
37
|
},
|
|
56
38
|
"files": [
|
|
57
39
|
"dist",
|
|
58
|
-
"README.md"
|
|
59
|
-
"!**/__tests__/**",
|
|
60
|
-
"!**/*.test.*",
|
|
61
|
-
"!**/*.spec.*"
|
|
40
|
+
"README.md"
|
|
62
41
|
],
|
|
63
42
|
"sideEffects": false,
|
|
64
43
|
"dependencies": {
|
|
65
|
-
"
|
|
66
|
-
"@selvajs/
|
|
67
|
-
"@selvajs/schemas": "5.3.0
|
|
44
|
+
"undici": "^8.12.2",
|
|
45
|
+
"@selvajs/compute": "^5.0.0",
|
|
46
|
+
"@selvajs/schemas": "^5.3.0"
|
|
68
47
|
},
|
|
69
48
|
"devDependencies": {
|
|
70
|
-
"@
|
|
71
|
-
"@types/node": "^26.6.5",
|
|
49
|
+
"@types/node": "^24.0.0",
|
|
72
50
|
"esbuild": "^0.28.2",
|
|
73
|
-
"eslint": "^10.
|
|
74
|
-
"eslint-config-prettier": "^10.1.8",
|
|
75
|
-
"globals": "^17.13.0",
|
|
51
|
+
"eslint": "^10.14.0",
|
|
76
52
|
"prettier": "^3.9.10",
|
|
77
|
-
"tsdown": "^0.23.
|
|
53
|
+
"tsdown": "^0.23.4",
|
|
78
54
|
"typescript": "~6.0.3",
|
|
79
|
-
"vite": "^8.3.5",
|
|
80
55
|
"vitest": "^5.0.5",
|
|
81
56
|
"@selvajs/config": "0.0.4"
|
|
82
57
|
},
|
|
@@ -87,7 +62,7 @@
|
|
|
87
62
|
"access": "public"
|
|
88
63
|
},
|
|
89
64
|
"scripts": {
|
|
90
|
-
"build": "
|
|
65
|
+
"build": "tsdown",
|
|
91
66
|
"dev": "tsdown --watch",
|
|
92
67
|
"lint": "eslint .",
|
|
93
68
|
"lint:fix": "eslint . --fix",
|
|
@@ -95,6 +70,6 @@
|
|
|
95
70
|
"format:check": "prettier --check .",
|
|
96
71
|
"test": "vitest run",
|
|
97
72
|
"test:watch": "vitest watch",
|
|
98
|
-
"type-check": "tsc --noEmit"
|
|
73
|
+
"type-check": "tsc --noEmit && tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.server.json"
|
|
99
74
|
}
|
|
100
75
|
}
|
package/dist/client.cjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./outcome-DUHQoZqq.cjs");let t=require("@selvajs/schemas"),n=require("@selvajs/compute/grasshopper");function r(e,t){return`external:${e}:${t}`}function i(e){let{scopeKey:t,inputId:n,value:i}=e;t&&n&&typeof sessionStorage<`u`&&sessionStorage.setItem(r(t,n),JSON.stringify(i))}function a(e){let{scopeKey:t,inputId:n}=e;if(!t||!n||typeof sessionStorage>`u`)return;let i=sessionStorage.getItem(r(t,n));if(i!==null)try{return JSON.parse(i)}catch{return}}function o(e){let{scopeKey:t,inputId:n}=e;t&&n&&typeof sessionStorage<`u`&&sessionStorage.removeItem(r(t,n))}function s(e){return(0,t.getInputItems)(e).filter(e=>e.source?.kind===`client`).map(e=>({paramId:e.paramId,displayName:e.displayName??e.paramId}))}const c=Object.freeze({solveId:null,running:!1,diagnostics:[],progress:/* @__PURE__ */ new Map,ended:null,other:[],lastSeq:0});function l(t,n){let r=n.solveId===t.solveId?t:{...c,solveId:n.solveId,running:!0};if(n.seq<=r.lastSeq)return t;let i={...r,lastSeq:n.seq},a=n.payload??{};switch(n.type){case`solveStarted`:return{...i,running:!0};case`solveEnded`:return{...i,running:!1,ended:{kind:typeof a.kind==`string`?a.kind:`ok`}};case`diagnostic`:{let[t]=e.t({diagnostics:[a]})?.diagnostics??[];return t?{...i,diagnostics:[...i.diagnostics,t]}:i}case`progress`:{let e=typeof a.source==`string`?a.source:``,t=new Map(i.progress),n=u(a),r=i.progress.get(e)?.fraction;return r!==void 0&&n.fraction!==void 0&&n.fraction<r&&(n.fraction=r),t.set(e,n),{...i,progress:t}}default:return{...i,other:[...i.other,n]}}}function u(e){let t=e=>typeof e==`number`&&Number.isFinite(e)?e:void 0,n=t(e.fraction),r=t(e.done),i=t(e.total),a=t(e.nextFraction),o=t(e.stepMs),s=e=>Math.min(1,Math.max(0,e));return{...typeof e.source==`string`?{source:e.source}:{},...n===void 0?{}:{fraction:s(n)},...r===void 0?{}:{done:r},...i===void 0?{}:{total:i},...typeof e.label==`string`?{label:e.label}:{},...a===void 0?{}:{nextFraction:s(a)},...o!==void 0&&o>0?{stepMs:o}:{}}}function d(e,n,r){let i=new Set(s(e).map(e=>e.paramId)),a={};for(let o of e.inputs)if(i.has(o.id)){let e=r({scopeKey:n,inputId:o.id});e!==void 0&&(a[o.id]=e)}else a[o.id]=o.default??(0,t.getDefaultValue)(o.paramType);for(let t of e.outputs)a[t.id]=null;return a}function f(e){let t=e===!1;return{hasPendingChanges:t,hasNeverSolved:t}}function p(e,t,n,r){return e.values[t]=n,r===!1?(e.pendingValues[t]=n,e.hasPendingChanges=!0,{state:e,shouldSolve:!1}):{state:e,shouldSolve:!0}}function m(e,t){if(!e?.inputs)return t;let n={};for(let r of e.inputs)r.id in t&&(n[r.id]=t[r.id]);return n}function h(e,t){return e.error=``,e.computeErrors=t.errors??[],e.computeWarnings=t.warnings??[],e.diagnostics=t.diagnostics??[],e.blocked=t.blocked??!1,e.blocked?(e.awaitingAck=!1,e.heldResult=null,_(e,t)):y(e.diagnostics)?(e.awaitingAck=!0,e.heldResult=t,e.pendingValues={},e.hasPendingChanges=!1,e):(e.awaitingAck=!1,e.heldResult=null,_(e,t))}function g(e){let t={};for(let n of e?.outputs??[])t[n.id]=null;return t}function _(e,t){e.meshes=t.meshes??[];let{meshes:n,...r}=t;return e.lastResult=r,Object.assign(e.values,t.outputs),e.pendingValues={},e.hasPendingChanges=!1,e.hasNeverSolved=!1,e}function v(e,t){return t?`solving`:e.awaitingAck?`review`:e.blocked?`blocked`:`idle`}function y(e){return(e??[]).some(e=>e.isGate&&e.level!==`remark`)}function b(e){let t=e.schema,n=f(t?.instanceSolve),r={values:d(t,e.scopeKey,a),error:``,computeErrors:[],computeWarnings:[],diagnostics:[],blocked:!1,awaitingAck:!1,heldResult:null,meshes:[],pendingValues:{},hasPendingChanges:n.hasPendingChanges,hasNeverSolved:n.hasNeverSolved,lastResult:null},i=/* @__PURE__ */ new Set,o=()=>i.forEach(e=>e()),s=/* @__PURE__ */ new Set,u=[],y={status:`aborted`,diagnostics:[]};function b(){return new Promise(e=>u.push(e))}function x(e){let t=u;u=[],t.forEach(t=>t(e)),s.forEach(t=>t(e))}function S(){let e=u;u=[],e.forEach(e=>e(y))}function C(e){let t=r.diagnostics;if(e.aborted)return x({status:`aborted`,diagnostics:t});if(e.blocked)return x({status:`blocked`,diagnostics:t});x({status:`applied`,result:r.lastResult,diagnostics:t})}let w=c,T=e.driver.onEvent?.(e=>{let t=l(w,e);t!==w&&(w=t,o())});function E(){let e=b();return D(),e}function D(){e.driver.solve({...m(t,r.values)}),o()}return{get values(){return r.values},get error(){return r.error},get computeErrors(){return r.computeErrors},get computeWarnings(){return r.computeWarnings},get diagnostics(){return r.diagnostics},get blocked(){return r.blocked},get awaitingAck(){return r.awaitingAck},acknowledge(){if(!r.awaitingAck)return;let e=r.heldResult;r.awaitingAck=!1,r.heldResult=null,e&&_(r,e),o(),e&&C(e)},discard(){r.awaitingAck&&(r.awaitingAck=!1,r.heldResult=null,o(),x({status:`discarded`,diagnostics:r.diagnostics}))},get live(){return w},get phase(){return v(r,e.driver.isSolving)},abort(){e.driver.cancel()},get meshes(){return r.meshes},get lastResult(){return r.lastResult},get hasPendingChanges(){return r.hasPendingChanges},get hasNeverSolved(){return r.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(e,n,i=!1){let{shouldSolve:a}=p(r,e,n,t?.instanceSolve);a||i?(i&&!a&&(r.pendingValues={},r.hasPendingChanges=!1),D()):o()},solve:E,ensureSolved(){return e.driver.isSolving||e.driver.hasQueued||r.awaitingAck?b():r.hasPendingChanges||r.hasNeverSolved||r.error||!r.lastResult?E():r.blocked?Promise.resolve({status:`blocked`,diagnostics:r.diagnostics}):Promise.resolve({status:`applied`,result:r.lastResult,diagnostics:r.diagnostics})},onSettled(e){return s.add(e),()=>s.delete(e)},loadValues(e){Object.assign(r.values,e),t?.instanceSolve===!1?(r.hasPendingChanges=!0,o()):D()},rebuild(n,i){S(),t=n,e.driver.clearCache?.(),r.meshes=[],r.lastResult=null,r.error=``,r.computeErrors=[],r.computeWarnings=[],r.diagnostics=[],r.blocked=!1,r.awaitingAck=!1,r.heldResult=null,r.pendingValues={},r.values=d(n,i,a);let s=f(n?.instanceSolve);r.hasPendingChanges=s.hasPendingChanges,r.hasNeverSolved=s.hasNeverSolved,n&&Object.keys(r.values).length>0&&n.instanceSolve!==!1?D():o()},report(n){h(r,n.blocked||n.aborted?{...n,outputs:g(t),meshes:[]}:n),o(),!(r.awaitingAck||e.driver.hasQueued)&&C(n)},reportError(t){r.error=t,o(),e.driver.hasQueued||x({status:`failed`,error:t})},reportAborted(){o(),e.driver.hasQueued||x(y)},subscribe(e){return i.add(e),()=>i.delete(e)},notify:o,dispose(){T?.(),i.clear(),s.clear(),S()}}}function x(e,t){let{runDeadlineMs:n,onChange:r}=t,i=!1,a=null,o=null;function s(e){i!==e&&(i=e,r?.())}function c(){o?.abort(),o=null}async function l(t){c(),o=new AbortController;let{signal:r}=o,i=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${n}ms — aborting`),o?.abort(new DOMException(`Run deadline exceeded`,`TimeoutError`))},n);s(!0);try{await e(t,r)}catch(e){(!(e instanceof Error)||e.name!==`AbortError`&&e.name!==`TimeoutError`)&&console.error(`[Solve/throttle] unhandled error in run:`,e)}finally{if(clearTimeout(i),s(!1),o=null,a!==null){let e=a;a=null,l(e)}}}function u(e){i?(a!==null&&console.debug(`[Solve/throttle] superseded pending run (latest-wins)`),a=e):l(e)}function d(){a=null,c()}return{trigger:u,get isRunning(){return i},get hasPending(){return a!==null},cancel:d}}function S(e){return C(e)}function C(e){if(typeof e!=`object`||!e)return JSON.stringify(e)??`null`;if(Array.isArray(e))return`[${e.map(C).join(`,`)}]`;let t=e;return`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${C(t[e])}`).join(`,`)}}`}function w(e={}){let{max:t=16,meshPolicy:n}=e,r=/* @__PURE__ */ new Map;function i(e){e.meshes?.length&&n?.release(e.meshes)}function a(e){return!n||!e.meshes?.length?e:{...e,meshes:n.clone(e.meshes)}}function o(e){let t=r.get(e);t!==void 0&&(r.delete(e),i(t))}return{get(e){let n=S(e),i=r.get(n);if(i!==void 0)return r.delete(n),r.set(n,i),console.info(`[Solve/memo] HIT — served from client memo (${r.size}/${t})`),a(i)},set(e,n){let i=S(e);for(o(i),r.set(i,a(n));r.size>t;){let e=r.keys().next().value;if(e===void 0)break;o(e),console.info(`[Solve/memo] evicted LRU entry (cap ${t})`)}},clear(){r.size>0&&console.info(`[Solve/memo] cleared ${r.size} entries (definition changed)`),r.forEach(i),r.clear()}}}function T(e,t,n){let{meshPolicy:r,solveDeadlineMs:i,onChange:a,events:o}=n,s=w({meshPolicy:r});function c(e){e.reason?.name===`TimeoutError`?t().reportError(`The solve took longer than ${Math.round(i/1e3)} s and was stopped.`):t().reportAborted()}let l=x(async(n,r)=>{let i=s.get(n);if(i!==void 0){if(r.aborted)return c(r);t().report(i)}else try{let i=await e(n,r);if(r.aborted)return console.debug(`[Solve/driver] solve completed after abort — result discarded`),c(r);let a={...i,values:n};s.set(n,a),t().report(a)}catch(e){if(r.aborted)return console.debug(`[Solve/driver] solve aborted (cancelled or timed out)`),c(r);console.warn(`[Solve/driver] solve failed:`,e),t().reportError(e instanceof Error?e.message:String(e))}},{runDeadlineMs:i,onChange:a});return{solve(e){l.trigger(e)},cancel(){l.cancel(),o?.cancelCurrent?.()},get isSolving(){return l.isRunning},get hasQueued(){return l.hasPending},clearCache(){s.clear()},...o?{onEvent:e=>o.subscribe(e)}:{}}}function E(){return typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID().replace(/-/g,``):Array.from({length:32},()=>Math.floor(Math.random()*16).toString(16)).join(``)}function D(e){let t=E(),n=/* @__PURE__ */ new Set,r=e.fetch??((...e)=>fetch(...e)),i=null,a=null;function o(){if(i||typeof EventSource>`u`)return;let r=`${e.endpoint}${e.endpoint.includes(`?`)?`&`:`?`}streamId=${t}`;i=new EventSource(r),i.addEventListener(`solve`,e=>{let t;try{t=JSON.parse(e.data)}catch{return}t.type===`solveStarted`?a=t.solveId:t.type===`solveEnded`&&a===t.solveId&&(a=null);for(let e of n)e(t)})}return o(),{streamId:t,get currentSolveId(){return a},subscribe(e){return n.add(e),o(),()=>{n.delete(e)}},cancelCurrent(){let t=a;t&&r(e.cancelEndpoint(t),{method:`POST`,keepalive:!0}).catch(()=>{})},close(){i?.close(),i=null,n.clear(),a=null}}}function O(e){let t={};if(!e)return t;for(let n of e.split(`,`)){let[e,...r]=n.split(`;`).map(e=>e.trim()),i=r.find(e=>e.startsWith(`dur=`));if(!e||!i)continue;let a=Number(i.slice(4));Number.isFinite(a)&&(t[e]=a)}return t}const k=e=>{throw Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},A=()=>{throw Error(`Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.`)};function j(t){let r=t.debug??!1,i=(...e)=>{r&&console.info(...e)},a=0;return async(o,s)=>{let c=a-Date.now();if(c>0)throw Error(`Rate limit reached. Try again in ${Math.ceil(c/1e3)}s.`);let l=performance.now(),u=t.inputs(),d=t.outputs(),f=t.channel?.(),p=t.versionId?.(),m=t.streamId?.(),h=JSON.stringify({inputs:u,values:o,definitionUrl:t.definitionUrl(),...p?{versionId:p}:f===`draft`?{channel:`draft`}:{},...m?{streamId:m}:{}}),g;try{g=await fetch(t.endpoint,{method:`POST`,headers:{"Content-Type":`application/json`},body:h,signal:s})}catch(e){if(s.aborted)return i(`[Compute] solve aborted during fetch — empty result discarded`),{outputs:{}};throw Error(`Request blocked — your session may have expired. Reload the page to sign in again.`,{cause:e})}if(s.aborted)return i(`[Compute] solve aborted after headers — empty result discarded`),{outputs:{}};let _=g.redirected&&new URL(g.url).pathname===`/login`;if((g.status===401||_)&&(t.onSessionExpired??A)(),!g.ok){let e=await g.text().catch(()=>``),n={};try{n=JSON.parse(e)}catch{e&&console.warn(`[Compute] non-JSON error body (HTTP ${g.status}):`,e.slice(0,300))}if(g.status===503||g.status===504)throw Error(n.message||`Compute server is offline or unreachable. Please try again later.`);if(g.status===429){let e=Number(g.headers.get(`Retry-After`))||Number(n.retryAfter)||5;a=Date.now()+e*1e3,(t.onRateLimited??k)(e)}throw Error(n.message||`Compute error (HTTP ${g.status})`)}let v=performance.now()-l,y=performance.now(),b=await g.text();if(s.aborted)return i(`[Compute] solve aborted mid-download — empty result discarded`),{outputs:{}};let x=performance.now()-y,S=b.length,C;try{C=JSON.parse(b)}catch(e){throw Error(`Received an invalid response from the server. Reload the page and try again.`,{cause:e})}let w=new n.GrasshopperResponseProcessor(C),T=e.t(C.selva?.outcome)??e.r(C.errors??[],C.warnings??[]),E=T.blocked||T.aborted,D=t.meshes&&!E?await t.meshes.extract(w.response,{debug:r}):[],j={};for(let e of E?[]:d){let t=w.getValue({byId:e.id},{parseValues:!0});j[e.id]=t??(e.nickname?w.getValue({byName:e.nickname},{parseValues:!0}):void 0)}if(r){let e=JSON.stringify(o).length,t=O(g.headers.get(`Server-Timing`)),n=t.total??0,r=Math.max(0,v-n),a=S/1048576,s=a>=1?`${a.toFixed(2)} MB`:`${(S/1024).toFixed(0)} KB`,c=x>0?(a/(x/1e3)).toFixed(1):`∞`,l=performance.memory?.usedJSHeapSize,u=l===void 0?``:` | heap=${(l/1048576).toFixed(0)} MB`;if(i(`[Compute] req=${(h.length/1024).toFixed(0)}KB (values ${(e/1024).toFixed(0)}KB) | ttfb=${v.toFixed(0)}ms (network≈${r.toFixed(0)} + server ${n.toFixed(0)}) | download=${x.toFixed(0)}ms (${s} @ ${c} MB/s)${u}`),t.selva_cache!==void 0){let e=t.selva_cache?`HIT — served without calling compute`:t.rhino_cache?`HIT — Rhino.Compute's result cache, solved nothing`:`miss — solved on Rhino.Compute`;i(`[Compute] └─ cache: ${e}`)}}return e.n({outputs:j,meshes:D,source:C},T)}}exports.EMPTY_LIVE_SOLVE=c,exports.applySolveResult=h,exports.applyValueChange=p,exports.buildInitialValues=d,exports.clearExternalValue=o,exports.createAsyncThrottle=x,exports.createComputeFetchSolveFn=j,exports.createRequestResponseDriver=T,exports.createSolveEventStream=D,exports.createSolveMemo=w,exports.createSolveSession=b,exports.decodeOutcome=e.t,exports.finalizeResult=e.n,exports.getExternalInputs=s,exports.makeInitialFlags=f,exports.pickInputValues=m,exports.readExternalValue=a,exports.reduceLiveSolve=l,exports.solvePhase=v,exports.stableInputKey=S,exports.writeExternalValue=i;
|
|
2
|
-
//# sourceMappingURL=client.cjs.map
|