@selvajs/solve 0.2.0-beta.1 → 0.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client/external-storage.ts","../src/client/solve-session-core.ts","../src/client/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts"],"sourcesContent":["// Transit storage for inputs with source.kind === 'client': a producer route writes the\n// value here, the solver route reads it back. Keyed by (scopeKey, inputId) so values\n// don't bleed across solver contexts — scopeKey is whatever uniquely identifies one\n// (sessionId in plugin-ui/preview, definition guid in selva/library).\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nfunction makeKey(scopeKey: string, inputId: string): string {\n\treturn `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;\n}\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst { scopeKey, inputId, value } = args;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return undefined;\n\tif (typeof sessionStorage === 'undefined') return undefined;\n\tconst raw = sessionStorage.getItem(makeKey(scopeKey, inputId));\n\tif (raw === null) return undefined;\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function clearExternalValue(ref: ExternalValueRef): void {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.removeItem(makeKey(scopeKey, inputId));\n}\n\nexport interface ExternalInput {\n\tparamId: string;\n\tdisplayName: string;\n}\n\nexport function getExternalInputs(schema: UISchema): ExternalInput[] {\n\treturn getInputItems(schema)\n\t\t.filter((item) => item.source?.kind === 'client')\n\t\t.map((item) => ({\n\t\t\tparamId: item.paramId,\n\t\t\tdisplayName: item.displayName ?? item.paramId\n\t\t}));\n}\n","// Pure transition logic for a Solve Session.\n//\n// This module holds the value/lifecycle state machine with no transport and no state\n// ownership: how values are seeded (including client-sourced hydration), when a value\n// change should dispatch a solve vs. defer it, and what a reported solve result does to\n// the flags. `solve-session.ts` is a thin shell that owns the state object and the\n// subscriber set; every decision lives here, which is what makes it directly testable.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getDefaultValue } from '@selvajs/schemas';\nimport { getExternalInputs, type ExternalValueRef } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\n\nexport interface SolveSessionState {\n\tvalues: Record<string, unknown>;\n\terror: string;\n\tcomputeErrors: string[];\n\tcomputeWarnings: string[];\n\tmeshes: unknown[];\n\t/** Values changed since the last solve, in manual (instanceSolve === false) mode. */\n\tpendingValues: Record<string, unknown>;\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n}\n\nexport type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;\n\n/**\n * Client-sourced inputs are hydrated from `read` and left absent when no stored value\n * exists, so the missing-inputs panel can detect them. Other inputs fall back to their\n * declared default, then the paramType default.\n */\nexport function buildInitialValues(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): Record<string, unknown> {\n\tconst clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));\n\tconst values: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (clientSet.has(input.id)) {\n\t\t\tconst stored = read({ scopeKey, inputId: input.id });\n\t\t\tif (stored !== undefined) values[input.id] = stored;\n\t\t\tcontinue;\n\t\t}\n\t\tvalues[input.id] = input.default ?? getDefaultValue(input.paramType);\n\t}\n\tfor (const output of schema.outputs) {\n\t\tvalues[output.id] = null;\n\t}\n\treturn values;\n}\n\n/** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */\nexport function makeInitialFlags(instanceSolve: boolean | undefined): {\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n} {\n\tconst manual = instanceSolve === false;\n\treturn { hasPendingChanges: manual, hasNeverSolved: manual };\n}\n\n/** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined\n): { state: SolveSessionState; shouldSolve: boolean } {\n\tstate.values[id] = value;\n\tif (instanceSolve === false) {\n\t\tstate.pendingValues[id] = value;\n\t\tstate.hasPendingChanges = true;\n\t\treturn { state, shouldSolve: false };\n\t}\n\treturn { state, shouldSolve: true };\n}\n\n/**\n * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value\n * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB\n * options-list output showed this can re-upload a multi-MB payload no backend reads.\n */\nexport function pickInputValues(\n\tschema: UISchema | undefined,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tif (!schema?.inputs) return values;\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (input.id in values) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\nexport function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.meshes = result.meshes ?? [];\n\tObject.assign(state.values, result.outputs);\n\tstate.pendingValues = {};\n\tstate.hasPendingChanges = false;\n\tstate.hasNeverSolved = false;\n\treturn state;\n}\n","// A framework-free shell over the pure transition logic in solve-session-core.ts: owns the\n// live values/flags, delegates every transition to the core, and drives solves through a\n// transport-agnostic SolveDriver.\n//\n// State is exposed as plain getters; it carries no framework reactivity of its own, so a\n// Svelte host must wrap it — see `useSolveSession.svelte.ts` in `@selvajs/ui`, which\n// republishes these getters as $state. Reading a getter without subscribing gives a\n// correct value but will not re-render.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { readExternalValue } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\nimport type { SolveDriver } from './drivers/driver.js';\nimport {\n\tbuildInitialValues,\n\tmakeInitialFlags,\n\tapplyValueChange,\n\tapplySolveResult,\n\tpickInputValues,\n\ttype SolveSessionState\n} from './solve-session-core.js';\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\treadonly meshes: unknown[];\n\treadonly hasPendingChanges: boolean;\n\treadonly hasNeverSolved: boolean;\n\treadonly isSolving: boolean;\n\t/** `forceSolve` overrides manual mode for system-initiated reconciliation (e.g. a dynamic value list pruning a vanished selection), so the output can't lag behind the new value. */\n\tsetValue(id: string, value: unknown, forceSolve?: boolean): void;\n\tsolve(): void;\n\t/** Merges incoming values, then solves (auto) or marks dirty (manual). */\n\tloadValues(incoming: Record<string, unknown>): void;\n\trebuild(schema: UISchema, scopeKey: string): void;\n\treport(result: SolveResult): void;\n\treportError(message: string): void;\n\t/** Fires after any mutation of the getters above, including driver-reported `isSolving` transitions. */\n\tsubscribe(listener: () => void): () => void;\n\t/** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */\n\tnotify(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\n\tconst flags = makeInitialFlags(currentSchema?.instanceSolve);\n\tconst state: SolveSessionState = {\n\t\tvalues: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tmeshes: [],\n\t\tpendingValues: {},\n\t\thasPendingChanges: flags.hasPendingChanges,\n\t\thasNeverSolved: flags.hasNeverSolved\n\t};\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => listeners.forEach((l) => l());\n\n\tfunction dispatch() {\n\t\t// Copy: state.values is the live map, and a driver may hold the payload across an\n\t\t// async solve — a later setValue must not mutate what's already in flight.\n\t\targs.driver.solve({ ...pickInputValues(currentSchema, state.values) });\n\t\temit();\n\t}\n\n\treturn {\n\t\tget values() {\n\t\t\treturn state.values;\n\t\t},\n\t\tget error() {\n\t\t\treturn state.error;\n\t\t},\n\t\tget computeErrors() {\n\t\t\treturn state.computeErrors;\n\t\t},\n\t\tget computeWarnings() {\n\t\t\treturn state.computeWarnings;\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget hasPendingChanges() {\n\t\t\treturn state.hasPendingChanges;\n\t\t},\n\t\tget hasNeverSolved() {\n\t\t\treturn state.hasNeverSolved;\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn args.driver.isSolving;\n\t\t},\n\n\t\tsetValue(id, value, forceSolve = false) {\n\t\t\tconst { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);\n\t\t\tif (shouldSolve || forceSolve) {\n\t\t\t\tif (forceSolve && !shouldSolve) {\n\t\t\t\t\tstate.pendingValues = {};\n\t\t\t\t\tstate.hasPendingChanges = false;\n\t\t\t\t}\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\tsolve() {\n\t\t\tdispatch();\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tObject.assign(state.values, incoming);\n\t\t\tif (currentSchema?.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\tstate.hasPendingChanges = true;\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\trebuild(schema, scopeKey) {\n\t\t\tcurrentSchema = schema;\n\t\t\t// The new definition has its own input space; a matching input key from the prior\n\t\t\t// one must not serve that definition's cached result.\n\t\t\targs.driver.clearCache?.();\n\t\t\tstate.meshes = [];\n\t\t\tstate.error = '';\n\t\t\tstate.computeErrors = [];\n\t\t\tstate.computeWarnings = [];\n\t\t\tstate.pendingValues = {};\n\t\t\tstate.values = buildInitialValues(schema, scopeKey, readExternalValue);\n\t\t\tconst f = makeInitialFlags(schema?.instanceSolve);\n\t\t\tstate.hasPendingChanges = f.hasPendingChanges;\n\t\t\tstate.hasNeverSolved = f.hasNeverSolved;\n\t\t\tif (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\treport(result) {\n\t\t\tapplySolveResult(state, result);\n\t\t\temit();\n\t\t},\n\n\t\treportError(message) {\n\t\t\tstate.error = message;\n\t\t\temit();\n\t\t},\n\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => listeners.delete(listener);\n\t\t},\n\n\t\tnotify: emit\n\t};\n}\n","/**\n * Single-in-flight, latest-wins throttle for any async operation: only one run at a\n * time, a newly triggered value overwrites the single pending slot (no queue), and a\n * stale run can be aborted via `AbortController` or timeout.\n *\n * Framework-free: `isRunning` is a plain getter, not a store. A reactive host must\n * subscribe via `onChange` (see `@selvajs/ui`'s rune adapter) — polling the getter\n * directly reads correct values but won't trigger a re-render.\n */\ninterface AsyncThrottleOptions {\n\ttimeout?: number;\n\tonChange?: () => void;\n}\n\nexport interface AsyncThrottle<T> {\n\ttrigger: (values: T) => void;\n\treadonly isRunning: boolean;\n\treadonly hasPending: boolean;\n\tcancel: () => void;\n}\n\n/** Fallback per-run abort timeout (ms) when the caller doesn't pass one. */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\nexport function createAsyncThrottle<T>(\n\trun: (values: T, signal: AbortSignal) => Promise<void>,\n\toptions: AsyncThrottleOptions = {}\n): AsyncThrottle<T> {\n\tconst { timeout = DEFAULT_TIMEOUT_MS, onChange } = options;\n\n\tlet isRunning = false;\n\tlet pendingValues: T | null = null;\n\tlet currentAbortController: AbortController | null = null;\n\n\tfunction setRunning(next: boolean) {\n\t\tif (isRunning === next) return;\n\t\tisRunning = next;\n\t\tonChange?.();\n\t}\n\n\tfunction abortCurrent() {\n\t\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\n\t}\n\n\tasync function execute(values: T) {\n\t\tabortCurrent();\n\n\t\tcurrentAbortController = new AbortController();\n\t\tconst { signal } = currentAbortController;\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\t// Cleared in `finally` on every other path, so firing means a genuine\n\t\t\t// timeout — the only signal for it (the abort itself is swallowed below).\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${timeout}ms — aborting`);\n\t\t\tcurrentAbortController?.abort();\n\t\t}, timeout);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, signal);\n\t\t} catch (err) {\n\t\t\t// AbortError is expected (timeout or cancel). Non-abort errors must be\n\t\t\t// handled inside `run` — re-throwing here would produce an unhandled\n\t\t\t// rejection because `execute` is always called fire-and-forget.\n\t\t\tif (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {\n\t\t\t\tconsole.error('[Solve/throttle] unhandled error in run:', err);\n\t\t\t}\n\t\t} finally {\n\t\t\tclearTimeout(timeoutId);\n\t\t\tsetRunning(false);\n\t\t\tcurrentAbortController = null;\n\n\t\t\tif (pendingValues !== null) {\n\t\t\t\tconst next = pendingValues;\n\t\t\t\tpendingValues = null;\n\t\t\t\texecute(next);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction trigger(values: T) {\n\t\tif (isRunning) {\n\t\t\tif (pendingValues !== null) {\n\t\t\t\t// Latest-wins: the previously-queued values are dropped, not run.\n\t\t\t\t// `debug`, not `info`: a slider scrub fires this per frame, and it is a trace of\n\t\t\t\t// normal operation. Promoting it would spam a host's console on every drag.\n\t\t\t\t// eslint-disable-next-line no-console -- see above\n\t\t\t\tconsole.debug('[Solve/throttle] superseded pending run (latest-wins)');\n\t\t\t}\n\t\t\tpendingValues = values;\n\t\t} else {\n\t\t\texecute(values);\n\t\t}\n\t}\n\n\tfunction cancel() {\n\t\tpendingValues = null;\n\t\tabortCurrent();\n\t}\n\n\treturn {\n\t\ttrigger,\n\t\tget isRunning() {\n\t\t\treturn isRunning;\n\t\t},\n\t\tget hasPending() {\n\t\t\treturn pendingValues !== null;\n\t\t},\n\t\tcancel\n\t};\n}\n","// A small LRU keyed on a stable serialization of the solve inputs, sitting in front of the\n// request/response driver: re-solving inputs already seen this session (e.g. dragging a\n// slider back) returns instantly instead of round-tripping. A hit is always a complete\n// result, since only fully-solved values are ever stored.\n//\n// A SolveResult can carry live renderer objects, and a viewer typically takes ownership of\n// every mesh array it renders — disposing the previous content on the next update. So the\n// memo can neither hand out its own instances (they'd be disposed under it, then re-added\n// dead on the next hit) nor drop entries silently (their GPU buffers would leak). It\n// therefore keeps private copies, serves a fresh copy per hit, and releases an entry\n// whenever it leaves the map — but it does not know what a mesh is: `TMesh` is opaque and\n// the clone/release policy is injected, which keeps `three` out of this package entirely.\n\nimport type { SolveResult } from '../shared/solve-fn.js';\n\n/**\n * Deterministic string key for a set of input values. Object keys are sorted at every\n * level so two logically-equal inputs (built in different key order) collide, matching the\n * server's stable-input keying intent. Values are plain JSON — the projected solve inputs\n * never contain functions or cycles.\n */\nexport function stableInputKey(values: Record<string, unknown>): string {\n\treturn serialize(values);\n}\n\nfunction serialize(value: unknown): string {\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(serialize).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(',')}}`;\n}\n\n/**\n * Omit for mesh-free results, or for meshes no consumer takes ownership of: the memo then\n * stores and serves them by reference, which is correct precisely because nothing frees\n * them behind its back.\n *\n * @property clone - Called on the way in AND on every hit, so neither the stored entry nor\n * a previously served one can be disposed by the other's owner.\n * @property release - Must NOT free anything shared with live content — see\n * `releaseSceneObjects` in `@selvajs/visualization/parse`, which skips materials for\n * exactly that reason.\n */\nexport interface MeshPolicy<TMesh> {\n\tclone(meshes: TMesh[]): TMesh[];\n\trelease(meshes: TMesh[]): void;\n}\n\nexport interface SolveMemoOptions<TMesh> {\n\t/**\n\t * Entry cap (entries, not bytes). Solve results can be large, so the default is\n\t * deliberately small — this targets the tight slider-scrub loop, not a durable cache.\n\t */\n\tmax?: number;\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\nexport interface SolveMemo<TMesh = unknown> {\n\tget(values: Record<string, unknown>): SolveResult<TMesh> | undefined;\n\t/** Evicts the LRU tail if `set` pushes the map past its cap. */\n\tset(values: Record<string, unknown>, result: SolveResult<TMesh>): void;\n\tclear(): void;\n}\n\nexport function createSolveMemo<TMesh = unknown>(\n\toptions: SolveMemoOptions<TMesh> = {}\n): SolveMemo<TMesh> {\n\tconst { max = 16, meshPolicy } = options;\n\tconst entries = new Map<string, SolveResult<TMesh>>();\n\n\tfunction release(result: SolveResult<TMesh>): void {\n\t\tif (result.meshes?.length) meshPolicy?.release(result.meshes);\n\t}\n\n\tfunction copy(result: SolveResult<TMesh>): SolveResult<TMesh> {\n\t\tif (!meshPolicy || !result.meshes?.length) return result;\n\t\treturn { ...result, meshes: meshPolicy.clone(result.meshes) };\n\t}\n\n\tfunction evict(key: string): void {\n\t\tconst entry = entries.get(key);\n\t\tif (entry === undefined) return;\n\t\tentries.delete(key);\n\t\trelease(entry);\n\t}\n\n\treturn {\n\t\tget(values) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tconst hit = entries.get(key);\n\t\t\tif (hit === undefined) return undefined;\n\t\t\t// Refresh recency: re-insert at the tail.\n\t\t\tentries.delete(key);\n\t\t\tentries.set(key, hit);\n\t\t\t// A memo hit skips the transport entirely, so no other log line fires —\n\t\t\t// this line is the only trace it wasn't a fresh solve.\n\t\t\tconsole.info(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);\n\t\t\t// The consumer disposes what it renders, so the retained entry must never be\n\t\t\t// the same instance handed out twice.\n\t\t\treturn copy(hit);\n\t\t},\n\t\tset(values, result) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\t// Overwriting a key strands the old value's resources unless it's released first.\n\t\t\tevict(key);\n\t\t\tentries.set(key, copy(result));\n\t\t\twhile (entries.size > max) {\n\t\t\t\tconst oldest = entries.keys().next().value;\n\t\t\t\tif (oldest === undefined) break;\n\t\t\t\tevict(oldest);\n\t\t\t\tconsole.info(`[Solve/memo] evicted LRU entry (cap ${max})`);\n\t\t\t}\n\t\t},\n\t\tclear() {\n\t\t\tif (entries.size > 0) {\n\t\t\t\tconsole.info(`[Solve/memo] cleared ${entries.size} entries (definition changed)`);\n\t\t\t}\n\t\t\tentries.forEach(release);\n\t\t\tentries.clear();\n\t\t}\n\t};\n}\n","import { createAsyncThrottle } from '../async-throttle.js';\nimport { createSolveMemo, type MeshPolicy } from '../solve-memo.js';\nimport type { SolveFn } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\ttimeout?: number;\n\tonChange?: () => void;\n\t/** Mesh ownership policy for the result memo — see `MeshPolicy` (solve-memo.ts). */\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\n/**\n * `getReporter` is lazy (`() => session`) because the session and driver reference each\n * other — the host builds the driver first, then the session, then can hand this back.\n */\nexport function createRequestResponseDriver<TMesh = unknown>(\n\tonSolve: SolveFn<TMesh>,\n\tgetReporter: () => SolveReporter<TMesh>,\n\toptions: RequestResponseDriverOptions<TMesh> = {}\n): SolveDriver {\n\tconst { meshPolicy, ...throttleOptions } = options;\n\n\t// Checked inside the throttled run, not before triggering it, so a hit only serves\n\t// after the throttle's latest-wins ordering has picked these values to run.\n\tconst memo = createSolveMemo<TMesh>({ meshPolicy });\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(async (values, signal) => {\n\t\tconst cached = memo.get(values);\n\t\tif (cached !== undefined) {\n\t\t\tif (signal.aborted) return;\n\t\t\tgetReporter().report(cached);\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst result = await onSolve(values, signal);\n\t\t\tif (signal.aborted) {\n\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\tconsole.debug('[Solve/driver] solve completed after abort — result discarded');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tmemo.set(values, result);\n\t\t\tgetReporter().report(result);\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\tconsole.debug('[Solve/driver] solve aborted (superseded, cancelled, or timed out)');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\tgetReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t}\n\t}, throttleOptions);\n\n\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel() {\n\t\t\tthrottle.cancel();\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tclearCache() {\n\t\t\tmemo.clear();\n\t\t}\n\t};\n}\n"],"mappings":"AAMA,OAAS,iBAAAA,MAAqB,mBAE9B,IAAMC,EAAiB,WAEvB,SAASC,EAAQC,EAAkBC,EAAyB,CAC3D,MAAO,GAAGH,CAAc,IAAIE,CAAQ,IAAIC,CAAO,EAChD,CAOO,SAASC,EAAmBC,EAAmD,CACrF,GAAM,CAAE,SAAAH,EAAU,QAAAC,EAAS,MAAAG,CAAM,EAAID,EACjC,CAACH,GAAY,CAACC,GACd,OAAO,eAAmB,KAC9B,eAAe,QAAQF,EAAQC,EAAUC,CAAO,EAAG,KAAK,UAAUG,CAAK,CAAC,CACzE,CAEO,SAASC,EAAkBC,EAA4C,CAC7E,GAAM,CAAE,SAAAN,EAAU,QAAAC,CAAQ,EAAIK,EAE9B,GADI,CAACN,GAAY,CAACC,GACd,OAAO,eAAmB,IAAa,OAC3C,IAAMM,EAAM,eAAe,QAAQR,EAAQC,EAAUC,CAAO,CAAC,EAC7D,GAAIM,IAAQ,KACZ,GAAI,CACH,OAAO,KAAK,MAAMA,CAAG,CACtB,MAAQ,CACP,MACD,CACD,CAEO,SAASC,EAAmBF,EAA6B,CAC/D,GAAM,CAAE,SAAAN,EAAU,QAAAC,CAAQ,EAAIK,EAC1B,CAACN,GAAY,CAACC,GACd,OAAO,eAAmB,KAC9B,eAAe,WAAWF,EAAQC,EAAUC,CAAO,CAAC,CACrD,CAOO,SAASQ,EAAkBC,EAAmC,CACpE,OAAOb,EAAca,CAAM,EACzB,OAAQC,GAASA,EAAK,QAAQ,OAAS,QAAQ,EAC/C,IAAKA,IAAU,CACf,QAASA,EAAK,QACd,YAAaA,EAAK,aAAeA,EAAK,OACvC,EAAE,CACJ,CCjDA,OAAS,mBAAAC,MAAuB,mBAuBzB,SAASC,EACfC,EACAC,EACAC,EAC0B,CAC1B,IAAMC,EAAY,IAAI,IAAIC,EAAkBJ,CAAM,EAAE,IAAKK,GAAMA,EAAE,OAAO,CAAC,EACnEC,EAAkC,CAAC,EACzC,QAAWC,KAASP,EAAO,OAAQ,CAClC,GAAIG,EAAU,IAAII,EAAM,EAAE,EAAG,CAC5B,IAAMC,EAASN,EAAK,CAAE,SAAAD,EAAU,QAASM,EAAM,EAAG,CAAC,EAC/CC,IAAW,SAAWF,EAAOC,EAAM,EAAE,EAAIC,GAC7C,QACD,CACAF,EAAOC,EAAM,EAAE,EAAIA,EAAM,SAAWE,EAAgBF,EAAM,SAAS,CACpE,CACA,QAAWG,KAAUV,EAAO,QAC3BM,EAAOI,EAAO,EAAE,EAAI,KAErB,OAAOJ,CACR,CAGO,SAASK,EAAiBC,EAG/B,CACD,IAAMC,EAASD,IAAkB,GACjC,MAAO,CAAE,kBAAmBC,EAAQ,eAAgBA,CAAO,CAC5D,CAGO,SAASC,EACfC,EACAC,EACAC,EACAL,EACqD,CAErD,OADAG,EAAM,OAAOC,CAAE,EAAIC,EACfL,IAAkB,IACrBG,EAAM,cAAcC,CAAE,EAAIC,EAC1BF,EAAM,kBAAoB,GACnB,CAAE,MAAAA,EAAO,YAAa,EAAM,GAE7B,CAAE,MAAAA,EAAO,YAAa,EAAK,CACnC,CAOO,SAASG,EACflB,EACAM,EAC0B,CAC1B,GAAI,CAACN,GAAQ,OAAQ,OAAOM,EAC5B,IAAMa,EAAkC,CAAC,EACzC,QAAWZ,KAASP,EAAO,OACtBO,EAAM,MAAMD,IAAQa,EAAOZ,EAAM,EAAE,EAAID,EAAOC,EAAM,EAAE,GAE3D,OAAOY,CACR,CAEO,SAASC,EAAiBL,EAA0BM,EAAwC,CAClG,OAAAN,EAAM,MAAQ,GACdA,EAAM,cAAgBM,EAAO,QAAU,CAAC,EACxCN,EAAM,gBAAkBM,EAAO,UAAY,CAAC,EAC5CN,EAAM,OAASM,EAAO,QAAU,CAAC,EACjC,OAAO,OAAON,EAAM,OAAQM,EAAO,OAAO,EAC1CN,EAAM,cAAgB,CAAC,EACvBA,EAAM,kBAAoB,GAC1BA,EAAM,eAAiB,GAChBA,CACR,CCtDO,SAASO,EAAmBC,EAAsC,CACxE,IAAIC,EAAgBD,EAAK,OAEnBE,EAAQC,EAAiBF,GAAe,aAAa,EACrDG,EAA2B,CAChC,OAAQC,EAAmBJ,EAAeD,EAAK,SAAUM,CAAiB,EAC1E,MAAO,GACP,cAAe,CAAC,EAChB,gBAAiB,CAAC,EAClB,OAAQ,CAAC,EACT,cAAe,CAAC,EAChB,kBAAmBJ,EAAM,kBACzB,eAAgBA,EAAM,cACvB,EAEMK,EAAY,IAAI,IAChBC,EAAO,IAAMD,EAAU,QAASE,GAAMA,EAAE,CAAC,EAE/C,SAASC,GAAW,CAGnBV,EAAK,OAAO,MAAM,CAAE,GAAGW,EAAgBV,EAAeG,EAAM,MAAM,CAAE,CAAC,EACrEI,EAAK,CACN,CAEA,MAAO,CACN,IAAI,QAAS,CACZ,OAAOJ,EAAM,MACd,EACA,IAAI,OAAQ,CACX,OAAOA,EAAM,KACd,EACA,IAAI,eAAgB,CACnB,OAAOA,EAAM,aACd,EACA,IAAI,iBAAkB,CACrB,OAAOA,EAAM,eACd,EACA,IAAI,QAAS,CACZ,OAAOA,EAAM,MACd,EACA,IAAI,mBAAoB,CACvB,OAAOA,EAAM,iBACd,EACA,IAAI,gBAAiB,CACpB,OAAOA,EAAM,cACd,EACA,IAAI,WAAY,CACf,OAAOJ,EAAK,OAAO,SACpB,EAEA,SAASY,EAAIC,EAAOC,EAAa,GAAO,CACvC,GAAM,CAAE,YAAAC,CAAY,EAAIC,EAAiBZ,EAAOQ,EAAIC,EAAOZ,GAAe,aAAa,EACnFc,GAAeD,GACdA,GAAc,CAACC,IAClBX,EAAM,cAAgB,CAAC,EACvBA,EAAM,kBAAoB,IAE3BM,EAAS,GAETF,EAAK,CAEP,EAEA,OAAQ,CACPE,EAAS,CACV,EAEA,WAAWO,EAAU,CACpB,OAAO,OAAOb,EAAM,OAAQa,CAAQ,EAChChB,GAAe,gBAAkB,GACpCS,EAAS,GAETN,EAAM,kBAAoB,GAC1BI,EAAK,EAEP,EAEA,QAAQU,EAAQC,EAAU,CACzBlB,EAAgBiB,EAGhBlB,EAAK,OAAO,aAAa,EACzBI,EAAM,OAAS,CAAC,EAChBA,EAAM,MAAQ,GACdA,EAAM,cAAgB,CAAC,EACvBA,EAAM,gBAAkB,CAAC,EACzBA,EAAM,cAAgB,CAAC,EACvBA,EAAM,OAASC,EAAmBa,EAAQC,EAAUb,CAAiB,EACrE,IAAMc,EAAIjB,EAAiBe,GAAQ,aAAa,EAChDd,EAAM,kBAAoBgB,EAAE,kBAC5BhB,EAAM,eAAiBgB,EAAE,eACrBF,GAAU,OAAO,KAAKd,EAAM,MAAM,EAAE,OAAS,GAAKc,EAAO,gBAAkB,GAC9ER,EAAS,EAETF,EAAK,CAEP,EAEA,OAAOa,EAAQ,CACdC,EAAiBlB,EAAOiB,CAAM,EAC9Bb,EAAK,CACN,EAEA,YAAYe,EAAS,CACpBnB,EAAM,MAAQmB,EACdf,EAAK,CACN,EAEA,UAAUgB,EAAU,CACnB,OAAAjB,EAAU,IAAIiB,CAAQ,EACf,IAAMjB,EAAU,OAAOiB,CAAQ,CACvC,EAEA,OAAQhB,CACT,CACD,CC/IO,SAASiB,EACfC,EACAC,EAAgC,CAAC,EACd,CACnB,GAAM,CAAE,QAAAC,EAAU,IAAoB,SAAAC,CAAS,EAAIF,EAE/CG,EAAY,GACZC,EAA0B,KAC1BC,EAAiD,KAErD,SAASC,EAAWC,EAAe,CAC9BJ,IAAcI,IAClBJ,EAAYI,EACZL,IAAW,EACZ,CAEA,SAASM,GAAe,CACvBH,GAAwB,MAAM,EAC9BA,EAAyB,IAC1B,CAEA,eAAeI,EAAQC,EAAW,CACjCF,EAAa,EAEbH,EAAyB,IAAI,gBAC7B,GAAM,CAAE,OAAAM,CAAO,EAAIN,EACbO,EAAY,WAAW,IAAM,CAGlC,QAAQ,KAAK,iCAAiCX,CAAO,oBAAe,EACpEI,GAAwB,MAAM,CAC/B,EAAGJ,CAAO,EAEVK,EAAW,EAAI,EACf,GAAI,CACH,MAAMP,EAAIW,EAAQC,CAAM,CACzB,OAASE,EAAK,EAIT,EAAEA,aAAe,QAAWA,EAAI,OAAS,cAAgBA,EAAI,OAAS,iBACzE,QAAQ,MAAM,2CAA4CA,CAAG,CAE/D,QAAE,CAKD,GAJA,aAAaD,CAAS,EACtBN,EAAW,EAAK,EAChBD,EAAyB,KAErBD,IAAkB,KAAM,CAC3B,IAAMG,EAAOH,EACbA,EAAgB,KAChBK,EAAQF,CAAI,CACb,CACD,CACD,CAEA,SAASO,EAAQJ,EAAW,CACvBP,GACCC,IAAkB,MAKrB,QAAQ,MAAM,uDAAuD,EAEtEA,EAAgBM,GAEhBD,EAAQC,CAAM,CAEhB,CAEA,SAASK,GAAS,CACjBX,EAAgB,KAChBI,EAAa,CACd,CAEA,MAAO,CACN,QAAAM,EACA,IAAI,WAAY,CACf,OAAOX,CACR,EACA,IAAI,YAAa,CAChB,OAAOC,IAAkB,IAC1B,EACA,OAAAW,CACD,CACD,CCzFO,SAASC,EAAeC,EAAyC,CACvE,OAAOC,EAAUD,CAAM,CACxB,CAEA,SAASC,EAAUC,EAAwB,CAC1C,GAAIA,IAAU,MAAQ,OAAOA,GAAU,SAAU,OAAO,KAAK,UAAUA,CAAK,GAAK,OACjF,GAAI,MAAM,QAAQA,CAAK,EAAG,MAAO,IAAIA,EAAM,IAAID,CAAS,EAAE,KAAK,GAAG,CAAC,IACnE,IAAME,EAAMD,EAEZ,MAAO,IADM,OAAO,KAAKC,CAAG,EAAE,KAAK,EACnB,IAAKC,GAAM,GAAG,KAAK,UAAUA,CAAC,CAAC,IAAIH,EAAUE,EAAIC,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,GAAG,CAAC,GAClF,CAkCO,SAASC,EACfC,EAAmC,CAAC,EACjB,CACnB,GAAM,CAAE,IAAAC,EAAM,GAAI,WAAAC,CAAW,EAAIF,EAC3BG,EAAU,IAAI,IAEpB,SAASC,EAAQC,EAAkC,CAC9CA,EAAO,QAAQ,QAAQH,GAAY,QAAQG,EAAO,MAAM,CAC7D,CAEA,SAASC,EAAKD,EAAgD,CAC7D,MAAI,CAACH,GAAc,CAACG,EAAO,QAAQ,OAAeA,EAC3C,CAAE,GAAGA,EAAQ,OAAQH,EAAW,MAAMG,EAAO,MAAM,CAAE,CAC7D,CAEA,SAASE,EAAMC,EAAmB,CACjC,IAAMC,EAAQN,EAAQ,IAAIK,CAAG,EACzBC,IAAU,SACdN,EAAQ,OAAOK,CAAG,EAClBJ,EAAQK,CAAK,EACd,CAEA,MAAO,CACN,IAAIf,EAAQ,CACX,IAAMc,EAAMf,EAAeC,CAAM,EAC3BgB,EAAMP,EAAQ,IAAIK,CAAG,EAC3B,GAAIE,IAAQ,OAEZ,OAAAP,EAAQ,OAAOK,CAAG,EAClBL,EAAQ,IAAIK,EAAKE,CAAG,EAGpB,QAAQ,KAAK,oDAA+CP,EAAQ,IAAI,IAAIF,CAAG,GAAG,EAG3EK,EAAKI,CAAG,CAChB,EACA,IAAIhB,EAAQW,EAAQ,CACnB,IAAMG,EAAMf,EAAeC,CAAM,EAIjC,IAFAa,EAAMC,CAAG,EACTL,EAAQ,IAAIK,EAAKF,EAAKD,CAAM,CAAC,EACtBF,EAAQ,KAAOF,GAAK,CAC1B,IAAMU,EAASR,EAAQ,KAAK,EAAE,KAAK,EAAE,MACrC,GAAIQ,IAAW,OAAW,MAC1BJ,EAAMI,CAAM,EACZ,QAAQ,KAAK,uCAAuCV,CAAG,GAAG,CAC3D,CACD,EACA,OAAQ,CACHE,EAAQ,KAAO,GAClB,QAAQ,KAAK,wBAAwBA,EAAQ,IAAI,+BAA+B,EAEjFA,EAAQ,QAAQC,CAAO,EACvBD,EAAQ,MAAM,CACf,CACD,CACD,CC1GO,SAASS,EACfC,EACAC,EACAC,EAA+C,CAAC,EAClC,CACd,GAAM,CAAE,WAAAC,EAAY,GAAGC,CAAgB,EAAIF,EAIrCG,EAAOC,EAAuB,CAAE,WAAAH,CAAW,CAAC,EAE5CI,EAAWC,EAA6C,MAAOC,EAAQC,IAAW,CACvF,IAAMC,EAASN,EAAK,IAAII,CAAM,EAC9B,GAAIE,IAAW,OAAW,CACzB,GAAID,EAAO,QAAS,OACpBT,EAAY,EAAE,OAAOU,CAAM,EAC3B,MACD,CACA,GAAI,CACH,IAAMC,EAAS,MAAMZ,EAAQS,EAAQC,CAAM,EAC3C,GAAIA,EAAO,QAAS,CAEnB,QAAQ,MAAM,oEAA+D,EAC7E,MACD,CACAL,EAAK,IAAII,EAAQG,CAAM,EACvBX,EAAY,EAAE,OAAOW,CAAM,CAC5B,OAASC,EAAK,CACb,GAAIH,EAAO,QAAS,CAEnB,QAAQ,MAAM,oEAAoE,EAClF,MACD,CACA,QAAQ,KAAK,+BAAgCG,CAAG,EAChDZ,EAAY,EAAE,YAAYY,aAAe,MAAQA,EAAI,QAAU,OAAOA,CAAG,CAAC,CAC3E,CACD,EAAGT,CAAe,EAElB,MAAO,CACN,MAAMK,EAAQ,CACbF,EAAS,QAAQE,CAAM,CACxB,EACA,QAAS,CACRF,EAAS,OAAO,CACjB,EACA,IAAI,WAAY,CACf,OAAOA,EAAS,SACjB,EACA,YAAa,CACZF,EAAK,MAAM,CACZ,CACD,CACD","names":["getInputItems","STORAGE_PREFIX","makeKey","scopeKey","inputId","writeExternalValue","args","value","readExternalValue","ref","raw","clearExternalValue","getExternalInputs","schema","item","getDefaultValue","buildInitialValues","schema","scopeKey","read","clientSet","getExternalInputs","e","values","input","stored","getDefaultValue","output","makeInitialFlags","instanceSolve","manual","applyValueChange","state","id","value","pickInputValues","picked","applySolveResult","result","createSolveSession","args","currentSchema","flags","makeInitialFlags","state","buildInitialValues","readExternalValue","listeners","emit","l","dispatch","pickInputValues","id","value","forceSolve","shouldSolve","applyValueChange","incoming","schema","scopeKey","f","result","applySolveResult","message","listener","createAsyncThrottle","run","options","timeout","onChange","isRunning","pendingValues","currentAbortController","setRunning","next","abortCurrent","execute","values","signal","timeoutId","err","trigger","cancel","stableInputKey","values","serialize","value","obj","k","createSolveMemo","options","max","meshPolicy","entries","release","result","copy","evict","key","entry","hit","oldest","createRequestResponseDriver","onSolve","getReporter","options","meshPolicy","throttleOptions","memo","createSolveMemo","throttle","createAsyncThrottle","values","signal","cached","result","err"]}
1
+ {"version":3,"sources":["../src/client/external-storage.ts","../src/client/solve-session-core.ts","../src/client/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts","../src/client/compute-fetch-solve-fn.ts"],"sourcesContent":["// Transit storage for inputs with source.kind === 'client': a producer route writes the\n// value here, the solver route reads it back. Keyed by (scopeKey, inputId) so values\n// don't bleed across solver contexts — scopeKey is whatever uniquely identifies one\n// (sessionId in plugin-ui/preview, definition guid in selva/library).\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nfunction makeKey(scopeKey: string, inputId: string): string {\n\treturn `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;\n}\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst { scopeKey, inputId, value } = args;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return undefined;\n\tif (typeof sessionStorage === 'undefined') return undefined;\n\tconst raw = sessionStorage.getItem(makeKey(scopeKey, inputId));\n\tif (raw === null) return undefined;\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function clearExternalValue(ref: ExternalValueRef): void {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.removeItem(makeKey(scopeKey, inputId));\n}\n\nexport interface ExternalInput {\n\tparamId: string;\n\tdisplayName: string;\n}\n\nexport function getExternalInputs(schema: UISchema): ExternalInput[] {\n\treturn getInputItems(schema)\n\t\t.filter((item) => item.source?.kind === 'client')\n\t\t.map((item) => ({\n\t\t\tparamId: item.paramId,\n\t\t\tdisplayName: item.displayName ?? item.paramId\n\t\t}));\n}\n","// Pure transition logic for a Solve Session.\n//\n// This module holds the value/lifecycle state machine with no transport and no state\n// ownership: how values are seeded (including client-sourced hydration), when a value\n// change should dispatch a solve vs. defer it, and what a reported solve result does to\n// the flags. `solve-session.ts` is a thin shell that owns the state object and the\n// subscriber set; every decision lives here, which is what makes it directly testable.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getDefaultValue } from '@selvajs/schemas';\nimport { getExternalInputs, type ExternalValueRef } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\n\nexport interface SolveSessionState {\n\tvalues: Record<string, unknown>;\n\terror: string;\n\tcomputeErrors: string[];\n\tcomputeWarnings: string[];\n\tmeshes: unknown[];\n\t/** Values changed since the last solve, in manual (instanceSolve === false) mode. */\n\tpendingValues: Record<string, unknown>;\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n}\n\nexport type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;\n\n/**\n * Client-sourced inputs are hydrated from `read` and left absent when no stored value\n * exists, so the missing-inputs panel can detect them. Other inputs fall back to their\n * declared default, then the paramType default.\n */\nexport function buildInitialValues(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): Record<string, unknown> {\n\tconst clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));\n\tconst values: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (clientSet.has(input.id)) {\n\t\t\tconst stored = read({ scopeKey, inputId: input.id });\n\t\t\tif (stored !== undefined) values[input.id] = stored;\n\t\t\tcontinue;\n\t\t}\n\t\tvalues[input.id] = input.default ?? getDefaultValue(input.paramType);\n\t}\n\tfor (const output of schema.outputs) {\n\t\tvalues[output.id] = null;\n\t}\n\treturn values;\n}\n\n/** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */\nexport function makeInitialFlags(instanceSolve: boolean | undefined): {\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n} {\n\tconst manual = instanceSolve === false;\n\treturn { hasPendingChanges: manual, hasNeverSolved: manual };\n}\n\n/** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined\n): { state: SolveSessionState; shouldSolve: boolean } {\n\tstate.values[id] = value;\n\tif (instanceSolve === false) {\n\t\tstate.pendingValues[id] = value;\n\t\tstate.hasPendingChanges = true;\n\t\treturn { state, shouldSolve: false };\n\t}\n\treturn { state, shouldSolve: true };\n}\n\n/**\n * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value\n * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB\n * options-list output showed this can re-upload a multi-MB payload no backend reads.\n */\nexport function pickInputValues(\n\tschema: UISchema | undefined,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tif (!schema?.inputs) return values;\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (input.id in values) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\nexport function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.meshes = result.meshes ?? [];\n\tObject.assign(state.values, result.outputs);\n\tstate.pendingValues = {};\n\tstate.hasPendingChanges = false;\n\tstate.hasNeverSolved = false;\n\treturn state;\n}\n","// A framework-free shell over the pure transition logic in solve-session-core.ts: owns the\n// live values/flags, delegates every transition to the core, and drives solves through a\n// transport-agnostic SolveDriver.\n//\n// State is exposed as plain getters; it carries no framework reactivity of its own, so a\n// Svelte host must wrap it — see `useSolveSession.svelte.ts` in `@selvajs/ui`, which\n// republishes these getters as $state. Reading a getter without subscribing gives a\n// correct value but will not re-render.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { readExternalValue } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\nimport type { SolveDriver } from './drivers/driver.js';\nimport {\n\tbuildInitialValues,\n\tmakeInitialFlags,\n\tapplyValueChange,\n\tapplySolveResult,\n\tpickInputValues,\n\ttype SolveSessionState\n} from './solve-session-core.js';\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\treadonly meshes: unknown[];\n\treadonly hasPendingChanges: boolean;\n\treadonly hasNeverSolved: boolean;\n\treadonly isSolving: boolean;\n\t/** `forceSolve` overrides manual mode for system-initiated reconciliation (e.g. a dynamic value list pruning a vanished selection), so the output can't lag behind the new value. */\n\tsetValue(id: string, value: unknown, forceSolve?: boolean): void;\n\tsolve(): void;\n\t/** Merges incoming values, then solves (auto) or marks dirty (manual). */\n\tloadValues(incoming: Record<string, unknown>): void;\n\trebuild(schema: UISchema, scopeKey: string): void;\n\treport(result: SolveResult): void;\n\treportError(message: string): void;\n\t/** Fires after any mutation of the getters above, including driver-reported `isSolving` transitions. */\n\tsubscribe(listener: () => void): () => void;\n\t/** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */\n\tnotify(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\n\tconst flags = makeInitialFlags(currentSchema?.instanceSolve);\n\tconst state: SolveSessionState = {\n\t\tvalues: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tmeshes: [],\n\t\tpendingValues: {},\n\t\thasPendingChanges: flags.hasPendingChanges,\n\t\thasNeverSolved: flags.hasNeverSolved\n\t};\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => listeners.forEach((l) => l());\n\n\tfunction dispatch() {\n\t\t// Copy: state.values is the live map, and a driver may hold the payload across an\n\t\t// async solve — a later setValue must not mutate what's already in flight.\n\t\targs.driver.solve({ ...pickInputValues(currentSchema, state.values) });\n\t\temit();\n\t}\n\n\treturn {\n\t\tget values() {\n\t\t\treturn state.values;\n\t\t},\n\t\tget error() {\n\t\t\treturn state.error;\n\t\t},\n\t\tget computeErrors() {\n\t\t\treturn state.computeErrors;\n\t\t},\n\t\tget computeWarnings() {\n\t\t\treturn state.computeWarnings;\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget hasPendingChanges() {\n\t\t\treturn state.hasPendingChanges;\n\t\t},\n\t\tget hasNeverSolved() {\n\t\t\treturn state.hasNeverSolved;\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn args.driver.isSolving;\n\t\t},\n\n\t\tsetValue(id, value, forceSolve = false) {\n\t\t\tconst { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);\n\t\t\tif (shouldSolve || forceSolve) {\n\t\t\t\tif (forceSolve && !shouldSolve) {\n\t\t\t\t\tstate.pendingValues = {};\n\t\t\t\t\tstate.hasPendingChanges = false;\n\t\t\t\t}\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\tsolve() {\n\t\t\tdispatch();\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tObject.assign(state.values, incoming);\n\t\t\tif (currentSchema?.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\tstate.hasPendingChanges = true;\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\trebuild(schema, scopeKey) {\n\t\t\tcurrentSchema = schema;\n\t\t\t// The new definition has its own input space; a matching input key from the prior\n\t\t\t// one must not serve that definition's cached result.\n\t\t\targs.driver.clearCache?.();\n\t\t\tstate.meshes = [];\n\t\t\tstate.error = '';\n\t\t\tstate.computeErrors = [];\n\t\t\tstate.computeWarnings = [];\n\t\t\tstate.pendingValues = {};\n\t\t\tstate.values = buildInitialValues(schema, scopeKey, readExternalValue);\n\t\t\tconst f = makeInitialFlags(schema?.instanceSolve);\n\t\t\tstate.hasPendingChanges = f.hasPendingChanges;\n\t\t\tstate.hasNeverSolved = f.hasNeverSolved;\n\t\t\tif (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\treport(result) {\n\t\t\tapplySolveResult(state, result);\n\t\t\temit();\n\t\t},\n\n\t\treportError(message) {\n\t\t\tstate.error = message;\n\t\t\temit();\n\t\t},\n\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => listeners.delete(listener);\n\t\t},\n\n\t\tnotify: emit\n\t};\n}\n","/**\n * Single-in-flight, latest-wins throttle for any async operation: only one run at a\n * time, a newly triggered value overwrites the single pending slot (no queue), and a\n * stale run can be aborted via `AbortController` or timeout.\n *\n * Framework-free: `isRunning` is a plain getter, not a store. A reactive host must\n * subscribe via `onChange` (see `@selvajs/ui`'s rune adapter) — polling the getter\n * directly reads correct values but won't trigger a re-render.\n */\ninterface AsyncThrottleOptions {\n\ttimeout?: number;\n\tonChange?: () => void;\n}\n\nexport interface AsyncThrottle<T> {\n\ttrigger: (values: T) => void;\n\treadonly isRunning: boolean;\n\treadonly hasPending: boolean;\n\tcancel: () => void;\n}\n\n/** Fallback per-run abort timeout (ms) when the caller doesn't pass one. */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\nexport function createAsyncThrottle<T>(\n\trun: (values: T, signal: AbortSignal) => Promise<void>,\n\toptions: AsyncThrottleOptions = {}\n): AsyncThrottle<T> {\n\tconst { timeout = DEFAULT_TIMEOUT_MS, onChange } = options;\n\n\tlet isRunning = false;\n\tlet pendingValues: T | null = null;\n\tlet currentAbortController: AbortController | null = null;\n\n\tfunction setRunning(next: boolean) {\n\t\tif (isRunning === next) return;\n\t\tisRunning = next;\n\t\tonChange?.();\n\t}\n\n\tfunction abortCurrent() {\n\t\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\n\t}\n\n\tasync function execute(values: T) {\n\t\tabortCurrent();\n\n\t\tcurrentAbortController = new AbortController();\n\t\tconst { signal } = currentAbortController;\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\t// Cleared in `finally` on every other path, so firing means a genuine\n\t\t\t// timeout — the only signal for it (the abort itself is swallowed below).\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${timeout}ms — aborting`);\n\t\t\tcurrentAbortController?.abort();\n\t\t}, timeout);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, signal);\n\t\t} catch (err) {\n\t\t\t// AbortError is expected (timeout or cancel). Non-abort errors must be\n\t\t\t// handled inside `run` — re-throwing here would produce an unhandled\n\t\t\t// rejection because `execute` is always called fire-and-forget.\n\t\t\tif (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {\n\t\t\t\tconsole.error('[Solve/throttle] unhandled error in run:', err);\n\t\t\t}\n\t\t} finally {\n\t\t\tclearTimeout(timeoutId);\n\t\t\tsetRunning(false);\n\t\t\tcurrentAbortController = null;\n\n\t\t\tif (pendingValues !== null) {\n\t\t\t\tconst next = pendingValues;\n\t\t\t\tpendingValues = null;\n\t\t\t\texecute(next);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction trigger(values: T) {\n\t\tif (isRunning) {\n\t\t\tif (pendingValues !== null) {\n\t\t\t\t// Latest-wins: the previously-queued values are dropped, not run.\n\t\t\t\t// `debug`, not `info`: a slider scrub fires this per frame, and it is a trace of\n\t\t\t\t// normal operation. Promoting it would spam a host's console on every drag.\n\t\t\t\t// eslint-disable-next-line no-console -- see above\n\t\t\t\tconsole.debug('[Solve/throttle] superseded pending run (latest-wins)');\n\t\t\t}\n\t\t\tpendingValues = values;\n\t\t} else {\n\t\t\texecute(values);\n\t\t}\n\t}\n\n\tfunction cancel() {\n\t\tpendingValues = null;\n\t\tabortCurrent();\n\t}\n\n\treturn {\n\t\ttrigger,\n\t\tget isRunning() {\n\t\t\treturn isRunning;\n\t\t},\n\t\tget hasPending() {\n\t\t\treturn pendingValues !== null;\n\t\t},\n\t\tcancel\n\t};\n}\n","// LRU cache of solve results keyed on a stable serialization of the inputs, so re-solving\n// something already seen this session (e.g. dragging a slider back) returns instantly\n// instead of round-tripping.\n//\n// A viewer typically takes ownership of every mesh it renders, disposing the previous\n// content on the next update. So the memo can't hand out its own stored instances (they'd\n// get disposed under it) or drop entries silently (their GPU buffers would leak): it keeps\n// private copies via an injected clone/release policy, which also keeps `three` out of this\n// package — `TMesh` stays opaque here.\n\nimport type { SolveResult } from '../shared/solve-fn.js';\n\n/**\n * Deterministic string key for a set of input values. Sorts object keys at every level so\n * two logically-equal inputs built in different key order collide.\n */\nexport function stableInputKey(values: Record<string, unknown>): string {\n\treturn serialize(values);\n}\n\nfunction serialize(value: unknown): string {\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(serialize).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(',')}}`;\n}\n\n/**\n * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then\n * stores and serves them by reference, which is safe only because nothing frees them behind\n * its back.\n */\nexport interface MeshPolicy<TMesh> {\n\t/** Called on the way in and on every hit, so the stored entry and a served copy are never the same instance. */\n\tclone(meshes: TMesh[]): TMesh[];\n\t/** Must not free anything shared with live content (e.g. materials another object still uses). */\n\trelease(meshes: TMesh[]): void;\n}\n\nexport interface SolveMemoOptions<TMesh> {\n\t/** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */\n\tmax?: number;\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\nexport interface SolveMemo<TMesh = unknown> {\n\tget(values: Record<string, unknown>): SolveResult<TMesh> | undefined;\n\tset(values: Record<string, unknown>, result: SolveResult<TMesh>): void;\n\tclear(): void;\n}\n\nexport function createSolveMemo<TMesh = unknown>(\n\toptions: SolveMemoOptions<TMesh> = {}\n): SolveMemo<TMesh> {\n\tconst { max = 16, meshPolicy } = options;\n\tconst entries = new Map<string, SolveResult<TMesh>>();\n\n\tfunction release(result: SolveResult<TMesh>): void {\n\t\tif (result.meshes?.length) meshPolicy?.release(result.meshes);\n\t}\n\n\tfunction copy(result: SolveResult<TMesh>): SolveResult<TMesh> {\n\t\tif (!meshPolicy || !result.meshes?.length) return result;\n\t\treturn { ...result, meshes: meshPolicy.clone(result.meshes) };\n\t}\n\n\tfunction evict(key: string): void {\n\t\tconst entry = entries.get(key);\n\t\tif (entry === undefined) return;\n\t\tentries.delete(key);\n\t\trelease(entry);\n\t}\n\n\treturn {\n\t\tget(values) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tconst hit = entries.get(key);\n\t\t\tif (hit === undefined) return undefined;\n\t\t\tentries.delete(key);\n\t\t\tentries.set(key, hit);\n\t\t\tconsole.info(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);\n\t\t\treturn copy(hit);\n\t\t},\n\t\tset(values, result) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tevict(key);\n\t\t\tentries.set(key, copy(result));\n\t\t\twhile (entries.size > max) {\n\t\t\t\tconst oldest = entries.keys().next().value;\n\t\t\t\tif (oldest === undefined) break;\n\t\t\t\tevict(oldest);\n\t\t\t\tconsole.info(`[Solve/memo] evicted LRU entry (cap ${max})`);\n\t\t\t}\n\t\t},\n\t\tclear() {\n\t\t\tif (entries.size > 0) {\n\t\t\t\tconsole.info(`[Solve/memo] cleared ${entries.size} entries (definition changed)`);\n\t\t\t}\n\t\t\tentries.forEach(release);\n\t\t\tentries.clear();\n\t\t}\n\t};\n}\n","import { createAsyncThrottle } from '../async-throttle.js';\nimport { createSolveMemo, type MeshPolicy } from '../solve-memo.js';\nimport type { SolveFn } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\ttimeout?: number;\n\tonChange?: () => void;\n\t/** Mesh ownership policy for the result memo — see `MeshPolicy` (solve-memo.ts). */\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\n/**\n * `getReporter` is lazy (`() => session`) because the session and driver reference each\n * other — the host builds the driver first, then the session, then can hand this back.\n */\nexport function createRequestResponseDriver<TMesh = unknown>(\n\tonSolve: SolveFn<TMesh>,\n\tgetReporter: () => SolveReporter<TMesh>,\n\toptions: RequestResponseDriverOptions<TMesh> = {}\n): SolveDriver {\n\tconst { meshPolicy, ...throttleOptions } = options;\n\n\t// Checked inside the throttled run, not before triggering it, so a hit only serves\n\t// after the throttle's latest-wins ordering has picked these values to run.\n\tconst memo = createSolveMemo<TMesh>({ meshPolicy });\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(async (values, signal) => {\n\t\tconst cached = memo.get(values);\n\t\tif (cached !== undefined) {\n\t\t\tif (signal.aborted) return;\n\t\t\tgetReporter().report(cached);\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst result = await onSolve(values, signal);\n\t\t\tif (signal.aborted) {\n\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\tconsole.debug('[Solve/driver] solve completed after abort — result discarded');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Stamped here rather than in the SolveFn: a memo hit never calls it, so values\n\t\t\t// captured inside would pair the served result with whatever solved last.\n\t\t\tconst stamped = { ...result, values };\n\t\t\tmemo.set(values, stamped);\n\t\t\tgetReporter().report(stamped);\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\tconsole.debug('[Solve/driver] solve aborted (superseded, cancelled, or timed out)');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\tgetReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t}\n\t}, throttleOptions);\n\n\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel() {\n\t\t\tthrottle.cancel();\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tclearCache() {\n\t\t\tmemo.clear();\n\t\t}\n\t};\n}\n","/**\n * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a\n * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +\n * `SolveEngine` produce server-side) and unpacks the result.\n *\n * Ports the resilience behavior an interactive solve endpoint needs regardless\n * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response\n * guarding, and abort handling at every await point — so a new consumer gets it\n * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:\n * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a\n * viewer-enabled consumer supplies its own `meshes.extract` hook (typically\n * `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).\n */\n\nimport { GrasshopperResponseProcessor, type GrasshopperComputeResponse } from '@selvajs/compute';\nimport type { SolveFn, SolveResult } from '../shared/solve-fn.js';\n\nexport interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {\n\t/** The compute endpoint to POST to, e.g. `/api/compute`. */\n\tendpoint: string;\n\t/** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */\n\tdefinitionUrl: () => string;\n\t/** Schema input params, sent as the request body's `inputs`. */\n\tinputs: () => Array<{ id: string }>;\n\t/** Schema output params — drives byId/byName output extraction after a solve. */\n\toutputs: () => Array<{ id: string; nickname?: string }>;\n\tchannel?: () => 'live' | 'draft' | undefined;\n\tversionId?: () => string | null | undefined;\n\t/**\n\t * Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`\n\t * and no rhino3dm module is ever loaded. `loadRhino` is called lazily on the\n\t * first solve that needs meshes and cached for the lifetime of this `SolveFn`.\n\t */\n\tmeshes?: {\n\t\tloadRhino: () => Promise<TRhino>;\n\t\textract: (\n\t\t\tresponse: GrasshopperComputeResponse,\n\t\t\topts: { debug: boolean; rhino: TRhino }\n\t\t) => TMesh[] | Promise<TMesh[]>;\n\t};\n\t/** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */\n\tdebug?: boolean;\n\t/** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */\n\tonRateLimited?: (retryAfterSeconds: number) => void;\n\t/** Called on a 401 or a login-page redirect. Default: throws an actionable \"reload to sign in again\" Error. */\n\tonSessionExpired?: () => void;\n}\n\nfunction parseServerTiming(header: string | null): Record<string, number> {\n\tconst out: Record<string, number> = {};\n\tif (!header) return out;\n\tfor (const part of header.split(',')) {\n\t\tconst [name, ...params] = part.split(';').map((s) => s.trim());\n\t\tconst dur = params.find((p) => p.startsWith('dur='));\n\t\tif (!name || !dur) continue;\n\t\tconst ms = Number(dur.slice(4));\n\t\tif (Number.isFinite(ms)) out[name] = ms;\n\t}\n\treturn out;\n}\n\nconst defaultOnRateLimited = (retryAfterSeconds: number): never => {\n\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);\n};\n\nconst defaultOnSessionExpired = (): never => {\n\tthrow new Error(\n\t\t'Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.'\n\t);\n};\n\nexport function createComputeFetchSolveFn<TMesh = unknown, TRhino = unknown>(\n\topts: ComputeFetchSolveFnOptions<TMesh, TRhino>\n): SolveFn<TMesh, GrasshopperComputeResponse> {\n\tconst debug = opts.debug ?? false;\n\tconst log = (...args: unknown[]) => {\n\t\tif (debug) console.info(...args);\n\t};\n\n\tlet rhinoPromise: Promise<TRhino> | null = null;\n\tfunction getRhino(): Promise<TRhino> {\n\t\tif (!opts.meshes) throw new Error('createComputeFetchSolveFn: meshes option not configured.');\n\t\tif (!rhinoPromise) rhinoPromise = opts.meshes.loadRhino();\n\t\treturn rhinoPromise;\n\t}\n\n\tlet cooldownUntil = 0;\n\n\treturn async (values, signal): Promise<SolveResult<TMesh, GrasshopperComputeResponse>> => {\n\t\tconst remainingMs = cooldownUntil - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(remainingMs / 1000)}s.`);\n\t\t}\n\n\t\tconst solveStart = performance.now();\n\t\tconst inputs = opts.inputs();\n\t\tconst outputs = opts.outputs();\n\t\tconst channel = opts.channel?.();\n\t\tconst versionId = opts.versionId?.();\n\n\t\tconst payload = JSON.stringify({\n\t\t\tinputs,\n\t\t\tvalues,\n\t\t\tdefinitionUrl: opts.definitionUrl(),\n\t\t\t...(versionId ? { versionId } : channel === 'draft' ? { channel: 'draft' } : {})\n\t\t});\n\t\tconst valuesBytes = JSON.stringify(values).length;\n\n\t\tlet res: Response;\n\t\ttry {\n\t\t\tres = await fetch(opts.endpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tbody: payload,\n\t\t\t\tsignal\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\tlog('[Compute] solve aborted during fetch — empty result discarded');\n\t\t\t\treturn { outputs: {} };\n\t\t\t}\n\t\t\tthrow new Error(\n\t\t\t\t'Request blocked — your session may have expired. Reload the page to sign in again.',\n\t\t\t\t{ cause: err }\n\t\t\t);\n\t\t}\n\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted after headers — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\n\t\tconst redirectedToLogin = res.redirected && new URL(res.url).pathname === '/login';\n\t\tif (res.status === 401 || redirectedToLogin) {\n\t\t\t(opts.onSessionExpired ?? defaultOnSessionExpired)();\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tif (res.status === 503) {\n\t\t\t\tthrow new Error('Compute server is offline or unreachable. Please try again later.');\n\t\t\t}\n\t\t\tconst errorBody = await res.text().catch(() => '');\n\t\t\tlet d: { message?: string; retryAfter?: number } = {};\n\t\t\ttry {\n\t\t\t\td = JSON.parse(errorBody);\n\t\t\t} catch {\n\t\t\t\tif (errorBody) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[Compute] non-JSON error body (HTTP ${res.status}):`,\n\t\t\t\t\t\terrorBody.slice(0, 300)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (res.status === 429) {\n\t\t\t\tconst retryAfter = Number(res.headers.get('Retry-After')) || Number(d.retryAfter) || 5;\n\t\t\t\tcooldownUntil = Date.now() + retryAfter * 1000;\n\t\t\t\t(opts.onRateLimited ?? defaultOnRateLimited)(retryAfter);\n\t\t\t}\n\t\t\tthrow new Error(d.message || `Compute error (HTTP ${res.status})`);\n\t\t}\n\n\t\tconst ttfbMs = performance.now() - solveStart;\n\n\t\tconst downloadStart = performance.now();\n\t\tconst bodyText = await res.text();\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted mid-download — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\t\tconst downloadMs = performance.now() - downloadStart;\n\t\tconst bytes = bodyText.length;\n\n\t\tlet solved: GrasshopperComputeResponse;\n\t\ttry {\n\t\t\tsolved = JSON.parse(bodyText);\n\t\t} catch (err) {\n\t\t\tthrow new Error(\n\t\t\t\t'Received an invalid response from the server. Reload the page and try again.',\n\t\t\t\t{\n\t\t\t\t\tcause: err\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\tconst processor = new GrasshopperResponseProcessor(solved, false);\n\n\t\tconst meshes = opts.meshes\n\t\t\t? await opts.meshes.extract(processor.response, { debug, rhino: await getRhino() })\n\t\t\t: [];\n\n\t\tconst resultOutputs: Record<string, unknown> = {};\n\t\tfor (const o of outputs) {\n\t\t\tconst byId = processor.getValue({ byId: o.id }, { parseValues: true });\n\t\t\tresultOutputs[o.id] =\n\t\t\t\tbyId ??\n\t\t\t\t(o.nickname\n\t\t\t\t\t? processor.getValue({ byName: o.nickname }, { parseValues: true })\n\t\t\t\t\t: undefined);\n\t\t}\n\n\t\tif (debug) {\n\t\t\tconst serverTiming = parseServerTiming(res.headers.get('Server-Timing'));\n\t\t\tconst serverTotal = serverTiming.total ?? 0;\n\t\t\tconst networkMs = Math.max(0, ttfbMs - serverTotal);\n\t\t\tconst sizeMB = bytes / (1024 * 1024);\n\t\t\tconst size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bytes / 1024).toFixed(0)} KB`;\n\t\t\tconst mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1000)).toFixed(1) : '∞';\n\t\t\tconst heapMB = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory\n\t\t\t\t?.usedJSHeapSize;\n\t\t\tconst heap = heapMB !== undefined ? ` | heap=${(heapMB / (1024 * 1024)).toFixed(0)} MB` : '';\n\t\t\tlog(\n\t\t\t\t`[Compute] req=${(payload.length / 1024).toFixed(0)}KB (values ${(valuesBytes / 1024).toFixed(0)}KB) | ` +\n\t\t\t\t\t`ttfb=${ttfbMs.toFixed(0)}ms (network≈${networkMs.toFixed(0)} + server ${serverTotal.toFixed(0)}) | ` +\n\t\t\t\t\t`download=${downloadMs.toFixed(0)}ms (${size} @ ${mbps} MB/s)${heap}`\n\t\t\t);\n\t\t\tif (serverTiming.selva_cache !== undefined) {\n\t\t\t\tconst cacheMsg = serverTiming.selva_cache\n\t\t\t\t\t? 'HIT — served without calling compute'\n\t\t\t\t\t: 'miss — solved on Rhino.Compute';\n\t\t\t\tlog(`[Compute] └─ cache: ${cacheMsg}`);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\toutputs: resultOutputs,\n\t\t\tmeshes,\n\t\t\terrors: solved.errors ?? [],\n\t\t\twarnings: solved.warnings ?? [],\n\t\t\tsource: solved\n\t\t};\n\t};\n}\n"],"mappings":"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","names":["getInputItems","STORAGE_PREFIX","makeKey","scopeKey","inputId","writeExternalValue","args","value","readExternalValue","ref","raw","clearExternalValue","getExternalInputs","schema","item","getDefaultValue","buildInitialValues","schema","scopeKey","read","clientSet","getExternalInputs","e","values","input","stored","getDefaultValue","output","makeInitialFlags","instanceSolve","manual","applyValueChange","state","id","value","pickInputValues","picked","applySolveResult","result","createSolveSession","args","currentSchema","flags","makeInitialFlags","state","buildInitialValues","readExternalValue","listeners","emit","l","dispatch","pickInputValues","id","value","forceSolve","shouldSolve","applyValueChange","incoming","schema","scopeKey","f","result","applySolveResult","message","listener","createAsyncThrottle","run","options","timeout","onChange","isRunning","pendingValues","currentAbortController","setRunning","next","abortCurrent","execute","values","signal","timeoutId","err","trigger","cancel","stableInputKey","values","serialize","value","obj","k","createSolveMemo","options","max","meshPolicy","entries","release","result","copy","evict","key","entry","hit","oldest","createRequestResponseDriver","onSolve","getReporter","options","meshPolicy","throttleOptions","memo","createSolveMemo","throttle","createAsyncThrottle","values","signal","cached","result","stamped","err","GrasshopperResponseProcessor","parseServerTiming","header","out","part","name","params","s","dur","p","ms","defaultOnRateLimited","retryAfterSeconds","defaultOnSessionExpired","createComputeFetchSolveFn","opts","debug","log","args","rhinoPromise","getRhino","cooldownUntil","values","signal","remainingMs","solveStart","inputs","outputs","channel","versionId","payload","valuesBytes","res","err","redirectedToLogin","errorBody","d","retryAfter","ttfbMs","downloadStart","bodyText","downloadMs","bytes","solved","processor","meshes","resultOutputs","o","byId","serverTiming","serverTotal","networkMs","sizeMB","size","mbps","heapMB","heap","cacheMsg"]}
package/dist/server.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _compute = require('@selvajs/compute');var _zlib = require('zlib');var _util = require('util');function T(e,r){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:_nullishCoalesce(e.minimum, () => (null)),maximum:_nullishCoalesce(e.maximum, () => (null)),atLeast:1,atMost:1,stepSize:e.stepSize,default:_nullishCoalesce(r, () => (e.default))},s=_compute.processInput.call(void 0, t),o=_nullishCoalesce(r, () => (e.default));return s.default==null&&o!=null&&(Array.isArray(o)||s.paramType==="ValueList"&&typeof o!="object")&&(s.default=o),s}var $=_util.promisify.call(void 0, _zlib.gzip),P= exports.COMPUTE_CONTRACT_VERSION =1,E= exports.COMPUTE_VERSION_HEADER ="X-Selva-Compute-Version";function B(e,r){return _compute.TreeBuilder.fromInputParams(e.filter(t=>t.paramType).map(t=>T(t,r[t.id])))}async function D(e){let{client:r,signal:t}=e,s=performance.now(),o=_nullishCoalesce(e.inputTree, () => (B(e.inputs,e.values))),u=performance.now()-s,i=r.solveMeta.seq,l=r.rhinoTiming.seq,n,a=performance.now();try{n=await r.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let b=performance.now()-a;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:b}:{kind:"timeout",durationMs:b,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof _compute.RhinoComputeError){if(v.code===_compute.ErrorCodes.QUEUE_FULL)return{kind:"shed",durationMs:b,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===_compute.ErrorCodes.QUEUE_TIMEOUT)return{kind:"shed",durationMs:b,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:b,error:v}}let c=performance.now()-a,h=performance.now(),d;try{d=JSON.stringify(n)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,y=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,f=null,g=0;if(y){let v=performance.now();f=await $(Buffer.from(d)),g=performance.now()-v}let S=performance.now()-e.loadStartMs,M=r.solveMeta.seq===i+1?r.solveMeta.last:null,k=M&&!M.fromCache&&r.rhinoTiming.seq===l+1?r.rhinoTiming.last:null,C={"Content-Type":"application/json","Server-Timing":U({defLoadMs:e.defLoadMs,treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,rhino:k,settle:M,byteRefOutcome:e.byteRefOutcome,prepMarks:_nullishCoalesce(e.prepMarks, () => ([]))}),[E]:String(P),Vary:"Accept-Encoding"},w={treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,serializedBytes:d.length,compressedBytes:_nullishCoalesce(_optionalChain([f, 'optionalAccess', _2 => _2.byteLength]), () => (null))},x;return f?(C["Content-Encoding"]="gzip",C["Content-Length"]=String(f.byteLength),x={body:new Uint8Array(f),encoding:"gzip",headers:C,result:n,metrics:w}):(C["Content-Length"]=String(Buffer.byteLength(d)),x={body:d,headers:C,result:n,metrics:w}),{kind:"ok",envelope:x,solveMs:c,errorCount:_nullishCoalesce(_optionalChain([n, 'access', _3 => _3.errors, 'optionalAccess', _4 => _4.length]), () => (0)),warningCount:_nullishCoalesce(_optionalChain([n, 'access', _5 => _5.warnings, 'optionalAccess', _6 => _6.length]), () => (0))}}function F(e,r){let t=/\bgzip\b/i.test(r);if(e.encoding==="gzip"&&!t){let s=_zlib.gunzipSync.call(void 0, e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(s)),{body:s,headers:o}}return{body:e.body,headers:e.headers}}function U(e){let r=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let u=t.decode+t.solve+t.encode,i=Math.max(0,e.solveMs-u);r+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[u,i]of e.prepMarks)r+=`, p_${u};dur=${i.toFixed(1)}`;let s=e.settle;s&&(r+=`, selva_cache;dur=${s.fromCache?1:0}`,s.definitionReuploaded!==void 0&&(r+=`, def_reupload;dur=${s.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let u=o.loaded?o.fromCache?"hit":"miss":"skipped";r+=`, def_bytes;desc=${u}`}return r}function V(e){return e.id}var N=16;function j(e){let r=_nullishCoalesce(e.maxCachedClients, () => (N)),t=new Map,s=new Map;e.debugVerbose&&_compute.enableDebugLogging.call(void 0, );let o=i=>{e.debug&&_optionalChain([e, 'access', _7 => _7.onDebugLog, 'optionalCall', _8 => _8(i)])};async function u(i,l){let n={last:null,seq:0},a={last:null,seq:0},c={serverUrl:i.serverUrl,apiKey:i.apiKey,debug:e.debugVerbose,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:p=>{let m=_nullishCoalesce(p.decode, () => (0)),y=_nullishCoalesce(p.solve, () => (0)),f=_nullishCoalesce(p.encode, () => (0));if(n.seq+=1,n.last={decode:m,solve:y,encode:f},!e.debug)return;let g=m<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",S=e.cachesolve&&y<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${m}ms solve=${y}ms encode=${f}ms | definition-cache: ${g} | solve-cache (cachesolve): ${S}`)}};l&&(c.headers={"X-Selva-Definition":l});let h=await _compute.GrasshopperClient.create(c),d=h.createScheduler({mode:"queue",maxConcurrent:e.maxConcurrentSolves,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(p,m)=>{if(a.seq+=1,m.status==="success"&&(a.last={fromCache:m.fromCache===!0,definitionReuploaded:m.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${m.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(m.durationMs)}ms)`),m.fromCache||o(m.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":m.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!m.fromCache))){let y=_optionalChain([m, 'access', _9 => _9.response, 'optionalAccess', _10 => _10.errors]),f=Array.isArray(y)?y.length:0;if(f>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${f} GH error(s); ${g}. First: ${JSON.stringify(y[0])}`)}}}});return{client:h,scheduler:d,rhinoTiming:n,solveMeta:a}}return{async getClient(i,l){let n=i.id,a=t.get(n);if(a)return t.delete(n),t.set(n,a),a;let c=s.get(n);if(c)return c;o(`[Compute/client-cache] miss \u2014 building warm client for server ${n}`);let h=(async()=>{let d=await u(i,_optionalChain([l, 'optionalAccess', _11 => _11.definitionGuid]));if(t.size>=r){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${r})`),_optionalChain([t, 'access', _12 => _12.get, 'call', _13 => _13(p), 'optionalAccess', _14 => _14.scheduler, 'access', _15 => _15.dispose, 'call', _16 => _16()]),t.delete(p))}return t.set(n,d),d})().finally(()=>s.delete(n));return s.set(n,h),h},evict(i){let l=i,n=t.get(l);n&&(o(`[Compute/client-cache] evicted warm client for server ${l} (config change)`),n.scheduler.dispose(),t.delete(l))},solveCacheStats(){let i={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let l of t.values()){let n=l.scheduler.cacheStats();i.entries+=n.entries,i.bytes+=n.bytes,i.hits+=n.hits,i.misses+=n.misses,i.evictions+=n.evictions}return i},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.dispose();t.clear()}}}function H(e){let r=Math.max(0,Math.floor(e)),t=new Map,s=0,o={hits:0,misses:0,evictions:0};function u(n,a){t.delete(n),t.set(n,a)}function i(){for(;s>r&&t.size>0;){let n=t.keys().next().value,a=t.get(n);t.delete(n),s-=a.bytes.byteLength,o.evictions+=1}}function l(n,a){if(r===0||a.byteLength>r)return;let c=t.get(n);c&&(s-=c.bytes.byteLength),t.set(n,{bytes:a}),s+=a.byteLength,i()}return{getOrLoad(n,a){let c={loaded:!1,fromCache:!1};return{key:n,outcome:c,load:async()=>{let h=t.get(n);if(h)return u(n,h),o.hits+=1,c.loaded=!0,c.fromCache=!0,h.bytes;o.misses+=1,c.loaded=!0,c.fromCache=!1;let d=await a();return l(n,d),d}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:s}},clear(){t.clear(),s=0}}}function W(e={}){let r=new Map;return{run(t,s,o){let u=r.get(t);if(u)return _optionalChain([e, 'access', _17 => _17.onJoin, 'optionalCall', _18 => _18(t)]),_optionalChain([u, 'access', _19 => _19.notifyOwner, 'optionalCall', _20 => _20()]),u.promise;let i={notifyOwner:o,promise:void 0},l=(async()=>s())().finally(()=>{i.notifyOwner=void 0,r.get(t)===i&&r.delete(t)});return i.promise=l,r.set(t,i),l},inFlight(){return r.size}}}exports.COMPUTE_CONTRACT_VERSION = P; exports.COMPUTE_VERSION_HEADER = E; exports.adaptEnvelopeToEncoding = F; exports.buildSolveInputTree = B; exports.createClientCache = j; exports.createDefinitionByteCache = H; exports.createSolveCacheSingleFlight = W; exports.runSolvePipeline = D; exports.serverIdentity = V; exports.transformInputParameter = T;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _compute = require('@selvajs/compute');var _zlib = require('zlib');var _util = require('util');function A(e,n){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:_nullishCoalesce(e.minimum, () => (null)),maximum:_nullishCoalesce(e.maximum, () => (null)),atLeast:1,atMost:1,stepSize:e.stepSize,default:_nullishCoalesce(n, () => (e.default))},r=_compute.processInput.call(void 0, t),o=_nullishCoalesce(n, () => (e.default));return r.default==null&&o!=null&&(Array.isArray(o)||r.paramType==="ValueList"&&typeof o!="object")&&(r.default=o),r}var j=_util.promisify.call(void 0, _zlib.gzip),F= exports.COMPUTE_CONTRACT_VERSION =1,_= exports.COMPUTE_VERSION_HEADER ="X-Selva-Compute-Version";function E(e,n){return _compute.TreeBuilder.fromInputParams(e.filter(t=>t.paramType).map(t=>A(t,n[t.id])))}async function B(e){let{client:n,signal:t}=e,r=performance.now(),o=_nullishCoalesce(e.inputTree, () => (E(e.inputs,e.values))),c=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,l=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let x=performance.now()-l;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:x}:{kind:"timeout",durationMs:x,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof _compute.RhinoComputeError){if(v.code===_compute.ErrorCodes.QUEUE_FULL)return{kind:"shed",durationMs:x,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===_compute.ErrorCodes.QUEUE_TIMEOUT)return{kind:"shed",durationMs:x,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:x,error:v}}let d=performance.now()-l,h=performance.now(),a;try{a=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(a.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,M=/\bgzip\b/i.test(e.acceptEncoding)&&a.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(a)),S=performance.now()-v}let y=performance.now()-e.loadStartMs,u=n.solveMeta.seq===s+1?n.solveMeta.last:null,f=u&&!u.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,g={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,rhino:f,settle:u,byteRefOutcome:e.byteRefOutcome,prepMarks:_nullishCoalesce(e.prepMarks, () => ([]))}),[_]:String(F),Vary:"Accept-Encoding"},R={treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,serializedBytes:a.length,compressedBytes:_nullishCoalesce(_optionalChain([C, 'optionalAccess', _2 => _2.byteLength]), () => (null))},D;return C?(g["Content-Encoding"]="gzip",g["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:g,result:i,metrics:R}):(g["Content-Length"]=String(Buffer.byteLength(a)),D={body:a,headers:g,result:i,metrics:R}),{kind:"ok",envelope:D,solveMs:d,errorCount:_nullishCoalesce(_optionalChain([i, 'access', _3 => _3.errors, 'optionalAccess', _4 => _4.length]), () => (0)),warningCount:_nullishCoalesce(_optionalChain([i, 'access', _5 => _5.warnings, 'optionalAccess', _6 => _6.length]), () => (0))}}function T(e,n){let t=/\bgzip\b/i.test(n);if(e.encoding==="gzip"&&!t){let r=_zlib.gunzipSync.call(void 0, e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(r)),{body:r,headers:o}}return{body:e.body,headers:e.headers}}function V(e){let n=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let c=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-c);n+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${s.toFixed(1)}`}for(let[c,s]of e.prepMarks)n+=`, p_${c};dur=${s.toFixed(1)}`;let r=e.settle;r&&(n+=`, selva_cache;dur=${r.fromCache?1:0}`,r.definitionReuploaded!==void 0&&(n+=`, def_reupload;dur=${r.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let c=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${c}`}return n}function H(e){return e.id}var J=16,z=1,X=300*1e3;function k(e){let n=_nullishCoalesce(e.maxWarmComputeServers, () => (J)),t=new Map,r=new Map;e.debug==="verbose"&&_compute.enableDebugLogging.call(void 0, );let o=s=>{e.debug&&_optionalChain([e, 'access', _7 => _7.onDebugLog, 'optionalCall', _8 => _8(s)])};async function c(s,m){let i={last:null,seq:0},l={last:null,seq:0},d=e.debug==="verbose",h={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:d,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:y=>{let u=_nullishCoalesce(y.decode, () => (0)),f=_nullishCoalesce(y.solve, () => (0)),b=_nullishCoalesce(y.encode, () => (0));if(i.seq+=1,i.last={decode:u,solve:f,encode:b},!e.debug)return;let g=u<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",R=e.cachesolve&&f<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${u}ms solve=${f}ms encode=${b}ms | definition-cache: ${g} | solve-cache (cachesolve): ${R}`)}};m&&(h.headers={"X-Selva-Definition":m});let a=await _compute.GrasshopperClient.create(h),p=z,w=0,M=!1;async function C(y){if(!M){M=!0;try{let u=await Promise.resolve(_optionalChain([a, 'access', _9 => _9.serverStats, 'optionalAccess', _10 => _10.getActiveChildren, 'call', _11 => _11({initialize:!1})])).catch(()=>null);w=Date.now();let f=!u||u<=0?z:u;if(f===p)return;o(`[Compute/client-cache] concurrency ${p} \u2192 ${f} (server child count)`),p=f,_optionalChain([y, 'optionalAccess', _12 => _12.setMaxConcurrent, 'call', _13 => _13(f)])}finally{M=!1}}}await C();let S=a.createScheduler({mode:"queue",maxConcurrent:p,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(y,u)=>{if(l.seq+=1,u.status==="success"&&(u.fromCache!==!0&&Date.now()-w>=X&&C(S),l.last={fromCache:u.fromCache===!0,definitionReuploaded:u.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${u.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(u.durationMs)}ms)`),u.fromCache||o(u.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":u.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!u.fromCache))){let f=_optionalChain([u, 'access', _14 => _14.response, 'optionalAccess', _15 => _15.errors]),b=Array.isArray(f)?f.length:0;if(b>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${g}. First: ${JSON.stringify(f[0])}`)}}}});return{client:a,scheduler:S,rhinoTiming:i,solveMeta:l}}return{async getClient(s,m){let i=s.id,l=t.get(i);if(l)return t.delete(i),t.set(i,l),l;let d=r.get(i);if(d)return d;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let h=(async()=>{let a=await c(s,_optionalChain([m, 'optionalAccess', _16 => _16.definitionGuid]));if(t.size>=n){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${n})`),_optionalChain([t, 'access', _17 => _17.get, 'call', _18 => _18(p), 'optionalAccess', _19 => _19.scheduler, 'access', _20 => _20.dispose, 'call', _21 => _21()]),t.delete(p))}return t.set(i,a),a})().finally(()=>r.delete(i));return r.set(i,h),h},evict(s){let m=s,i=t.get(m);i&&(o(`[Compute/client-cache] evicted warm client for server ${m} (config change)`),i.scheduler.dispose(),t.delete(m))},solveCacheStats(){let s={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let m of t.values()){let i=m.scheduler.cacheStats();s.entries+=i.entries,s.bytes+=i.bytes,s.hits+=i.hits,s.misses+=i.misses,s.evictions+=i.evictions}return s},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.dispose();t.clear()}}}function P(e){let n=Math.max(0,Math.floor(e)),t=new Map,r=0,o={hits:0,misses:0,evictions:0};function c(i,l){t.delete(i),t.set(i,l)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,l=t.get(i);t.delete(i),r-=l.bytes.byteLength,o.evictions+=1}}function m(i,l){if(n===0||l.byteLength>n)return;let d=t.get(i);d&&(r-=d.bytes.byteLength),t.set(i,{bytes:l}),r+=l.byteLength,s()}return{getOrLoad(i,l){let d={loaded:!1,fromCache:!1};return{key:i,outcome:d,load:async()=>{let h=t.get(i);if(h)return c(i,h),o.hits+=1,d.loaded=!0,d.fromCache=!0,h.bytes;o.misses+=1,d.loaded=!0,d.fromCache=!1;let a=await l();return m(i,a),a}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:r}},clear(){t.clear(),r=0}}}function O(e={}){let n=new Map;return{run(t,r,o){let c=n.get(t);if(c)return _optionalChain([e, 'access', _22 => _22.onJoin, 'optionalCall', _23 => _23(t)]),_optionalChain([c, 'access', _24 => _24.notifyOwner, 'optionalCall', _25 => _25()]),c.promise;let s={notifyOwner:o,promise:void 0},m=(async()=>r())().finally(()=>{s.notifyOwner=void 0,n.get(t)===s&&n.delete(t)});return s.promise=m,n.set(t,s),m},inFlight(){return n.size}}}var _platform = require('@selvajs/platform');function te(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&"outcome"in e}function ne(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&!("outcome"in e)&&!("key"in e)&&"versionId"in e}var L=class{constructor(n){this.limits=n.limits;let t=_nullishCoalesce(n.logger, () => (new _platform.NoopLogger));this.clientCache=k({maxSolveDurationMs:this.limits.maxSolveDurationMs,cachesolve:this.limits.computeServerCachesolve,cacheerroredsolves:this.limits.computeCacheErroredSolves,reuseServerDefinitionCache:this.limits.computeReuseDefinitionCache,maxQueueDepth:this.limits.computeMaxQueueDepth,queueWaitMs:this.limits.computeQueueWaitMs,responseCacheMaxBytes:this.limits.computeSolveCacheBytes,debug:_nullishCoalesce(n.debug, () => (!1)),maxWarmComputeServers:n.maxWarmComputeServers,onDebugLog:_nullishCoalesce(n.onDebugLog, () => ((r=>t.debug(r,{component:"Compute/client-cache"}))))}),this.byteCache=P(this.limits.computeDefinitionCacheBytes),this.singleFlight=O({onJoin:n.onSolveCoalesced})}getClient(n,t){return this.clientCache.getClient(n,t)}definitionRef(n,t){return this.byteCache.getOrLoad(n,t)}evictServer(n){this.clientCache.evict(n)}clearSolveCaches(){this.clientCache.clearSolveCaches()}stats(){return{client:this.clientCache.solveCacheStats(),definitionBytes:this.byteCache.stats(),coalescing:{inFlight:this.singleFlight.inFlight()}}}async solve(n){let{definitionSource:t,byteRefOutcome:r,identity:o}=this.resolveDefinitionSource(n),c=await this.clientCache.getClient(n.server,{definitionGuid:n.definitionGuid}),s=E(n.inputs,n.values),m=`${o}:${n.server.id}:${_compute.stableStringify.call(void 0, s)}`,i=new AbortController,l=!1,d=()=>{l||i.abort()};n.signal.addEventListener("abort",d,{once:!0});let h=_nullishCoalesce(n.acceptEncoding, () => ("")),a;try{a=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:c,responseMaxBytes:this.limits.computeResponseMaxBytes,maxSolveDurationMs:this.limits.maxSolveDurationMs,acceptEncoding:h,signal:i.signal,loadStartMs:_nullishCoalesce(n.loadStartMs, () => (performance.now())),defLoadMs:_nullishCoalesce(n.defLoadMs, () => (0)),prepMarks:n.prepMarks}),()=>{l=!0})}finally{n.signal.removeEventListener("abort",d)}if(a.kind!=="ok")return a;let p=T(a.envelope,h);return{...a,envelope:{...a.envelope,body:p.body,headers:p.headers}}}toResponse(n,t){let r=(o,c)=>{_optionalChain([t, 'optionalAccess', _26 => _26.onError])&&t.onError(o,c);let s={"Content-Type":"application/json"};return c.retryAfter!==void 0&&(s["Retry-After"]=String(c.retryAfter)),{status:o,headers:s,body:JSON.stringify(c)}};switch(n.kind){case"ok":return{status:200,headers:n.envelope.headers,body:n.envelope.body};case"timeout":return r(504,{message:n.message});case"client_abort":return r(499,{message:"Client closed request"});case"too_large":return r(413,{message:"Solve result is too large to return. This usually means a file output exceeds the supported size."});case"shed":return r(503,{message:n.message,retryAfter:n.retryAfterSeconds});case"compute_error":throw n.error}}toWebResponse(n){let t=this.toResponse(n);return new Response(typeof t.body=="string"?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(n){let t=n.definitionSource;if(ne(t)){let r=this.byteCache.getOrLoad(t.versionId,t.load);return{definitionSource:r,byteRefOutcome:r.outcome,identity:r.key}}if(te(t))return{definitionSource:t,byteRefOutcome:t.outcome,identity:t.key};if(_compute.isDefinitionRef.call(void 0, t))return{definitionSource:t,identity:t.key};if(!n.definitionKey)throw new Error("SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.");return{definitionSource:t,identity:n.definitionKey}}};exports.COMPUTE_CONTRACT_VERSION = F; exports.COMPUTE_VERSION_HEADER = _; exports.SolveEngine = L; exports.adaptEnvelopeToEncoding = T; exports.buildSolveInputTree = E; exports.createClientCache = k; exports.createDefinitionByteCache = P; exports.createSolveCacheSingleFlight = O; exports.isDefinitionRef = _compute.isDefinitionRef; exports.runSolvePipeline = B; exports.serverIdentity = H; exports.transformInputParameter = A;
2
2
  //# sourceMappingURL=server.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/solve-pipeline.ts","../src/server/transform-input.ts"],"names":["transformInputParameter","input","value","raw","processed","processInput","effective","gzipAsync","promisify","COMPUTE_CONTRACT_VERSION","COMPUTE_VERSION_HEADER","buildSolveInputTree","inputs","values","TreeBuilder","runSolvePipeline","args","client","signal","treeBuildStart","inputTree","treeBuildMs","settleSeqBefore","rhinoSeqBefore","result","solveStart","err","durationMs","RhinoComputeError","ErrorCodes","solveMs","serializeStart","serialized","serializeMs","worthSendingGzip","compressed","gzipMs","gzipStart","serverTotalMs","settle","rhino","headers","buildServerTiming","metrics","envelope","adaptEnvelopeToEncoding","acceptEncoding","waiterWantsGzip","json","gunzipSync","parts","header"],"mappings":"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","file":"/home/runner/work/selva/selva/packages/solve/dist/server.cjs","sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * framework-free half of a solve and returns a discriminated {@link SolveOutcome}\n * instead of throwing for expected failures. Nothing here touches auth, the\n * database, share tokens, rate limits, or metric sinks — those stay app policy\n * in the route that calls this.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n/** Async gzip — off the event loop, unlike `gzipSync`, which blocks it for multi-MB bodies. */\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 = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from\n\t * the definition-byte cache) whose bytes the scheduler materializes ONLY when\n\t * an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves\n\t * zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/**\n\t * When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome\n\t * so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for\n\t * raw-bytes solves.\n\t */\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}. When present\n\t * the pipeline skips its own build and solves this exact object.\n\t *\n\t * Needed whenever the caller coalesces concurrent solves: 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/** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */\n\tmaxSolveDurationMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Request abort signal, forwarded to the scheduler so a client disconnect\n\t * cancels the upstream compute call. Its `aborted` flag also disambiguates a\n\t * client disconnect from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/**\n\t * Wall-clock origin (`performance.now()` captured at the top of the request)\n\t * so the pipeline's `load`/`total` phase timings line up with the caller's\n\t * pre-solve prep.\n\t */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/**\n\t * Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`\n\t * Server-Timing entries.\n\t */\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 * Discriminated result. `ok` carries the envelope; every other variant names an\n * expected failure the transport maps to a status code (the app route maps\n * timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;\n * `compute_error` re-surfaces the original error for the generic 500/503 path).\n * `durationMs` on the error variants is the solve wall time up to the failure,\n * for 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/**\n\t * Scheduler backpressure shed the solve before it executed — the per-server\n\t * queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline\n\t * (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.\n\t * `retryAfterSeconds` is a suggested backoff hint for the client.\n\t */\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\n/**\n * Build the transformed input tree — the exact object handed to the scheduler.\n * `runSolvePipeline` calls this itself; a caller only needs it directly to see\n * the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).\n */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\t// --- input tree build ---------------------------------------------------\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// --- solve --------------------------------------------------------------\n\t// Snapshot the shared telemetry sequence counters. The holders live on the\n\t// warm client and are written by every request on this server; with\n\t// maxConcurrentSolves > 1 another request can write them mid-flight. After\n\t// the solve, a slot is attributed to THIS request only when exactly one\n\t// write happened since this snapshot — for solveMeta that single settle is\n\t// necessarily ours (onSettle fires before scheduler.solve() resolves).\n\t// Anything ambiguous is dropped from Server-Timing, never misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// Distinguish timeout (scheduler deadline timer) from client disconnect\n\t\t// (request signal). AbortError with the request signal NOT aborted means\n\t\t// the scheduler's own timeout fired — the solve genuinely timed out.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (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.maxSolveDurationMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// A full queue or an over-deadline queue wait sheds the solve BEFORE compute\n\t\t// runs. Classify these as `shed` rather than the generic `compute_error` —\n\t\t// they're load signals, not failures, and the client should back off and retry.\n\t\tif (err instanceof RhinoComputeError) {\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\t// --- serialize (size-guarded) ------------------------------------------\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 single string at ~512 MB; an oversized `file` output trips a\n\t\t// RangeError here. Surface it as the same too-large outcome as the byte cap.\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// --- gzip ---------------------------------------------------------------\n\t// Compress BEFORE the timing snapshot so its cost is a measured phase (`gzip`\n\t// in Server-Timing, included in `total`). Buffered (not streamed) so\n\t// Content-Length is known and a connection cut mid-transfer fails hard\n\t// instead of truncating the JSON. Skip gzip for bodies under the 1 KB\n\t// break-even — not 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\t// --- per-request telemetry attribution (see snapshot above) -------------\n\t// settle: ours iff exactly one settle happened since the snapshot. rhino:\n\t// only meaningful when the settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\t// --- Server-Timing envelope ----------------------------------------------\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// A downstream proxy skips re-encoding an already-`Content-Encoding`d body,\n\t\t// so this never double-compresses.\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}. That\n * envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a\n * later waiter with a different `Accept-Encoding` would otherwise get a body\n * labelled with the wrong encoding (`Vary` can't help — this is one object\n * shared across waiters, not a cache lookup). Encoding is deliberately not part\n * of the coalesce key, so mixed clients still coalesce; this adapts the shared\n * 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// The only mismatch that corrupts the response: a gzip body handed to a waiter\n\t// that did not advertise gzip. Gunzip it back to the JSON string and drop the\n\t// encoding headers. 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 ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * Assemble the `Server-Timing` header. The browser reads these to attribute its\n * round-trip: `total` here == the server's headers-to-out wall time, so browser\n * `ttfb − total` ≈ network+send latency. When the compute server reported its\n * own decode/solve/encode (VektorNode fork), `rhino_*` is time ON the compute\n * server and `compute_link` is everything between (network + queue wait). Prep\n * sub-phases ride as `p_*`; cache verdicts as 0/1 `selva_cache`/`def_reupload`.\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 onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\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\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// Byte-cache verdict for `DefinitionRef` solves: skipped = the scheduler never\n\t// materialized bytes (pointer-known solve — the whole point of this path), hit\n\t// = warm byte cache, miss = fell through to 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 { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`\n * and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`\n * are absent on `SchemaInput` (they live on the layout item's config), so\n * valueList/file inputs fall back to carrying the selected value with no option\n * list — the same value that reaches Grasshopper.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// Dynamic value lists ride the ValueList contract on the wire — the plugin's\n\t\t// GetDynamicValueListParameter deliberately reuses TypeName \"ValueList\" so the\n\t\t// compute fork's existing ValueList input case assigns it. processInput has no\n\t\t// dynamicValueList handler; without this mapping it falls back to Geometry with\n\t\t// a null default and TreeBuilder DROPS the input, so the user's selection never\n\t\t// reaches the definition (the param then solves on its own stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// Two cases where processInput drops a default we must carry anyway — a\n\t// dropped default means the input is omitted from the solve and the user's\n\t// selection never reaches Grasshopper:\n\t// - a plain array default (multi-select widgets, e.g. a dynamic value list\n\t// checklist) is dropped as \"malformed\".\n\t// - the ValueList fallback drops a scalar default it can't validate against\n\t// a `values` map — and `SchemaInput` never carries the option map (it\n\t// lives on the layout item's config), so every app value-list selection\n\t// lands here.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Scoped to ValueList: for other types a dropped scalar default means\n\t\t\t// the package rejected it as unparseable — restoring it would ship\n\t\t\t// garbage to Grasshopper.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n"]}
1
+ {"version":3,"sources":["../src/server/solve-pipeline.ts","../src/server/transform-input.ts"],"names":["transformInputParameter","input","value","raw","processed","processInput","effective","gzipAsync","promisify","COMPUTE_CONTRACT_VERSION","COMPUTE_VERSION_HEADER","buildSolveInputTree","inputs","values","TreeBuilder","runSolvePipeline","args","client","signal","treeBuildStart","inputTree","treeBuildMs","settleSeqBefore","rhinoSeqBefore","result","solveStart","err","durationMs","RhinoComputeError","ErrorCodes","solveMs","serializeStart","serialized","serializeMs","worthSendingGzip","compressed","gzipMs","gzipStart","serverTotalMs","settle","rhino","headers","buildServerTiming","metrics","envelope","adaptEnvelopeToEncoding","acceptEncoding","waiterWantsGzip","json","gunzipSync","parts","header"],"mappings":"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","file":"/home/runner/work/selva/selva/packages/solve/dist/server.cjs","sourcesContent":["/**\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\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } 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 = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\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\tmaxSolveDurationMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\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\n/** The transformed input tree exactly as handed to the scheduler; `runSolvePipeline` calls this itself unless the caller supplies {@link SolvePipelineArgs.inputTree}. */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\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\t// client.solveMeta/rhinoTiming live on the warm client and are written by\n\t// every request on this server; with scheduler maxConcurrent > 1, another\n\t// request can write them mid-flight. A slot is attributed to THIS request\n\t// only if exactly one write happened since this snapshot — for solveMeta\n\t// that single settle is necessarily ours, since onSettle fires before\n\t// scheduler.solve() resolves. Anything ambiguous is dropped from\n\t// Server-Timing rather than misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// AbortError with the request signal NOT aborted means the scheduler's own\n\t\t// deadline timer fired, not a client disconnect.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (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.maxSolveDurationMs / 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 RhinoComputeError) {\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\t// rhino is only meaningful when settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\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 ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\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\n * decode/solve/encode (VektorNode fork), `rhino_*` is time on the compute\n * server and `compute_link` is everything between it (network + queue wait).\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 onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\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\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\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 { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * `SchemaInput` has no `values`/`acceptedFormats` (those live on the layout item's config), so\n * valueList/file inputs here carry just the selected value with no option list.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// processInput has no dynamicValueList handler — the plugin's GetDynamicValueListParameter\n\t\t// reuses TypeName \"ValueList\" on the wire, so map it here or it falls back to Geometry with\n\t\t// a null default and TreeBuilder drops the input (Grasshopper solves on a stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// processInput drops a default in two cases we need to override, or the input is omitted from\n\t// the solve and the user's selection never reaches Grasshopper: a plain array default (e.g. a\n\t// dynamic value list checklist) is rejected as malformed; and ValueList drops a scalar default\n\t// it can't validate against a `values` map — which SchemaInput never carries, so every app\n\t// value-list selection hits this path.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Restoring for other types would mean shipping a value the package rejected as\n\t\t\t// genuinely unparseable — scoped to ValueList only.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n"]}