@selvajs/solve 2.0.0 → 2.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +2 -0
- package/dist/client.js +23 -6
- package/dist/client.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +52 -19
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -258,6 +258,8 @@ interface MeshPolicy<TMesh> {
|
|
|
258
258
|
clone(meshes: TMesh[]): TMesh[];
|
|
259
259
|
/** Must not free anything shared with live content (e.g. materials another object still uses). */
|
|
260
260
|
release(meshes: TMesh[]): void;
|
|
261
|
+
/** Bytes the meshes hold. Without it the memo caps by entry count only. */
|
|
262
|
+
byteSize?(meshes: TMesh[]): number;
|
|
261
263
|
}
|
|
262
264
|
interface RequestResponseDriverOptions<TMesh> {
|
|
263
265
|
/** How long one solve may take before the client aborts it (ms). Pass the deadline the solve's server enforces so the two agree. */
|
package/dist/client.js
CHANGED
|
@@ -605,9 +605,15 @@ function stableInputKey(input) {
|
|
|
605
605
|
const obj = value;
|
|
606
606
|
return `{${Object.keys(obj).filter((k) => obj[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${stableInputKey(obj[k])}`).join(",")}}`;
|
|
607
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Every entry is a full copy of its geometry, next to the copy on screen. A 13.6-million-triangle
|
|
610
|
+
* scene is about 390 MB per copy, so a count cap alone lets 16 entries reach 6 GB.
|
|
611
|
+
*/
|
|
612
|
+
const DEFAULT_MEMO_MAX_BYTES = 268435456;
|
|
608
613
|
function createSolveMemo(options = {}) {
|
|
609
|
-
const { max = 16, meshPolicy } = options;
|
|
614
|
+
const { max = 16, maxBytes = DEFAULT_MEMO_MAX_BYTES, meshPolicy } = options;
|
|
610
615
|
const entries = /* @__PURE__ */ new Map();
|
|
616
|
+
let totalBytes = 0;
|
|
611
617
|
function release(result) {
|
|
612
618
|
if (result.meshes?.length) meshPolicy?.release(result.meshes);
|
|
613
619
|
}
|
|
@@ -622,7 +628,8 @@ function createSolveMemo(options = {}) {
|
|
|
622
628
|
const entry = entries.get(key);
|
|
623
629
|
if (entry === void 0) return;
|
|
624
630
|
entries.delete(key);
|
|
625
|
-
|
|
631
|
+
totalBytes -= entry.bytes;
|
|
632
|
+
release(entry.result);
|
|
626
633
|
}
|
|
627
634
|
return {
|
|
628
635
|
get(values) {
|
|
@@ -632,20 +639,30 @@ function createSolveMemo(options = {}) {
|
|
|
632
639
|
entries.delete(key);
|
|
633
640
|
entries.set(key, hit);
|
|
634
641
|
console.debug(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);
|
|
635
|
-
return copy(hit);
|
|
642
|
+
return copy(hit.result);
|
|
636
643
|
},
|
|
637
644
|
set(values, result) {
|
|
638
645
|
const key = stableInputKey(values);
|
|
639
646
|
evict(key);
|
|
640
|
-
|
|
647
|
+
const bytes = result.meshes?.length ? meshPolicy?.byteSize?.(result.meshes) ?? 0 : 0;
|
|
648
|
+
if (bytes > maxBytes) {
|
|
649
|
+
console.debug(`[Solve/memo] not memoized: ${(bytes / 1048576).toFixed(0)} MB of meshes exceeds ${(maxBytes / 1048576).toFixed(0)} MB`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
entries.set(key, {
|
|
653
|
+
result: copy(result),
|
|
654
|
+
bytes
|
|
655
|
+
});
|
|
656
|
+
totalBytes += bytes;
|
|
641
657
|
for (const oldest of entries.keys()) {
|
|
642
|
-
if (entries.size <= max) break;
|
|
658
|
+
if (entries.size <= max && totalBytes <= maxBytes) break;
|
|
643
659
|
evict(oldest);
|
|
644
660
|
}
|
|
645
661
|
},
|
|
646
662
|
clear() {
|
|
647
|
-
entries.forEach(release);
|
|
663
|
+
entries.forEach((entry) => release(entry.result));
|
|
648
664
|
entries.clear();
|
|
665
|
+
totalBytes = 0;
|
|
649
666
|
}
|
|
650
667
|
};
|
|
651
668
|
}
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client/external-storage.ts","../src/client/events/live-solve.ts","../src/client/session/solve-session-core.ts","../src/client/session/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts","../src/client/events/solve-event-stream.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\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\n/** Null without a usable store. Merely reading `sessionStorage` throws when the browser blocks it. */\nfunction openSlot(ref: ExternalValueRef): { storage: Storage; key: string } | null {\n\tif (!ref.scopeKey || !ref.inputId) return null;\n\ttry {\n\t\tif (typeof sessionStorage === 'undefined') return null;\n\t\treturn {\n\t\t\tstorage: sessionStorage,\n\t\t\tkey: `${STORAGE_PREFIX}:${ref.scopeKey}:${ref.inputId}`\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst slot = openSlot(args);\n\tif (!slot) return;\n\ttry {\n\t\tslot.storage.setItem(slot.key, JSON.stringify(args.value));\n\t} catch (err) {\n\t\t// Over quota. The input then reads as missing, which the panel reports.\n\t\tconsole.warn('[Solve/storage] could not store an external value:', err);\n\t}\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst slot = openSlot(ref);\n\tif (!slot) return undefined;\n\tconst raw = slot.storage.getItem(slot.key);\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 slot = openSlot(ref);\n\tslot?.storage.removeItem(slot.key);\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","// What the running (or most recent) solve has said over the live channel, folded into one state\n// a host can render. Pure: `reduceLiveSolve` returns a new state when an event changes something\n// and the same object when it does not, so a host can compare by reference.\n//\n// One case per known kind. An unknown kind is kept in `other`, untouched, so a newer plugin's\n// events reach a host that knows them without this package having to.\n\nimport type { ProgressPayload, SolveEndedKind, SolveEvent } from '@selvajs/schemas';\nimport { decodeDiagnostic } from '../../shared/outcome.js';\nimport type { SolveDiagnostic } from '../../shared/solve-fn.js';\n\nexport interface LiveSolveState {\n\t/** The solve these events belong to; null before the first event. */\n\treadonly solveId: string | null;\n\t/** Between that solve's first event and its `solveEnded`. */\n\treadonly running: boolean;\n\t/** `diagnostic` events in arrival order. */\n\treadonly diagnostics: readonly SolveDiagnostic[];\n\t/** The newest `progress` per source. A report without a source is keyed by `''`. */\n\treadonly progress: ReadonlyMap<string, ProgressPayload>;\n\t/** `solveEnded`'s payload once it arrives. */\n\treadonly ended: { readonly kind: SolveEndedKind } | null;\n\t/** Events of kinds this package does not know, oldest first. */\n\treadonly other: readonly SolveEvent[];\n\t/** Highest `seq` applied for this solve. A late or repeated delivery at or below it is ignored. */\n\treadonly lastSeq: number;\n}\n\nexport const EMPTY_LIVE_SOLVE: LiveSolveState = Object.freeze({\n\tsolveId: null,\n\trunning: false,\n\tdiagnostics: [],\n\tprogress: new Map(),\n\tended: null,\n\tother: [],\n\tlastSeq: 0\n});\n\nfunction readEndedKind(payload: Record<string, unknown>): SolveEndedKind {\n\tconst { kind } = payload;\n\t// `error`, not `ok`: a payload this package can't read must not look like a clean finish.\n\treturn kind === 'ok' || kind === 'blocked' || kind === 'aborted' ? kind : 'error';\n}\n\nexport function reduceLiveSolve(state: LiveSolveState, event: SolveEvent): LiveSolveState {\n\t// A late end for a solve already replaced says nothing about the one on screen.\n\tif (event.type === 'solveEnded' && state.solveId !== null && event.solveId !== state.solveId) {\n\t\treturn state;\n\t}\n\t// A new id starts a new solve: nothing the previous one said carries over.\n\tconst base: LiveSolveState =\n\t\tevent.solveId === state.solveId\n\t\t\t? state\n\t\t\t: { ...EMPTY_LIVE_SOLVE, solveId: event.solveId, running: true };\n\tif (event.seq <= base.lastSeq) return state;\n\tconst next = { ...base, lastSeq: event.seq };\n\tconst payload = (event.payload ?? {}) as Record<string, unknown>;\n\n\tswitch (event.type) {\n\t\tcase 'solveStarted':\n\t\t\treturn { ...next, running: true };\n\n\t\tcase 'solveEnded':\n\t\t\treturn { ...next, running: false, ended: { kind: readEndedKind(payload) } };\n\n\t\tcase 'diagnostic': {\n\t\t\t// Same normalization as a finished solve's messages, so a live row and its final\n\t\t\t// counterpart are the same object shape.\n\t\t\tconst diagnostic = decodeDiagnostic(payload);\n\t\t\treturn diagnostic ? { ...next, diagnostics: [...next.diagnostics, diagnostic] } : next;\n\t\t}\n\n\t\tcase 'progress': {\n\t\t\tconst source = typeof payload.source === 'string' ? payload.source : '';\n\t\t\tconst progress = new Map(next.progress);\n\t\t\tconst report = readProgress(payload);\n\t\t\t// A bar that jumps back reads as broken. Within one solve a source only moves forward.\n\t\t\tconst previous = next.progress.get(source)?.fraction;\n\t\t\tif (previous !== undefined && (report.fraction ?? 0) < previous) report.fraction = previous;\n\t\t\tprogress.set(source, report);\n\t\t\treturn { ...next, progress };\n\t\t}\n\n\t\tdefault:\n\t\t\treturn { ...next, other: [...next.other, event] };\n\t}\n}\n\nfunction readProgress(payload: Record<string, unknown>): ProgressPayload {\n\tconst num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);\n\tconst fraction = num(payload.fraction);\n\tconst done = num(payload.done);\n\tconst total = num(payload.total);\n\tconst nextFraction = num(payload.nextFraction);\n\tconst stepMs = num(payload.stepMs);\n\tconst clamp = (v: number) => Math.min(1, Math.max(0, v));\n\treturn {\n\t\t...(typeof payload.source === 'string' ? { source: payload.source } : {}),\n\t\t...(fraction !== undefined ? { fraction: clamp(fraction) } : {}),\n\t\t...(done !== undefined ? { done } : {}),\n\t\t...(total !== undefined ? { total } : {}),\n\t\t...(typeof payload.label === 'string' ? { label: payload.label } : {}),\n\t\t...(nextFraction !== undefined ? { nextFraction: clamp(nextFraction) } : {}),\n\t\t...(stepMs !== undefined && stepMs > 0 ? { stepMs } : {})\n\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 { SolveDiagnostic, SolveResult } from '../../shared/solve-fn.js';\n\n/**\n * The slice of a reported result the session keeps addressable, so a host can commit exactly\n * what the viewer is showing — `source` and `values` included, already memo-correct.\n *\n * Mesh-free by construction. `SolveResult.meshes` are GPU-backed and the viewer disposes what\n * it renders on the next scene update, which is why the memo carries a `MeshPolicy`\n * clone/release contract at all. Retaining them here would hand out disposed instances after\n * the next solve, with no policy governing them and no owner. Everything left is inert JSON;\n * live meshes stay on `session.meshes`.\n */\nexport type RetainedSolveResult<TSource = unknown> = Omit<SolveResult<never, TSource>, 'meshes'>;\n\nexport interface SolveSessionState {\n\tvalues: Record<string, unknown>;\n\terror: string;\n\tcomputeErrors: string[];\n\tcomputeWarnings: string[];\n\t/**\n\t * The same messages with their level, source and `isGate`. What decides whether a solve\n\t * interrupts the user; the two string arrays above stay the plain display lists.\n\t */\n\tdiagnostics: SolveDiagnostic[];\n\t/**\n\t * The last solve was refused by a guard in the definition. Separate from `computeErrors`\n\t * being non-empty: a solve can report errors and still return usable outputs, and the UI\n\t * needs to tell \"solved, with complaints\" from \"would not solve\".\n\t */\n\tblocked: boolean;\n\t/**\n\t * The result withheld until the user answers for it, kept whole so `acknowledge()` can apply\n\t * it without a re-solve. While set, `meshes` and the solve's outputs stay at their previous\n\t * values. A newer solve drops it: the user only ever confirms the most recent result.\n\t */\n\theldResult: SolveResult | null;\n\tmeshes: unknown[];\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n\t/**\n\t * The last result reported, minus its meshes. Null before the first solve.\n\t *\n\t * Lags `values` by design: nothing updates it on `setValue` or `loadValues`, so between a\n\t * value change and the next report it describes the *previous* input set. That is what a\n\t * commit gate wants — what is on screen, not what is pending — so `lastResult.values`\n\t * disagreeing with `state.values` mid-scrub is the feature, not a bug.\n\t */\n\tlastResult: RetainedSolveResult | null;\n\t/**\n\t * Why the result on screen does not answer the current inputs, when nothing else says so: the\n\t * last solve was aborted, or the user discarded it. Nothing marks those inputs dirty, so\n\t * without this `ensureSolved` would hand out the older result as theirs.\n\t */\n\tunsolved: 'aborted' | 'discarded' | null;\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 createInitialState(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): SolveSessionState {\n\tconst manual = schema.instanceSolve === false;\n\treturn {\n\t\tvalues: buildInitialValues(schema, scopeKey, read),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tdiagnostics: [],\n\t\tblocked: false,\n\t\theldResult: null,\n\t\tmeshes: [],\n\t\thasPendingChanges: manual,\n\t\thasNeverSolved: manual,\n\t\tlastResult: null,\n\t\tunsolved: null\n\t};\n}\n\n/**\n * Whether the change should dispatch. Manual mode defers and raises the dirty flag instead;\n * `force` overrides that, and what it solves is every pending edit.\n */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined,\n\tforce = false\n): boolean {\n\tstate.values[id] = value;\n\tif (instanceSolve !== false) return true;\n\tstate.hasPendingChanges = !force;\n\treturn force;\n}\n\n/** Same rule as `applyValueChange`, for a whole set. Whether it should dispatch. */\nexport function applyLoadedValues(\n\tstate: SolveSessionState,\n\tincoming: Record<string, unknown>,\n\tinstanceSolve: boolean | undefined\n): boolean {\n\tObject.assign(state.values, incoming);\n\tif (instanceSolve !== false) return true;\n\tstate.hasPendingChanges = true;\n\treturn false;\n}\n\n// Worth sending even unchanged: a dynamic list's options can change under the same selection, and\n// a file can be picked again after its parameter was cleared.\nconst RESEND_UNCHANGED = new Set(['dynamicValueList', 'file']);\n\n/**\n * Whether setting `value` would solve again for nothing: it is what the last solve sent and what\n * the session holds, and nothing failed or aborted since (`lastSent` is null then). A field that\n * reports on input and again on blur would otherwise solve twice, and Grasshopper re-applies every\n * dynamic list on each update, so the repeat is a real solve. Objects never count as repeats.\n */\nexport function isRepeatOfLastSolve(\n\tschema: UISchema,\n\tlastSent: Record<string, unknown> | null,\n\tvalues: Record<string, unknown>,\n\tid: string,\n\tvalue: unknown\n): boolean {\n\tif (!lastSent || !(id in lastSent)) return false;\n\tif (value !== null && typeof value === 'object') return false;\n\tif (!Object.is(lastSent[id], value) || !Object.is(values[id], value)) return false;\n\tconst paramType = schema.inputs.find((input) => input.id === id)?.paramType;\n\treturn !RESEND_UNCHANGED.has(paramType ?? '');\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,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (Object.hasOwn(values, input.id)) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\n/** An input was edited after `sent` was dispatched, so a result for `sent` does not cover it. */\nexport function inputsChangedSince(\n\tschema: UISchema,\n\tvalues: Record<string, unknown>,\n\tsent: Record<string, unknown> | null\n): boolean {\n\tif (!sent) return false;\n\tconst current = pickInputValues(schema, values);\n\treturn Object.keys(current).some(\n\t\t(id) => !Object.hasOwn(sent, id) || !Object.is(current[id], sent[id])\n\t);\n}\n\n/**\n * `editedSinceSent` keeps the dirty flag for a value changed while the solve ran: its result\n * answers the older value, and clearing the flag would hide the Calculate prompt on a stale answer.\n */\nexport function applySolveResult(\n\tstate: SolveSessionState,\n\tresult: SolveResult,\n\teditedSinceSent: boolean\n): void {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.diagnostics = result.diagnostics ?? [];\n\tstate.blocked = result.blocked ?? false;\n\tstate.unsolved = result.aborted ? 'aborted' : null;\n\n\t// The solve is finished either way — Grasshopper cannot be paused mid-solution — so what is\n\t// held back here is the *result*, not the computation. Everything the solve produced is kept\n\t// intact in `heldResult` and released verbatim by `acknowledge()`.\n\t//\n\t// A blocked solve is applied at once: there is no result to weigh, and the previous one must\n\t// not stay on screen looking like the answer to the current inputs. The caller blanks the\n\t// outputs (see `blankOutputs`); the empty mesh list clears the viewer.\n\tif (!state.blocked && needsAcknowledgement(state.diagnostics)) {\n\t\tstate.heldResult = result;\n\t\tstate.hasPendingChanges &&= editedSinceSent;\n\t\treturn;\n\t}\n\n\tstate.heldResult = null;\n\tcommitSolveResult(state, result, editedSinceSent);\n}\n\n/**\n * Every schema output set to null. A blocked solve reports `outputs: {}`, and merging that into\n * `values` would leave the previous solve's outputs behind.\n */\nexport function blankOutputs(schema: UISchema): Record<string, null> {\n\tconst blank: Record<string, null> = {};\n\tfor (const output of schema.outputs) blank[output.id] = null;\n\treturn blank;\n}\n\n/** Writes a result into the state. Shared by the clean path and by `acknowledge()`. */\nexport function commitSolveResult(\n\tstate: SolveSessionState,\n\tresult: SolveResult,\n\teditedSinceSent: boolean\n): void {\n\tstate.meshes = result.meshes ?? [];\n\tconst { meshes: _meshes, ...retained } = result;\n\tstate.lastResult = retained;\n\tObject.assign(state.values, result.outputs);\n\tstate.hasPendingChanges &&= editedSinceSent;\n\tstate.hasNeverSolved = false;\n}\n\n/** What `ensureSolved` has to do for the current inputs. */\nexport type EnsureSolvedStep = 'wait' | 'solve' | 'discarded' | 'blocked' | 'applied';\n\n/** `busy`: the driver is solving or has a solve queued. */\nexport function ensureSolvedStep(state: SolveSessionState, busy: boolean): EnsureSolvedStep {\n\tif (busy || state.heldResult) return 'wait';\n\tif (state.hasPendingChanges) return 'solve';\n\t// Not re-solved: the same inputs would raise the same messages and ask the user again.\n\tif (state.unsolved === 'discarded') return 'discarded';\n\tif (state.unsolved || state.hasNeverSolved || state.error || !state.lastResult) return 'solve';\n\treturn state.blocked ? 'blocked' : 'applied';\n}\n\nexport type SolvePhase = 'idle' | 'solving' | 'review' | 'blocked';\n\n/**\n * One value for what a host should show. Solving wins: a new solve supersedes a held or refused\n * result. `isSolving` is passed in because the driver owns it.\n */\nexport function solvePhase(state: SolveSessionState, isSolving: boolean): SolvePhase {\n\tif (isSolving) return 'solving';\n\tif (state.heldResult) return 'review';\n\tif (state.blocked) return 'blocked';\n\treturn 'idle';\n}\n\n/**\n * Whether this solve's messages still need the user's acknowledgement.\n *\n * Only a Selva Message component gates. Grasshopper raises warnings constantly for reasons that\n * concern whoever is editing the definition, not whoever is using the app — a component\n * complaining about its own inputs is not something an end user can act on, and interrupting\n * them for it trains them to dismiss the one message that mattered.\n *\n * Every solve that gates asks again, by design — an acknowledgement covers the solve in front of\n * the user, never a future one. Under `instanceSolve` with a live warning that means a dialog\n * per solve, which is the intended checkpoint, not an oversight.\n *\n * A transport that reports no structure has no diagnostics, so nothing gates: the messages are\n * still shown, just without interrupting.\n */\nexport function needsAcknowledgement(diagnostics: SolveDiagnostic[]): boolean {\n\treturn diagnostics.some((d) => d.isGate && d.level !== 'remark');\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 { SolveDiagnostic, SolveResult } from '../../shared/solve-fn.js';\nimport type { SolveDriver } from '../drivers/driver.js';\nimport { EMPTY_LIVE_SOLVE, reduceLiveSolve, type LiveSolveState } from '../events/live-solve.js';\nimport {\n\tapplyLoadedValues,\n\tapplySolveResult,\n\tapplyValueChange,\n\tblankOutputs,\n\tcommitSolveResult,\n\tcreateInitialState,\n\tensureSolvedStep,\n\tinputsChangedSince,\n\tisRepeatOfLastSolve,\n\tpickInputValues,\n\tsolvePhase,\n\ttype SolvePhase,\n\ttype RetainedSolveResult\n} from './solve-session-core.js';\n\n/**\n * How a solve ended, once nothing about it can change any more: a held result counts only\n * after the user accepts or rejects it, and a result a queued solve is about to replace never\n * counts. One per solve that lands, whether a host asked for it or an input change did.\n */\nexport type SolveSettlement =\n\t| { status: 'applied'; result: RetainedSolveResult; diagnostics: SolveDiagnostic[] }\n\t/** A held result the user rejected or a newer solve replaced; the screen still shows the one before it. */\n\t| { status: 'discarded'; diagnostics: SolveDiagnostic[] }\n\t/** The definition refused the solve. */\n\t| { status: 'blocked'; diagnostics: SolveDiagnostic[] }\n\t/** Cancelled, or stopped in Grasshopper, before it produced a result. */\n\t| { status: 'aborted'; diagnostics: SolveDiagnostic[] }\n\t/** The transport failed or the solve ran past its deadline; `error` is what the user sees. */\n\t| { status: 'failed'; error: string };\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\t/**\n\t * The last solve's messages with their level, source and `isGate`. Richer than the two\n\t * string arrays above and the list a message UI should render; they remain for hosts that\n\t * only want the text. Empty from a transport that reports no structure.\n\t */\n\treadonly diagnostics: SolveDiagnostic[];\n\t/**\n\t * The definition refused the last solve, or it was aborted. Applied at once, not held: the\n\t * outputs are blanked and the viewer cleared, and `computeErrors` holds the reason.\n\t */\n\treadonly blocked: boolean;\n\t/**\n\t * The last solve raised messages awaiting the user's confirmation, and its result is held\n\t * back until then. A host shows the messages and calls `acknowledge()` to release them.\n\t * Never true for a blocked solve.\n\t */\n\treadonly awaitingAck: boolean;\n\t/**\n\t * Releases a held result into the session. No-op when nothing is held. The solve already\n\t * ran — this applies what it produced, it does not re-solve.\n\t */\n\tacknowledge(): void;\n\t/**\n\t * Drops a held result instead of applying it. The viewer keeps what it was showing. No-op\n\t * when nothing is held, and never re-solves — like `acknowledge()`, it only decides the\n\t * fate of a result that already exists.\n\t */\n\tdiscard(): void;\n\t/**\n\t * What the running (or most recent) solve has said so far: its diagnostics, progress per\n\t * source, and how it ended. Reset when a solve with a new id starts, so a host renders \"what\n\t * this solve has said\", never a tail of the previous one. Stays empty when the driver has no\n\t * live channel.\n\t */\n\treadonly live: LiveSolveState;\n\t/**\n\t * Where the session is, as one value instead of four flags: `solving` while a solve runs,\n\t * `review` while a result waits for `acknowledge()`, `blocked` after a refused or aborted\n\t * solve, `idle` otherwise.\n\t */\n\treadonly phase: SolvePhase;\n\t/**\n\t * Asks the transport to stop the solve in flight. Cooperative on both paths — Grasshopper\n\t * stops at the next component boundary — so the result is a `solveEnded` (or nothing, if it\n\t * finished first), never an instant halt.\n\t */\n\tabort(): void;\n\treadonly meshes: unknown[];\n\t/**\n\t * What the viewer is currently showing, including the `source`/`values` pair a commit path\n\t * needs. Null before the first solve, and again after `rebuild()`. Survives `reportError`: a\n\t * failed solve leaves the last good geometry on screen, so a host committing what it sees\n\t * should still see what produced it.\n\t */\n\treadonly lastResult: RetainedSolveResult | null;\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\t/**\n\t * Solves the current inputs. Resolves with the settlement of the solve that lands, which is\n\t * a later one if the inputs change meanwhile: what is on screen when it resolves. Never\n\t * rejects; a failure is a status. A rebuild or dispose first resolves it as `aborted`.\n\t */\n\tsolve(): Promise<SolveSettlement>;\n\t/**\n\t * The result for the current inputs, solving only when it has to: right away from the result\n\t * on screen when nothing changed, after the running or held solve when there is one, else\n\t * after a new solve. For an action that needs a fresh result, such as an export. Inputs whose\n\t * result the user just discarded settle as `discarded` rather than ask them again.\n\t */\n\tensureSolved(): Promise<SolveSettlement>;\n\t/** Fires once per settled solve, the ones input changes start included. Returns the unsubscribe. */\n\tonSettled(listener: (settlement: SolveSettlement) => void): () => 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\treportAborted(): 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\t/**\n\t * Resets the driver, stops listening to its live events and drops every subscriber. Call when\n\t * the session is discarded: the event source can outlive it (the cloud stream belongs to the\n\t * tab), and would otherwise keep feeding a session nobody reads. Inert afterwards.\n\t */\n\tdispose(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\n/** One listener's bug must not cost the others their update, or strand a solve mid-landing. */\nfunction notifyAll<T>(listeners: Iterable<(value: T) => void>, value: T) {\n\tfor (const listener of [...listeners]) {\n\t\ttry {\n\t\t\tlistener(value);\n\t\t} catch (err) {\n\t\t\tconsole.error('[Solve/session] listener threw:', err);\n\t\t}\n\t}\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\t// The inputs the last dispatch sent; null after a failure or abort, so the same values retry.\n\tlet lastSent: Record<string, unknown> | null = null;\n\t// Survives a failure, unlike `lastSent`: a queued solve may be running on exactly these inputs.\n\tlet lastDispatched: Record<string, unknown> | null = null;\n\n\tconst state = createInitialState(currentSchema, args.scopeKey, readExternalValue);\n\tlet disposed = false;\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => notifyAll(listeners, undefined);\n\n\tconst settledListeners = new Set<(settlement: SolveSettlement) => void>();\n\tlet waiters: ((settlement: SolveSettlement) => void)[] = [];\n\tconst ABORTED: SolveSettlement = { status: 'aborted', diagnostics: [] };\n\n\tfunction nextSettlement(): Promise<SolveSettlement> {\n\t\tif (disposed) return Promise.resolve(ABORTED);\n\t\treturn new Promise((resolve) => waiters.push(resolve));\n\t}\n\n\tfunction settle(settlement: SolveSettlement) {\n\t\tconst waiting = waiters;\n\t\twaiters = [];\n\t\twaiting.forEach((resolve) => resolve(settlement));\n\t\tnotifyAll(settledListeners, settlement);\n\t}\n\n\t/** Resolves waiting callers without telling listeners: no solve ended, the session moved on. */\n\tfunction abandon() {\n\t\tconst waiting = waiters;\n\t\twaiters = [];\n\t\twaiting.forEach((resolve) => resolve(ABORTED));\n\t}\n\n\tfunction settlementOf(result: SolveResult): SolveSettlement {\n\t\tconst diagnostics = state.diagnostics;\n\t\tif (result.aborted) return { status: 'aborted', diagnostics };\n\t\tif (result.blocked) return { status: 'blocked', diagnostics };\n\t\treturn { status: 'applied', result: state.lastResult!, diagnostics };\n\t}\n\n\t/** One solve ended. Tells subscribers, then settles unless a queued solve is about to replace it. */\n\tfunction land(settlement: () => SolveSettlement) {\n\t\temit();\n\t\tif (!args.driver.hasQueued) settle(settlement());\n\t}\n\n\t/**\n\t * Waiters stay: `solve()` resolves with what lands, and that is now the newer solve. Only\n\t * listeners hear of it, so the dropped solve still settles exactly once.\n\t */\n\tfunction dropHeld() {\n\t\tif (!state.heldResult) return;\n\t\tstate.heldResult = null;\n\t\tnotifyAll(settledListeners, { status: 'discarded', diagnostics: state.diagnostics });\n\t}\n\n\tfunction editedSinceSent() {\n\t\treturn inputsChangedSince(currentSchema, state.values, lastDispatched);\n\t}\n\n\tlet live: LiveSolveState = EMPTY_LIVE_SOLVE;\n\t// A driver with no live channel never calls this; nothing else in the session depends on it.\n\tconst stopEvents = args.driver.onEvent?.((event) => {\n\t\tconst next = reduceLiveSolve(live, event);\n\t\tif (next === live) return;\n\t\tlive = next;\n\t\temit();\n\t});\n\n\tfunction solve(): Promise<SolveSettlement> {\n\t\tconst settled = nextSettlement();\n\t\tdispatch();\n\t\treturn settled;\n\t}\n\n\tfunction dispatch() {\n\t\tif (disposed) return;\n\t\tdropHeld();\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\tlastSent = { ...pickInputValues(currentSchema, state.values) };\n\t\tlastDispatched = lastSent;\n\t\targs.driver.solve(lastSent);\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 diagnostics() {\n\t\t\treturn state.diagnostics;\n\t\t},\n\t\tget blocked() {\n\t\t\treturn state.blocked;\n\t\t},\n\t\tget awaitingAck() {\n\t\t\treturn state.heldResult !== null;\n\t\t},\n\n\t\tacknowledge() {\n\t\t\tconst held = state.heldResult;\n\t\t\tif (!held) return;\n\t\t\tstate.heldResult = null;\n\t\t\tcommitSolveResult(state, held, editedSinceSent());\n\t\t\tland(() => settlementOf(held));\n\t\t},\n\n\t\tdiscard() {\n\t\t\tif (!state.heldResult) return;\n\t\t\tstate.heldResult = null;\n\t\t\tstate.unsolved = 'discarded';\n\t\t\t// `values`, `meshes` and `lastResult` are untouched: the viewer keeps showing the last\n\t\t\t// result the user accepted. The messages stay on `computeErrors`/`computeWarnings` so\n\t\t\t// the footer still explains why nothing changed.\n\t\t\tland(() => ({ status: 'discarded', diagnostics: state.diagnostics }));\n\t\t},\n\t\tget live() {\n\t\t\treturn live;\n\t\t},\n\t\tget phase() {\n\t\t\treturn solvePhase(state, args.driver.isSolving);\n\t\t},\n\t\tabort() {\n\t\t\targs.driver.cancel();\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget lastResult() {\n\t\t\treturn state.lastResult;\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\tif (!forceSolve && isRepeatOfLastSolve(currentSchema, lastSent, state.values, id, value))\n\t\t\t\treturn;\n\t\t\tif (applyValueChange(state, id, value, currentSchema.instanceSolve, forceSolve)) {\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\n\t\tensureSolved() {\n\t\t\tconst busy = args.driver.isSolving || args.driver.hasQueued === true;\n\t\t\tconst step = ensureSolvedStep(state, busy);\n\t\t\tif (step === 'wait') return nextSettlement();\n\t\t\tif (step === 'solve') return solve();\n\t\t\tif (step === 'applied') {\n\t\t\t\treturn Promise.resolve({\n\t\t\t\t\tstatus: step,\n\t\t\t\t\tresult: state.lastResult!,\n\t\t\t\t\tdiagnostics: state.diagnostics\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn Promise.resolve({ status: step, diagnostics: state.diagnostics });\n\t\t},\n\n\t\tonSettled(listener) {\n\t\t\tsettledListeners.add(listener);\n\t\t\treturn () => settledListeners.delete(listener);\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tif (applyLoadedValues(state, incoming, currentSchema.instanceSolve)) {\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\trebuild(schema, scopeKey) {\n\t\t\tif (disposed) return;\n\t\t\t// A caller waiting on the old definition's solve would otherwise get the new one's.\n\t\t\tabandon();\n\t\t\tcurrentSchema = schema;\n\t\t\tlastSent = null;\n\t\t\tlastDispatched = null;\n\t\t\t// The old definition's running solve and cached results must not reach the new one,\n\t\t\t// even where the two share input ids.\n\t\t\targs.driver.reset?.();\n\t\t\tObject.assign(state, createInitialState(schema, scopeKey, readExternalValue));\n\t\t\tif (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\tconst rejected = result.blocked || result.aborted;\n\t\t\t// An aborted solve answered nothing, so whatever was dirty stays dirty.\n\t\t\tconst edited = result.aborted === true || editedSinceSent();\n\t\t\tif (result.aborted) lastSent = null;\n\t\t\t// `finalizeResult` already dropped a rejected solve's outputs; this also nulls the\n\t\t\t// ones still in `values` from the previous solve, which only the session can name.\n\t\t\tapplySolveResult(\n\t\t\t\tstate,\n\t\t\t\trejected ? { ...result, outputs: blankOutputs(currentSchema), meshes: [] } : result,\n\t\t\t\tedited\n\t\t\t);\n\t\t\t// A result a queued solve replaces never asks the user anything, and never settles.\n\t\t\tif (args.driver.hasQueued) state.heldResult = null;\n\t\t\t// A held result settles on acknowledge() or discard().\n\t\t\tif (state.heldResult) return emit();\n\t\t\tland(() => settlementOf(result));\n\t\t},\n\n\t\treportError(message) {\n\t\t\tdropHeld();\n\t\t\tlastSent = null;\n\t\t\tstate.error = message;\n\t\t\tstate.unsolved = null;\n\t\t\tland(() => ({ status: 'failed', error: message }));\n\t\t},\n\n\t\treportAborted() {\n\t\t\tlastSent = null;\n\t\t\tstate.unsolved = 'aborted';\n\t\t\tland(() => ABORTED);\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\n\t\tdispose() {\n\t\t\tdisposed = true;\n\t\t\tstopEvents?.();\n\t\t\tlisteners.clear();\n\t\t\tsettledListeners.clear();\n\t\t\targs.driver.reset?.();\n\t\t\tabandon();\n\t\t}\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\t/**\n\t * How long one run may take before it is aborted (ms). Required and deliberately\n\t * undefaulted: this is a generic throttle, so it has no basis for guessing what\n\t * the caller's work costs. Callers driving a Selva solve pass the deadline the\n\t * server enforces (the compute server's own timeout) — a shorter local guess would abort\n\t * solves the server would have finished.\n\t */\n\trunDeadlineMs: 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/** `run` must settle once its signal fires: the slot frees only then. */\nexport function createAsyncThrottle<T>(\n\trun: (values: T, signal: AbortSignal) => Promise<void>,\n\toptions: AsyncThrottleOptions\n): AsyncThrottle<T> {\n\tconst { runDeadlineMs, 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\t// A host's bug must not strand `isRunning`, or throw out of `finally` below.\n\t\ttry {\n\t\t\tonChange?.();\n\t\t} catch (err) {\n\t\t\tconsole.error('[Solve/throttle] onChange threw:', err);\n\t\t}\n\t}\n\n\tasync function execute(values: T) {\n\t\tconst controller = new AbortController();\n\t\tcurrentAbortController = controller;\n\t\tconst deadlineId = setTimeout(() => {\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${runDeadlineMs}ms — aborting`);\n\t\t\t// The reason tells `run` a timeout from a cancel: one is a failure, the other not.\n\t\t\tcontroller.abort(new DOMException('Run deadline exceeded', 'TimeoutError'));\n\t\t}, runDeadlineMs);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, controller.signal);\n\t\t} catch (err) {\n\t\t\t// `execute` is called fire-and-forget, so re-throwing would be an unhandled rejection.\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(deadlineId);\n\t\t\t// Before `setRunning`: a subscriber can start the next run from inside it, and that\n\t\t\t// run's controller must survive this cleanup.\n\t\t\tif (currentAbortController === controller) currentAbortController = null;\n\t\t\tsetRunning(false);\n\n\t\t\tif (pendingValues !== null && !isRunning) {\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// eslint-disable-next-line no-console -- fires per frame during a slider scrub\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\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\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. Otherwise matches what\n * `JSON.stringify` sends: `toJSON` is honoured and an `undefined` property is absent, which a\n * server treats differently from `null`.\n *\n * Throws on a `bigint`, as `JSON.stringify` does.\n */\nexport function stableInputKey(input: unknown): string {\n\tconst hasToJSON =\n\t\tinput !== null &&\n\t\ttypeof input === 'object' &&\n\t\ttypeof (input as { toJSON?: unknown }).toJSON === 'function';\n\tconst value = hasToJSON ? (input as { toJSON(): unknown }).toJSON() : input;\n\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(stableInputKey).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj)\n\t\t.filter((k) => obj[k] !== undefined)\n\t\t.sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${stableInputKey(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\t// eslint-disable-next-line no-console -- fires per frame during a slider scrub\n\t\t\tconsole.debug(`[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\tfor (const oldest of entries.keys()) {\n\t\t\t\tif (entries.size <= max) break;\n\t\t\t\tevict(oldest);\n\t\t\t}\n\t\t},\n\t\tclear() {\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 { raceAbort } from '../../shared/abort.js';\nimport type { SolveFn, SolveResult } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveEventSource, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\t/** How long one solve may take before the client aborts it (ms). Pass the deadline the solve's server enforces so the two agree. */\n\tsolveDeadlineMs: number;\n\tonChange?: () => void;\n\t/** Mesh ownership policy for the result memo — see `MeshPolicy` (solve-memo.ts). */\n\tmeshPolicy?: MeshPolicy<TMesh>;\n\t/** Live events for the solves this driver runs; without it the session never sees any. */\n\tevents?: SolveEventSource;\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, solveDeadlineMs, onChange, events } = 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\t// Bumped by `reset`. A run from an older generation belongs to a definition that is gone: it\n\t// must neither fill the memo nor report into the session that replaced it.\n\tlet generation = 0;\n\n\t// A stopped run still ends in a report: a timeout is a failure the user should read, a\n\t// cancel is not. Either way a host awaiting the solve hears back.\n\tfunction reportStopped(signal: AbortSignal) {\n\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\tconsole.debug('[Solve/driver] solve stopped (cancelled or timed out)');\n\t\tconst reason = signal.reason as { name?: string } | undefined;\n\t\tif (reason?.name === 'TimeoutError') {\n\t\t\tgetReporter().reportError(\n\t\t\t\t`The solve took longer than ${Math.round(solveDeadlineMs / 1000)} s and was stopped.`\n\t\t\t);\n\t\t} else {\n\t\t\tgetReporter().reportAborted();\n\t\t}\n\t}\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(\n\t\tasync (values, signal) => {\n\t\t\tconst startedIn = generation;\n\t\t\tlet result: SolveResult<TMesh>;\n\t\t\t// Only the solve is inside the `try`. A report that throws is a host bug, and landing\n\t\t\t// here would settle a good solve a second time, as failed.\n\t\t\ttry {\n\t\t\t\tconst cached = memo.get(values);\n\t\t\t\tif (cached !== undefined) {\n\t\t\t\t\tresult = cached;\n\t\t\t\t} else {\n\t\t\t\t\t// Raced: a `SolveFn` that ignores its signal must not pin the throttle's slot past\n\t\t\t\t\t// the deadline. Its late settlement is dropped.\n\t\t\t\t\tconst solved = await raceAbort(onSolve(values, signal), signal);\n\t\t\t\t\t// Stamped here rather than in the SolveFn: a memo hit never calls it, so values\n\t\t\t\t\t// captured inside would pair the served result with whatever solved last.\n\t\t\t\t\tresult = { ...solved, values };\n\t\t\t\t\t// An abort is not an answer for these inputs, so the same values must solve again.\n\t\t\t\t\tif (!result.aborted && startedIn === generation) memo.set(values, result);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tif (startedIn !== generation) return;\n\t\t\t\tif (signal.aborted) return reportStopped(signal);\n\t\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\t\treturn getReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t\t}\n\t\t\tif (startedIn !== generation) return;\n\t\t\tif (signal.aborted) return reportStopped(signal);\n\t\t\tgetReporter().report(result);\n\t\t},\n\t\t// The throttle is generic, so the solve deadline is what bounds one of its runs.\n\t\t{ runDeadlineMs: solveDeadlineMs, onChange }\n\t);\n\n\tfunction cancel() {\n\t\t// Dropping the fetch alone stops the Compute child: the server closes the solve and the\n\t\t// plugin's next callback gets a 410. The explicit cancel is for a server that keeps the\n\t\t// solve open, and loses the race harmlessly (404) when the fetch abort closes it first.\n\t\tthrottle.cancel();\n\t\tevents?.cancelCurrent?.();\n\t}\n\n\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel,\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tget hasQueued() {\n\t\t\treturn throttle.hasPending;\n\t\t},\n\t\treset() {\n\t\t\tgeneration += 1;\n\t\t\tcancel();\n\t\t\tmemo.clear();\n\t\t},\n\t\t...(events ? { onEvent: (listener) => events.subscribe(listener) } : {})\n\t} satisfies SolveDriver;\n}\n","// The cloud half of the live channel: one SSE stream per tab, carrying every solve this tab\n// runs. The stream id is minted here rather than by the server so a solve request can name it\n// before the stream has even connected.\n//\n// Opened when created, not on first subscribe: the server binds a solve to the stream only if\n// the stream is already registered, and a lazily opened one was rarely connected by the time\n// the first solve ran.\n\nimport type { SolveEvent } from '@selvajs/schemas';\nimport type { SolveEventSource } from '../drivers/driver.js';\n\nconst MIN_RECONNECT_MS = 1_000;\nconst MAX_RECONNECT_MS = 30_000;\n\nexport interface SolveEventStreamOptions {\n\t/** The SSE endpoint, e.g. `/api/v1/solve-events`. `streamId` is appended as a query param. */\n\tendpoint: string;\n\t/** Builds the cancel URL for a solve, e.g. `(id) => \\`/api/v1/solve/${id}/cancel\\``. */\n\tcancelEndpoint: (solveId: string) => string;\n\tfetch?: typeof fetch;\n}\n\nexport interface SolveEventStream extends SolveEventSource {\n\t/** Send this with every solve request so the server can route that solve's events here. */\n\treadonly streamId: string;\n\t/** The solve the server last reported as started and not yet ended; null between solves. */\n\treadonly currentSolveId: string | null;\n\t/** Final: the stream does not reopen, and a later subscriber hears nothing. */\n\tclose(): void;\n}\n\nfunction mintStreamId(): string {\n\tif (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {\n\t\treturn crypto.randomUUID().replace(/-/g, '');\n\t}\n\treturn Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');\n}\n\nexport function createSolveEventStream(options: SolveEventStreamOptions): SolveEventStream {\n\tconst streamId = mintStreamId();\n\tconst listeners = new Set<(event: SolveEvent) => void>();\n\tconst doFetch = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args));\n\tlet source: EventSource | null = null;\n\tlet currentSolveId: string | null = null;\n\tlet reconnectMs = MIN_RECONNECT_MS;\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n\tfunction open() {\n\t\tif (source || typeof EventSource === 'undefined') return;\n\t\tconst url = `${options.endpoint}${options.endpoint.includes('?') ? '&' : '?'}streamId=${streamId}`;\n\t\tconst opened = new EventSource(url);\n\t\tsource = opened;\n\t\topened.addEventListener('open', () => {\n\t\t\treconnectMs = MIN_RECONNECT_MS;\n\t\t});\n\t\topened.addEventListener('solve', (raw) => {\n\t\t\tlet event: SolveEvent;\n\t\t\ttry {\n\t\t\t\tevent = JSON.parse((raw as MessageEvent<string>).data);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.type === 'solveStarted') currentSolveId = event.solveId;\n\t\t\telse if (event.type === 'solveEnded' && currentSolveId === event.solveId)\n\t\t\t\tcurrentSolveId = null;\n\t\t\tfor (const listener of listeners) listener(event);\n\t\t});\n\t\t// EventSource reconnects after a dropped connection on its own, but a non-200 closes it for\n\t\t// good: one 502 during a deploy would end progress and cancel until a reload.\n\t\topened.addEventListener('error', () => {\n\t\t\tif (opened.readyState !== EventSource.CLOSED || source !== opened) return;\n\t\t\tsource = null;\n\t\t\tcurrentSolveId = null;\n\t\t\treconnectTimer = setTimeout(open, reconnectMs);\n\t\t\treconnectMs = Math.min(reconnectMs * 2, MAX_RECONNECT_MS);\n\t\t});\n\t}\n\n\topen();\n\n\treturn {\n\t\tstreamId,\n\t\tget currentSolveId() {\n\t\t\treturn currentSolveId;\n\t\t},\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tcancelCurrent() {\n\t\t\tconst solveId = currentSolveId;\n\t\t\tif (!solveId) return;\n\t\t\t// Fire-and-forget: the answer arrives as `solveEnded` on the stream, or not at all if\n\t\t\t// the solve finished first, and either way there is nothing to await here.\n\t\t\tvoid doFetch(options.cancelEndpoint(solveId), { method: 'POST', keepalive: true }).catch(\n\t\t\t\t() => {}\n\t\t\t);\n\t\t},\n\t\tclose() {\n\t\t\tif (reconnectTimer) clearTimeout(reconnectTimer);\n\t\t\tsource?.close();\n\t\t\tsource = null;\n\t\t\tlisteners.clear();\n\t\t\tcurrentSolveId = null;\n\t\t}\n\t};\n}\n","/**\n * A ready-made `SolveFn` that POSTs to a `/api/compute`-shaped endpoint (the wire contract\n * `SolveEngine` produces) and unpacks the result, with 429 cooldown, session-expiry detection,\n * a bad-response guard, and abort handling at every await.\n *\n * Mesh decoding is NOT built in: this package stays renderer-agnostic, so a viewer-enabled\n * consumer supplies `meshes.extract` (typically `getThreeObjectsFromComputeResponse` from\n * `@selvajs/visualization/parse`).\n */\n\nimport {\n\tGrasshopperResponseProcessor,\n\ttype GrasshopperComputeResponse\n} from '@selvajs/compute/grasshopper';\nimport type { SolveFn, SolveResult } from '../shared/solve-fn.js';\nimport { decodeOutcome, finalizeResult, outcomeFromComputeMessages } from '../shared/outcome.js';\n\nexport interface ComputeFetchSolveFnOptions<TMesh = 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`. A stored definition's server ignores them. */\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/** The tab's live-event stream id (`createSolveEventStream().streamId`), so the server routes this solve's events to it. */\n\tstreamId?: () => string | null | undefined;\n\t/** Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`. */\n\tmeshes?: {\n\t\textract: (\n\t\t\tresponse: GrasshopperComputeResponse,\n\t\t\topts: { debug: boolean; signal: AbortSignal }\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 and on a solve refused during its cooldown, before the rate-limit Error. */\n\tonRateLimited?: (retryAfterSeconds: number) => void;\n\t/** Called on a 401 or a login-page redirect, before the \"sign in again\" Error. */\n\tonSessionExpired?: () => void;\n\t/** Where the server redirects an expired session. Default `/login`. */\n\tloginPath?: string;\n}\n\n/** Applied to a 429 that names no wait. */\nconst DEFAULT_RETRY_AFTER_S = 5;\n/** A server's hint, not a lock: past this the client tries again anyway. */\nconst MAX_RETRY_AFTER_S = 3600;\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\nfunction rateLimitedError(retryAfterSeconds: number): Error {\n\treturn new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);\n}\n\nfunction sessionExpiredError(): Error {\n\treturn 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\n/** `Retry-After` in either RFC form. Null when it names no usable wait. */\nfunction parseRetryAfter(value: string): number | null {\n\tif (!value.trim()) return null;\n\tconst seconds = Number(value);\n\tif (!Number.isNaN(seconds)) return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;\n\tconst at = Date.parse(value);\n\treturn Number.isNaN(at) ? null : Math.max(0, (at - Date.now()) / 1000);\n}\n\nfunction readRetryAfter(header: string | null, bodyHint: unknown): number | null {\n\tconst seconds =\n\t\t(header === null ? null : parseRetryAfter(header)) ??\n\t\t(typeof bodyHint === 'number' ? parseRetryAfter(String(bodyHint)) : null);\n\treturn seconds === null ? null : Math.min(seconds, MAX_RETRY_AFTER_S);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction parseErrorBody(text: string): { message?: string; retryAfter?: unknown } {\n\ttry {\n\t\tconst body: unknown = JSON.parse(text);\n\t\tif (!isPlainObject(body)) return {};\n\t\treturn {\n\t\t\t...(typeof body.message === 'string' ? { message: body.message } : {}),\n\t\t\tretryAfter: body.retryAfter\n\t\t};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\nexport function createComputeFetchSolveFn<TMesh = unknown>(\n\topts: ComputeFetchSolveFnOptions<TMesh>\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 cooldownUntil = 0;\n\n\tfunction rateLimited(retryAfterSeconds: number): Error {\n\t\topts.onRateLimited?.(retryAfterSeconds);\n\t\treturn rateLimitedError(retryAfterSeconds);\n\t}\n\n\treturn async (values, signal): Promise<SolveResult<TMesh, GrasshopperComputeResponse>> => {\n\t\tconst remainingMs = cooldownUntil - Date.now();\n\t\tif (remainingMs > 0) throw rateLimited(remainingMs / 1000);\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\t\tconst streamId = opts.streamId?.();\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\t...(streamId ? { streamId } : {})\n\t\t});\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\t// Also how a redirect to a cross-origin sign-in looks from here, so an expired session\n\t\t\t// stays a possibility.\n\t\t\tthrow new Error(\n\t\t\t\t'Could not reach the server. Check your connection, or reload the page if your session expired.',\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 =\n\t\t\tres.redirected && new URL(res.url).pathname === (opts.loginPath ?? '/login');\n\t\tif (res.status === 401 || redirectedToLogin) {\n\t\t\topts.onSessionExpired?.();\n\t\t\tthrow sessionExpiredError();\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tconst errorBody = await res.text().catch(() => '');\n\t\t\tconst { message, retryAfter: bodyHint } = parseErrorBody(errorBody);\n\t\t\tif (errorBody && message === undefined) {\n\t\t\t\tlog(`[Compute] unreadable error body (HTTP ${res.status}, ${errorBody.length} chars)`);\n\t\t\t}\n\t\t\t// Any status may ask for a wait: a shed 503 says the same thing as a 429, and ignoring it\n\t\t\t// would hit a saturated server hardest at the moment it asks clients to back off.\n\t\t\tconst retryAfter = readRetryAfter(res.headers.get('Retry-After'), bodyHint);\n\t\t\tif (retryAfter !== null) cooldownUntil = Date.now() + retryAfter * 1000;\n\t\t\tif (res.status === 503 || res.status === 504) {\n\t\t\t\t// The server names the failure class (unreachable, bad API key,\n\t\t\t\t// rate-limited, timed out) — show it rather than a blanket \"offline\".\n\t\t\t\tthrow new Error(\n\t\t\t\t\tmessage || 'Compute server is offline or unreachable. Please try again later.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (res.status === 429) {\n\t\t\t\tconst seconds = retryAfter ?? DEFAULT_RETRY_AFTER_S;\n\t\t\t\tcooldownUntil = Date.now() + seconds * 1000;\n\t\t\t\tthrow rateLimited(seconds);\n\t\t\t}\n\t\t\tthrow new Error(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\tlet bodyText: string;\n\t\ttry {\n\t\t\tbodyText = await res.text();\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\tlog('[Compute] solve aborted mid-download — empty result discarded');\n\t\t\t\treturn { outputs: {} };\n\t\t\t}\n\t\t\tthrow new Error('The connection dropped while receiving the result. Please try again.', {\n\t\t\t\tcause: err\n\t\t\t});\n\t\t}\n\t\tconst downloadMs = performance.now() - downloadStart;\n\t\tconst bodyChars = bodyText.length;\n\n\t\tconst invalidResponse = (cause?: unknown) =>\n\t\t\tnew Error('Received an invalid response from the server. Reload the page and try again.', {\n\t\t\t\tcause\n\t\t\t});\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(bodyText);\n\t\t} catch (err) {\n\t\t\tthrow invalidResponse(err);\n\t\t}\n\t\tif (!isPlainObject(parsed)) throw invalidResponse();\n\t\tconst solved = parsed as unknown as GrasshopperComputeResponse;\n\t\tconst processor = new GrasshopperResponseProcessor(solved);\n\n\t\t// The plugin's own verdict when the fork returned one; otherwise recovered from the\n\t\t// markers in Compute's flattened strings (stock server, or a plugin that predates it).\n\t\tconst outcome =\n\t\t\tdecodeOutcome(solved.selva?.outcome) ??\n\t\t\toutcomeFromComputeMessages(solved.errors ?? [], solved.warnings ?? []);\n\t\t// Decided before parsing: an older fork still ships a rejected solve's outputs, and the\n\t\t// geometry alone can be megabytes the client would only throw away.\n\t\tconst rejected = outcome.blocked || outcome.aborted;\n\n\t\tconst meshes =\n\t\t\topts.meshes && !rejected\n\t\t\t\t? await opts.meshes.extract(processor.response, { debug, signal })\n\t\t\t\t: [];\n\n\t\tconst resultOutputs: Record<string, unknown> = {};\n\t\tfor (const o of rejected ? [] : 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 valuesBytes = JSON.stringify(values).length;\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 = bodyChars / (1024 * 1024);\n\t\t\tconst size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bodyChars / 1024).toFixed(0)} KB`;\n\t\t\tconst mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1000)).toFixed(1) : '∞';\n\t\t\tconst heapBytes = (performance as Performance & { memory?: { usedJSHeapSize: number } })\n\t\t\t\t.memory?.usedJSHeapSize;\n\t\t\tconst heap =\n\t\t\t\theapBytes !== undefined ? ` | heap=${(heapBytes / (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: serverTiming.rhino_cache\n\t\t\t\t\t\t? \"HIT — Rhino.Compute's result cache, solved nothing\"\n\t\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 finalizeResult({ outputs: resultOutputs, meshes, source: solved }, outcome);\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client/external-storage.ts","../src/client/events/live-solve.ts","../src/client/session/solve-session-core.ts","../src/client/session/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts","../src/client/events/solve-event-stream.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\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\n/** Null without a usable store. Merely reading `sessionStorage` throws when the browser blocks it. */\nfunction openSlot(ref: ExternalValueRef): { storage: Storage; key: string } | null {\n\tif (!ref.scopeKey || !ref.inputId) return null;\n\ttry {\n\t\tif (typeof sessionStorage === 'undefined') return null;\n\t\treturn {\n\t\t\tstorage: sessionStorage,\n\t\t\tkey: `${STORAGE_PREFIX}:${ref.scopeKey}:${ref.inputId}`\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst slot = openSlot(args);\n\tif (!slot) return;\n\ttry {\n\t\tslot.storage.setItem(slot.key, JSON.stringify(args.value));\n\t} catch (err) {\n\t\t// Over quota. The input then reads as missing, which the panel reports.\n\t\tconsole.warn('[Solve/storage] could not store an external value:', err);\n\t}\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst slot = openSlot(ref);\n\tif (!slot) return undefined;\n\tconst raw = slot.storage.getItem(slot.key);\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 slot = openSlot(ref);\n\tslot?.storage.removeItem(slot.key);\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","// What the running (or most recent) solve has said over the live channel, folded into one state\n// a host can render. Pure: `reduceLiveSolve` returns a new state when an event changes something\n// and the same object when it does not, so a host can compare by reference.\n//\n// One case per known kind. An unknown kind is kept in `other`, untouched, so a newer plugin's\n// events reach a host that knows them without this package having to.\n\nimport type { ProgressPayload, SolveEndedKind, SolveEvent } from '@selvajs/schemas';\nimport { decodeDiagnostic } from '../../shared/outcome.js';\nimport type { SolveDiagnostic } from '../../shared/solve-fn.js';\n\nexport interface LiveSolveState {\n\t/** The solve these events belong to; null before the first event. */\n\treadonly solveId: string | null;\n\t/** Between that solve's first event and its `solveEnded`. */\n\treadonly running: boolean;\n\t/** `diagnostic` events in arrival order. */\n\treadonly diagnostics: readonly SolveDiagnostic[];\n\t/** The newest `progress` per source. A report without a source is keyed by `''`. */\n\treadonly progress: ReadonlyMap<string, ProgressPayload>;\n\t/** `solveEnded`'s payload once it arrives. */\n\treadonly ended: { readonly kind: SolveEndedKind } | null;\n\t/** Events of kinds this package does not know, oldest first. */\n\treadonly other: readonly SolveEvent[];\n\t/** Highest `seq` applied for this solve. A late or repeated delivery at or below it is ignored. */\n\treadonly lastSeq: number;\n}\n\nexport const EMPTY_LIVE_SOLVE: LiveSolveState = Object.freeze({\n\tsolveId: null,\n\trunning: false,\n\tdiagnostics: [],\n\tprogress: new Map(),\n\tended: null,\n\tother: [],\n\tlastSeq: 0\n});\n\nfunction readEndedKind(payload: Record<string, unknown>): SolveEndedKind {\n\tconst { kind } = payload;\n\t// `error`, not `ok`: a payload this package can't read must not look like a clean finish.\n\treturn kind === 'ok' || kind === 'blocked' || kind === 'aborted' ? kind : 'error';\n}\n\nexport function reduceLiveSolve(state: LiveSolveState, event: SolveEvent): LiveSolveState {\n\t// A late end for a solve already replaced says nothing about the one on screen.\n\tif (event.type === 'solveEnded' && state.solveId !== null && event.solveId !== state.solveId) {\n\t\treturn state;\n\t}\n\t// A new id starts a new solve: nothing the previous one said carries over.\n\tconst base: LiveSolveState =\n\t\tevent.solveId === state.solveId\n\t\t\t? state\n\t\t\t: { ...EMPTY_LIVE_SOLVE, solveId: event.solveId, running: true };\n\tif (event.seq <= base.lastSeq) return state;\n\tconst next = { ...base, lastSeq: event.seq };\n\tconst payload = (event.payload ?? {}) as Record<string, unknown>;\n\n\tswitch (event.type) {\n\t\tcase 'solveStarted':\n\t\t\treturn { ...next, running: true };\n\n\t\tcase 'solveEnded':\n\t\t\treturn { ...next, running: false, ended: { kind: readEndedKind(payload) } };\n\n\t\tcase 'diagnostic': {\n\t\t\t// Same normalization as a finished solve's messages, so a live row and its final\n\t\t\t// counterpart are the same object shape.\n\t\t\tconst diagnostic = decodeDiagnostic(payload);\n\t\t\treturn diagnostic ? { ...next, diagnostics: [...next.diagnostics, diagnostic] } : next;\n\t\t}\n\n\t\tcase 'progress': {\n\t\t\tconst source = typeof payload.source === 'string' ? payload.source : '';\n\t\t\tconst progress = new Map(next.progress);\n\t\t\tconst report = readProgress(payload);\n\t\t\t// A bar that jumps back reads as broken. Within one solve a source only moves forward.\n\t\t\tconst previous = next.progress.get(source)?.fraction;\n\t\t\tif (previous !== undefined && (report.fraction ?? 0) < previous) report.fraction = previous;\n\t\t\tprogress.set(source, report);\n\t\t\treturn { ...next, progress };\n\t\t}\n\n\t\tdefault:\n\t\t\treturn { ...next, other: [...next.other, event] };\n\t}\n}\n\nfunction readProgress(payload: Record<string, unknown>): ProgressPayload {\n\tconst num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);\n\tconst fraction = num(payload.fraction);\n\tconst done = num(payload.done);\n\tconst total = num(payload.total);\n\tconst nextFraction = num(payload.nextFraction);\n\tconst stepMs = num(payload.stepMs);\n\tconst clamp = (v: number) => Math.min(1, Math.max(0, v));\n\treturn {\n\t\t...(typeof payload.source === 'string' ? { source: payload.source } : {}),\n\t\t...(fraction !== undefined ? { fraction: clamp(fraction) } : {}),\n\t\t...(done !== undefined ? { done } : {}),\n\t\t...(total !== undefined ? { total } : {}),\n\t\t...(typeof payload.label === 'string' ? { label: payload.label } : {}),\n\t\t...(nextFraction !== undefined ? { nextFraction: clamp(nextFraction) } : {}),\n\t\t...(stepMs !== undefined && stepMs > 0 ? { stepMs } : {})\n\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 { SolveDiagnostic, SolveResult } from '../../shared/solve-fn.js';\n\n/**\n * The slice of a reported result the session keeps addressable, so a host can commit exactly\n * what the viewer is showing — `source` and `values` included, already memo-correct.\n *\n * Mesh-free by construction. `SolveResult.meshes` are GPU-backed and the viewer disposes what\n * it renders on the next scene update, which is why the memo carries a `MeshPolicy`\n * clone/release contract at all. Retaining them here would hand out disposed instances after\n * the next solve, with no policy governing them and no owner. Everything left is inert JSON;\n * live meshes stay on `session.meshes`.\n */\nexport type RetainedSolveResult<TSource = unknown> = Omit<SolveResult<never, TSource>, 'meshes'>;\n\nexport interface SolveSessionState {\n\tvalues: Record<string, unknown>;\n\terror: string;\n\tcomputeErrors: string[];\n\tcomputeWarnings: string[];\n\t/**\n\t * The same messages with their level, source and `isGate`. What decides whether a solve\n\t * interrupts the user; the two string arrays above stay the plain display lists.\n\t */\n\tdiagnostics: SolveDiagnostic[];\n\t/**\n\t * The last solve was refused by a guard in the definition. Separate from `computeErrors`\n\t * being non-empty: a solve can report errors and still return usable outputs, and the UI\n\t * needs to tell \"solved, with complaints\" from \"would not solve\".\n\t */\n\tblocked: boolean;\n\t/**\n\t * The result withheld until the user answers for it, kept whole so `acknowledge()` can apply\n\t * it without a re-solve. While set, `meshes` and the solve's outputs stay at their previous\n\t * values. A newer solve drops it: the user only ever confirms the most recent result.\n\t */\n\theldResult: SolveResult | null;\n\tmeshes: unknown[];\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n\t/**\n\t * The last result reported, minus its meshes. Null before the first solve.\n\t *\n\t * Lags `values` by design: nothing updates it on `setValue` or `loadValues`, so between a\n\t * value change and the next report it describes the *previous* input set. That is what a\n\t * commit gate wants — what is on screen, not what is pending — so `lastResult.values`\n\t * disagreeing with `state.values` mid-scrub is the feature, not a bug.\n\t */\n\tlastResult: RetainedSolveResult | null;\n\t/**\n\t * Why the result on screen does not answer the current inputs, when nothing else says so: the\n\t * last solve was aborted, or the user discarded it. Nothing marks those inputs dirty, so\n\t * without this `ensureSolved` would hand out the older result as theirs.\n\t */\n\tunsolved: 'aborted' | 'discarded' | null;\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 createInitialState(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): SolveSessionState {\n\tconst manual = schema.instanceSolve === false;\n\treturn {\n\t\tvalues: buildInitialValues(schema, scopeKey, read),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tdiagnostics: [],\n\t\tblocked: false,\n\t\theldResult: null,\n\t\tmeshes: [],\n\t\thasPendingChanges: manual,\n\t\thasNeverSolved: manual,\n\t\tlastResult: null,\n\t\tunsolved: null\n\t};\n}\n\n/**\n * Whether the change should dispatch. Manual mode defers and raises the dirty flag instead;\n * `force` overrides that, and what it solves is every pending edit.\n */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined,\n\tforce = false\n): boolean {\n\tstate.values[id] = value;\n\tif (instanceSolve !== false) return true;\n\tstate.hasPendingChanges = !force;\n\treturn force;\n}\n\n/** Same rule as `applyValueChange`, for a whole set. Whether it should dispatch. */\nexport function applyLoadedValues(\n\tstate: SolveSessionState,\n\tincoming: Record<string, unknown>,\n\tinstanceSolve: boolean | undefined\n): boolean {\n\tObject.assign(state.values, incoming);\n\tif (instanceSolve !== false) return true;\n\tstate.hasPendingChanges = true;\n\treturn false;\n}\n\n// Worth sending even unchanged: a dynamic list's options can change under the same selection, and\n// a file can be picked again after its parameter was cleared.\nconst RESEND_UNCHANGED = new Set(['dynamicValueList', 'file']);\n\n/**\n * Whether setting `value` would solve again for nothing: it is what the last solve sent and what\n * the session holds, and nothing failed or aborted since (`lastSent` is null then). A field that\n * reports on input and again on blur would otherwise solve twice, and Grasshopper re-applies every\n * dynamic list on each update, so the repeat is a real solve. Objects never count as repeats.\n */\nexport function isRepeatOfLastSolve(\n\tschema: UISchema,\n\tlastSent: Record<string, unknown> | null,\n\tvalues: Record<string, unknown>,\n\tid: string,\n\tvalue: unknown\n): boolean {\n\tif (!lastSent || !(id in lastSent)) return false;\n\tif (value !== null && typeof value === 'object') return false;\n\tif (!Object.is(lastSent[id], value) || !Object.is(values[id], value)) return false;\n\tconst paramType = schema.inputs.find((input) => input.id === id)?.paramType;\n\treturn !RESEND_UNCHANGED.has(paramType ?? '');\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,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (Object.hasOwn(values, input.id)) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\n/** An input was edited after `sent` was dispatched, so a result for `sent` does not cover it. */\nexport function inputsChangedSince(\n\tschema: UISchema,\n\tvalues: Record<string, unknown>,\n\tsent: Record<string, unknown> | null\n): boolean {\n\tif (!sent) return false;\n\tconst current = pickInputValues(schema, values);\n\treturn Object.keys(current).some(\n\t\t(id) => !Object.hasOwn(sent, id) || !Object.is(current[id], sent[id])\n\t);\n}\n\n/**\n * `editedSinceSent` keeps the dirty flag for a value changed while the solve ran: its result\n * answers the older value, and clearing the flag would hide the Calculate prompt on a stale answer.\n */\nexport function applySolveResult(\n\tstate: SolveSessionState,\n\tresult: SolveResult,\n\teditedSinceSent: boolean\n): void {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.diagnostics = result.diagnostics ?? [];\n\tstate.blocked = result.blocked ?? false;\n\tstate.unsolved = result.aborted ? 'aborted' : null;\n\n\t// The solve is finished either way — Grasshopper cannot be paused mid-solution — so what is\n\t// held back here is the *result*, not the computation. Everything the solve produced is kept\n\t// intact in `heldResult` and released verbatim by `acknowledge()`.\n\t//\n\t// A blocked solve is applied at once: there is no result to weigh, and the previous one must\n\t// not stay on screen looking like the answer to the current inputs. The caller blanks the\n\t// outputs (see `blankOutputs`); the empty mesh list clears the viewer.\n\tif (!state.blocked && needsAcknowledgement(state.diagnostics)) {\n\t\tstate.heldResult = result;\n\t\tstate.hasPendingChanges &&= editedSinceSent;\n\t\treturn;\n\t}\n\n\tstate.heldResult = null;\n\tcommitSolveResult(state, result, editedSinceSent);\n}\n\n/**\n * Every schema output set to null. A blocked solve reports `outputs: {}`, and merging that into\n * `values` would leave the previous solve's outputs behind.\n */\nexport function blankOutputs(schema: UISchema): Record<string, null> {\n\tconst blank: Record<string, null> = {};\n\tfor (const output of schema.outputs) blank[output.id] = null;\n\treturn blank;\n}\n\n/** Writes a result into the state. Shared by the clean path and by `acknowledge()`. */\nexport function commitSolveResult(\n\tstate: SolveSessionState,\n\tresult: SolveResult,\n\teditedSinceSent: boolean\n): void {\n\tstate.meshes = result.meshes ?? [];\n\tconst { meshes: _meshes, ...retained } = result;\n\tstate.lastResult = retained;\n\tObject.assign(state.values, result.outputs);\n\tstate.hasPendingChanges &&= editedSinceSent;\n\tstate.hasNeverSolved = false;\n}\n\n/** What `ensureSolved` has to do for the current inputs. */\nexport type EnsureSolvedStep = 'wait' | 'solve' | 'discarded' | 'blocked' | 'applied';\n\n/** `busy`: the driver is solving or has a solve queued. */\nexport function ensureSolvedStep(state: SolveSessionState, busy: boolean): EnsureSolvedStep {\n\tif (busy || state.heldResult) return 'wait';\n\tif (state.hasPendingChanges) return 'solve';\n\t// Not re-solved: the same inputs would raise the same messages and ask the user again.\n\tif (state.unsolved === 'discarded') return 'discarded';\n\tif (state.unsolved || state.hasNeverSolved || state.error || !state.lastResult) return 'solve';\n\treturn state.blocked ? 'blocked' : 'applied';\n}\n\nexport type SolvePhase = 'idle' | 'solving' | 'review' | 'blocked';\n\n/**\n * One value for what a host should show. Solving wins: a new solve supersedes a held or refused\n * result. `isSolving` is passed in because the driver owns it.\n */\nexport function solvePhase(state: SolveSessionState, isSolving: boolean): SolvePhase {\n\tif (isSolving) return 'solving';\n\tif (state.heldResult) return 'review';\n\tif (state.blocked) return 'blocked';\n\treturn 'idle';\n}\n\n/**\n * Whether this solve's messages still need the user's acknowledgement.\n *\n * Only a Selva Message component gates. Grasshopper raises warnings constantly for reasons that\n * concern whoever is editing the definition, not whoever is using the app — a component\n * complaining about its own inputs is not something an end user can act on, and interrupting\n * them for it trains them to dismiss the one message that mattered.\n *\n * Every solve that gates asks again, by design — an acknowledgement covers the solve in front of\n * the user, never a future one. Under `instanceSolve` with a live warning that means a dialog\n * per solve, which is the intended checkpoint, not an oversight.\n *\n * A transport that reports no structure has no diagnostics, so nothing gates: the messages are\n * still shown, just without interrupting.\n */\nexport function needsAcknowledgement(diagnostics: SolveDiagnostic[]): boolean {\n\treturn diagnostics.some((d) => d.isGate && d.level !== 'remark');\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 { SolveDiagnostic, SolveResult } from '../../shared/solve-fn.js';\nimport type { SolveDriver } from '../drivers/driver.js';\nimport { EMPTY_LIVE_SOLVE, reduceLiveSolve, type LiveSolveState } from '../events/live-solve.js';\nimport {\n\tapplyLoadedValues,\n\tapplySolveResult,\n\tapplyValueChange,\n\tblankOutputs,\n\tcommitSolveResult,\n\tcreateInitialState,\n\tensureSolvedStep,\n\tinputsChangedSince,\n\tisRepeatOfLastSolve,\n\tpickInputValues,\n\tsolvePhase,\n\ttype SolvePhase,\n\ttype RetainedSolveResult\n} from './solve-session-core.js';\n\n/**\n * How a solve ended, once nothing about it can change any more: a held result counts only\n * after the user accepts or rejects it, and a result a queued solve is about to replace never\n * counts. One per solve that lands, whether a host asked for it or an input change did.\n */\nexport type SolveSettlement =\n\t| { status: 'applied'; result: RetainedSolveResult; diagnostics: SolveDiagnostic[] }\n\t/** A held result the user rejected or a newer solve replaced; the screen still shows the one before it. */\n\t| { status: 'discarded'; diagnostics: SolveDiagnostic[] }\n\t/** The definition refused the solve. */\n\t| { status: 'blocked'; diagnostics: SolveDiagnostic[] }\n\t/** Cancelled, or stopped in Grasshopper, before it produced a result. */\n\t| { status: 'aborted'; diagnostics: SolveDiagnostic[] }\n\t/** The transport failed or the solve ran past its deadline; `error` is what the user sees. */\n\t| { status: 'failed'; error: string };\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\t/**\n\t * The last solve's messages with their level, source and `isGate`. Richer than the two\n\t * string arrays above and the list a message UI should render; they remain for hosts that\n\t * only want the text. Empty from a transport that reports no structure.\n\t */\n\treadonly diagnostics: SolveDiagnostic[];\n\t/**\n\t * The definition refused the last solve, or it was aborted. Applied at once, not held: the\n\t * outputs are blanked and the viewer cleared, and `computeErrors` holds the reason.\n\t */\n\treadonly blocked: boolean;\n\t/**\n\t * The last solve raised messages awaiting the user's confirmation, and its result is held\n\t * back until then. A host shows the messages and calls `acknowledge()` to release them.\n\t * Never true for a blocked solve.\n\t */\n\treadonly awaitingAck: boolean;\n\t/**\n\t * Releases a held result into the session. No-op when nothing is held. The solve already\n\t * ran — this applies what it produced, it does not re-solve.\n\t */\n\tacknowledge(): void;\n\t/**\n\t * Drops a held result instead of applying it. The viewer keeps what it was showing. No-op\n\t * when nothing is held, and never re-solves — like `acknowledge()`, it only decides the\n\t * fate of a result that already exists.\n\t */\n\tdiscard(): void;\n\t/**\n\t * What the running (or most recent) solve has said so far: its diagnostics, progress per\n\t * source, and how it ended. Reset when a solve with a new id starts, so a host renders \"what\n\t * this solve has said\", never a tail of the previous one. Stays empty when the driver has no\n\t * live channel.\n\t */\n\treadonly live: LiveSolveState;\n\t/**\n\t * Where the session is, as one value instead of four flags: `solving` while a solve runs,\n\t * `review` while a result waits for `acknowledge()`, `blocked` after a refused or aborted\n\t * solve, `idle` otherwise.\n\t */\n\treadonly phase: SolvePhase;\n\t/**\n\t * Asks the transport to stop the solve in flight. Cooperative on both paths — Grasshopper\n\t * stops at the next component boundary — so the result is a `solveEnded` (or nothing, if it\n\t * finished first), never an instant halt.\n\t */\n\tabort(): void;\n\treadonly meshes: unknown[];\n\t/**\n\t * What the viewer is currently showing, including the `source`/`values` pair a commit path\n\t * needs. Null before the first solve, and again after `rebuild()`. Survives `reportError`: a\n\t * failed solve leaves the last good geometry on screen, so a host committing what it sees\n\t * should still see what produced it.\n\t */\n\treadonly lastResult: RetainedSolveResult | null;\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\t/**\n\t * Solves the current inputs. Resolves with the settlement of the solve that lands, which is\n\t * a later one if the inputs change meanwhile: what is on screen when it resolves. Never\n\t * rejects; a failure is a status. A rebuild or dispose first resolves it as `aborted`.\n\t */\n\tsolve(): Promise<SolveSettlement>;\n\t/**\n\t * The result for the current inputs, solving only when it has to: right away from the result\n\t * on screen when nothing changed, after the running or held solve when there is one, else\n\t * after a new solve. For an action that needs a fresh result, such as an export. Inputs whose\n\t * result the user just discarded settle as `discarded` rather than ask them again.\n\t */\n\tensureSolved(): Promise<SolveSettlement>;\n\t/** Fires once per settled solve, the ones input changes start included. Returns the unsubscribe. */\n\tonSettled(listener: (settlement: SolveSettlement) => void): () => 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\treportAborted(): 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\t/**\n\t * Resets the driver, stops listening to its live events and drops every subscriber. Call when\n\t * the session is discarded: the event source can outlive it (the cloud stream belongs to the\n\t * tab), and would otherwise keep feeding a session nobody reads. Inert afterwards.\n\t */\n\tdispose(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\n/** One listener's bug must not cost the others their update, or strand a solve mid-landing. */\nfunction notifyAll<T>(listeners: Iterable<(value: T) => void>, value: T) {\n\tfor (const listener of [...listeners]) {\n\t\ttry {\n\t\t\tlistener(value);\n\t\t} catch (err) {\n\t\t\tconsole.error('[Solve/session] listener threw:', err);\n\t\t}\n\t}\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\t// The inputs the last dispatch sent; null after a failure or abort, so the same values retry.\n\tlet lastSent: Record<string, unknown> | null = null;\n\t// Survives a failure, unlike `lastSent`: a queued solve may be running on exactly these inputs.\n\tlet lastDispatched: Record<string, unknown> | null = null;\n\n\tconst state = createInitialState(currentSchema, args.scopeKey, readExternalValue);\n\tlet disposed = false;\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => notifyAll(listeners, undefined);\n\n\tconst settledListeners = new Set<(settlement: SolveSettlement) => void>();\n\tlet waiters: ((settlement: SolveSettlement) => void)[] = [];\n\tconst ABORTED: SolveSettlement = { status: 'aborted', diagnostics: [] };\n\n\tfunction nextSettlement(): Promise<SolveSettlement> {\n\t\tif (disposed) return Promise.resolve(ABORTED);\n\t\treturn new Promise((resolve) => waiters.push(resolve));\n\t}\n\n\tfunction settle(settlement: SolveSettlement) {\n\t\tconst waiting = waiters;\n\t\twaiters = [];\n\t\twaiting.forEach((resolve) => resolve(settlement));\n\t\tnotifyAll(settledListeners, settlement);\n\t}\n\n\t/** Resolves waiting callers without telling listeners: no solve ended, the session moved on. */\n\tfunction abandon() {\n\t\tconst waiting = waiters;\n\t\twaiters = [];\n\t\twaiting.forEach((resolve) => resolve(ABORTED));\n\t}\n\n\tfunction settlementOf(result: SolveResult): SolveSettlement {\n\t\tconst diagnostics = state.diagnostics;\n\t\tif (result.aborted) return { status: 'aborted', diagnostics };\n\t\tif (result.blocked) return { status: 'blocked', diagnostics };\n\t\treturn { status: 'applied', result: state.lastResult!, diagnostics };\n\t}\n\n\t/** One solve ended. Tells subscribers, then settles unless a queued solve is about to replace it. */\n\tfunction land(settlement: () => SolveSettlement) {\n\t\temit();\n\t\tif (!args.driver.hasQueued) settle(settlement());\n\t}\n\n\t/**\n\t * Waiters stay: `solve()` resolves with what lands, and that is now the newer solve. Only\n\t * listeners hear of it, so the dropped solve still settles exactly once.\n\t */\n\tfunction dropHeld() {\n\t\tif (!state.heldResult) return;\n\t\tstate.heldResult = null;\n\t\tnotifyAll(settledListeners, { status: 'discarded', diagnostics: state.diagnostics });\n\t}\n\n\tfunction editedSinceSent() {\n\t\treturn inputsChangedSince(currentSchema, state.values, lastDispatched);\n\t}\n\n\tlet live: LiveSolveState = EMPTY_LIVE_SOLVE;\n\t// A driver with no live channel never calls this; nothing else in the session depends on it.\n\tconst stopEvents = args.driver.onEvent?.((event) => {\n\t\tconst next = reduceLiveSolve(live, event);\n\t\tif (next === live) return;\n\t\tlive = next;\n\t\temit();\n\t});\n\n\tfunction solve(): Promise<SolveSettlement> {\n\t\tconst settled = nextSettlement();\n\t\tdispatch();\n\t\treturn settled;\n\t}\n\n\tfunction dispatch() {\n\t\tif (disposed) return;\n\t\tdropHeld();\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\tlastSent = { ...pickInputValues(currentSchema, state.values) };\n\t\tlastDispatched = lastSent;\n\t\targs.driver.solve(lastSent);\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 diagnostics() {\n\t\t\treturn state.diagnostics;\n\t\t},\n\t\tget blocked() {\n\t\t\treturn state.blocked;\n\t\t},\n\t\tget awaitingAck() {\n\t\t\treturn state.heldResult !== null;\n\t\t},\n\n\t\tacknowledge() {\n\t\t\tconst held = state.heldResult;\n\t\t\tif (!held) return;\n\t\t\tstate.heldResult = null;\n\t\t\tcommitSolveResult(state, held, editedSinceSent());\n\t\t\tland(() => settlementOf(held));\n\t\t},\n\n\t\tdiscard() {\n\t\t\tif (!state.heldResult) return;\n\t\t\tstate.heldResult = null;\n\t\t\tstate.unsolved = 'discarded';\n\t\t\t// `values`, `meshes` and `lastResult` are untouched: the viewer keeps showing the last\n\t\t\t// result the user accepted. The messages stay on `computeErrors`/`computeWarnings` so\n\t\t\t// the footer still explains why nothing changed.\n\t\t\tland(() => ({ status: 'discarded', diagnostics: state.diagnostics }));\n\t\t},\n\t\tget live() {\n\t\t\treturn live;\n\t\t},\n\t\tget phase() {\n\t\t\treturn solvePhase(state, args.driver.isSolving);\n\t\t},\n\t\tabort() {\n\t\t\targs.driver.cancel();\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget lastResult() {\n\t\t\treturn state.lastResult;\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\tif (!forceSolve && isRepeatOfLastSolve(currentSchema, lastSent, state.values, id, value))\n\t\t\t\treturn;\n\t\t\tif (applyValueChange(state, id, value, currentSchema.instanceSolve, forceSolve)) {\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\n\t\tensureSolved() {\n\t\t\tconst busy = args.driver.isSolving || args.driver.hasQueued === true;\n\t\t\tconst step = ensureSolvedStep(state, busy);\n\t\t\tif (step === 'wait') return nextSettlement();\n\t\t\tif (step === 'solve') return solve();\n\t\t\tif (step === 'applied') {\n\t\t\t\treturn Promise.resolve({\n\t\t\t\t\tstatus: step,\n\t\t\t\t\tresult: state.lastResult!,\n\t\t\t\t\tdiagnostics: state.diagnostics\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn Promise.resolve({ status: step, diagnostics: state.diagnostics });\n\t\t},\n\n\t\tonSettled(listener) {\n\t\t\tsettledListeners.add(listener);\n\t\t\treturn () => settledListeners.delete(listener);\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tif (applyLoadedValues(state, incoming, currentSchema.instanceSolve)) {\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\trebuild(schema, scopeKey) {\n\t\t\tif (disposed) return;\n\t\t\t// A caller waiting on the old definition's solve would otherwise get the new one's.\n\t\t\tabandon();\n\t\t\tcurrentSchema = schema;\n\t\t\tlastSent = null;\n\t\t\tlastDispatched = null;\n\t\t\t// The old definition's running solve and cached results must not reach the new one,\n\t\t\t// even where the two share input ids.\n\t\t\targs.driver.reset?.();\n\t\t\tObject.assign(state, createInitialState(schema, scopeKey, readExternalValue));\n\t\t\tif (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\tconst rejected = result.blocked || result.aborted;\n\t\t\t// An aborted solve answered nothing, so whatever was dirty stays dirty.\n\t\t\tconst edited = result.aborted === true || editedSinceSent();\n\t\t\tif (result.aborted) lastSent = null;\n\t\t\t// `finalizeResult` already dropped a rejected solve's outputs; this also nulls the\n\t\t\t// ones still in `values` from the previous solve, which only the session can name.\n\t\t\tapplySolveResult(\n\t\t\t\tstate,\n\t\t\t\trejected ? { ...result, outputs: blankOutputs(currentSchema), meshes: [] } : result,\n\t\t\t\tedited\n\t\t\t);\n\t\t\t// A result a queued solve replaces never asks the user anything, and never settles.\n\t\t\tif (args.driver.hasQueued) state.heldResult = null;\n\t\t\t// A held result settles on acknowledge() or discard().\n\t\t\tif (state.heldResult) return emit();\n\t\t\tland(() => settlementOf(result));\n\t\t},\n\n\t\treportError(message) {\n\t\t\tdropHeld();\n\t\t\tlastSent = null;\n\t\t\tstate.error = message;\n\t\t\tstate.unsolved = null;\n\t\t\tland(() => ({ status: 'failed', error: message }));\n\t\t},\n\n\t\treportAborted() {\n\t\t\tlastSent = null;\n\t\t\tstate.unsolved = 'aborted';\n\t\t\tland(() => ABORTED);\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\n\t\tdispose() {\n\t\t\tdisposed = true;\n\t\t\tstopEvents?.();\n\t\t\tlisteners.clear();\n\t\t\tsettledListeners.clear();\n\t\t\targs.driver.reset?.();\n\t\t\tabandon();\n\t\t}\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\t/**\n\t * How long one run may take before it is aborted (ms). Required and deliberately\n\t * undefaulted: this is a generic throttle, so it has no basis for guessing what\n\t * the caller's work costs. Callers driving a Selva solve pass the deadline the\n\t * server enforces (the compute server's own timeout) — a shorter local guess would abort\n\t * solves the server would have finished.\n\t */\n\trunDeadlineMs: 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/** `run` must settle once its signal fires: the slot frees only then. */\nexport function createAsyncThrottle<T>(\n\trun: (values: T, signal: AbortSignal) => Promise<void>,\n\toptions: AsyncThrottleOptions\n): AsyncThrottle<T> {\n\tconst { runDeadlineMs, 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\t// A host's bug must not strand `isRunning`, or throw out of `finally` below.\n\t\ttry {\n\t\t\tonChange?.();\n\t\t} catch (err) {\n\t\t\tconsole.error('[Solve/throttle] onChange threw:', err);\n\t\t}\n\t}\n\n\tasync function execute(values: T) {\n\t\tconst controller = new AbortController();\n\t\tcurrentAbortController = controller;\n\t\tconst deadlineId = setTimeout(() => {\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${runDeadlineMs}ms — aborting`);\n\t\t\t// The reason tells `run` a timeout from a cancel: one is a failure, the other not.\n\t\t\tcontroller.abort(new DOMException('Run deadline exceeded', 'TimeoutError'));\n\t\t}, runDeadlineMs);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, controller.signal);\n\t\t} catch (err) {\n\t\t\t// `execute` is called fire-and-forget, so re-throwing would be an unhandled rejection.\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(deadlineId);\n\t\t\t// Before `setRunning`: a subscriber can start the next run from inside it, and that\n\t\t\t// run's controller must survive this cleanup.\n\t\t\tif (currentAbortController === controller) currentAbortController = null;\n\t\t\tsetRunning(false);\n\n\t\t\tif (pendingValues !== null && !isRunning) {\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// eslint-disable-next-line no-console -- fires per frame during a slider scrub\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\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\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. Otherwise matches what\n * `JSON.stringify` sends: `toJSON` is honoured and an `undefined` property is absent, which a\n * server treats differently from `null`.\n *\n * Throws on a `bigint`, as `JSON.stringify` does.\n */\nexport function stableInputKey(input: unknown): string {\n\tconst hasToJSON =\n\t\tinput !== null &&\n\t\ttypeof input === 'object' &&\n\t\ttypeof (input as { toJSON?: unknown }).toJSON === 'function';\n\tconst value = hasToJSON ? (input as { toJSON(): unknown }).toJSON() : input;\n\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(stableInputKey).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj)\n\t\t.filter((k) => obj[k] !== undefined)\n\t\t.sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${stableInputKey(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\t/** Bytes the meshes hold. Without it the memo caps by entry count only. */\n\tbyteSize?(meshes: TMesh[]): number;\n}\n\n/**\n * Every entry is a full copy of its geometry, next to the copy on screen. A 13.6-million-triangle\n * scene is about 390 MB per copy, so a count cap alone lets 16 entries reach 6 GB.\n */\nexport const DEFAULT_MEMO_MAX_BYTES = 256 * 1024 * 1024;\n\nexport interface SolveMemoOptions<TMesh> {\n\t/** Entry cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */\n\tmax?: number;\n\t/** Mesh bytes across all entries, measured by `meshPolicy.byteSize`. A result larger than this is not memoized. */\n\tmaxBytes?: 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, maxBytes = DEFAULT_MEMO_MAX_BYTES, meshPolicy } = options;\n\tconst entries = new Map<string, { result: SolveResult<TMesh>; bytes: number }>();\n\tlet totalBytes = 0;\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\ttotalBytes -= entry.bytes;\n\t\trelease(entry.result);\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\t// eslint-disable-next-line no-console -- fires per frame during a slider scrub\n\t\t\tconsole.debug(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);\n\t\t\treturn copy(hit.result);\n\t\t},\n\t\tset(values, result) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tevict(key);\n\t\t\tconst bytes = result.meshes?.length ? (meshPolicy?.byteSize?.(result.meshes) ?? 0) : 0;\n\t\t\t// Measured before cloning, so an oversized result never costs its copy.\n\t\t\tif (bytes > maxBytes) {\n\t\t\t\t// eslint-disable-next-line no-console -- once per solve of an oversized scene\n\t\t\t\tconsole.debug(\n\t\t\t\t\t`[Solve/memo] not memoized: ${(bytes / 1048576).toFixed(0)} MB of meshes exceeds ${(maxBytes / 1048576).toFixed(0)} MB`\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tentries.set(key, { result: copy(result), bytes });\n\t\t\ttotalBytes += bytes;\n\t\t\tfor (const oldest of entries.keys()) {\n\t\t\t\tif (entries.size <= max && totalBytes <= maxBytes) break;\n\t\t\t\tevict(oldest);\n\t\t\t}\n\t\t},\n\t\tclear() {\n\t\t\tentries.forEach((entry) => release(entry.result));\n\t\t\tentries.clear();\n\t\t\ttotalBytes = 0;\n\t\t}\n\t};\n}\n","import { createAsyncThrottle } from '../async-throttle.js';\nimport { createSolveMemo, type MeshPolicy } from '../solve-memo.js';\nimport { raceAbort } from '../../shared/abort.js';\nimport type { SolveFn, SolveResult } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveEventSource, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\t/** How long one solve may take before the client aborts it (ms). Pass the deadline the solve's server enforces so the two agree. */\n\tsolveDeadlineMs: number;\n\tonChange?: () => void;\n\t/** Mesh ownership policy for the result memo — see `MeshPolicy` (solve-memo.ts). */\n\tmeshPolicy?: MeshPolicy<TMesh>;\n\t/** Live events for the solves this driver runs; without it the session never sees any. */\n\tevents?: SolveEventSource;\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, solveDeadlineMs, onChange, events } = 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\t// Bumped by `reset`. A run from an older generation belongs to a definition that is gone: it\n\t// must neither fill the memo nor report into the session that replaced it.\n\tlet generation = 0;\n\n\t// A stopped run still ends in a report: a timeout is a failure the user should read, a\n\t// cancel is not. Either way a host awaiting the solve hears back.\n\tfunction reportStopped(signal: AbortSignal) {\n\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\tconsole.debug('[Solve/driver] solve stopped (cancelled or timed out)');\n\t\tconst reason = signal.reason as { name?: string } | undefined;\n\t\tif (reason?.name === 'TimeoutError') {\n\t\t\tgetReporter().reportError(\n\t\t\t\t`The solve took longer than ${Math.round(solveDeadlineMs / 1000)} s and was stopped.`\n\t\t\t);\n\t\t} else {\n\t\t\tgetReporter().reportAborted();\n\t\t}\n\t}\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(\n\t\tasync (values, signal) => {\n\t\t\tconst startedIn = generation;\n\t\t\tlet result: SolveResult<TMesh>;\n\t\t\t// Only the solve is inside the `try`. A report that throws is a host bug, and landing\n\t\t\t// here would settle a good solve a second time, as failed.\n\t\t\ttry {\n\t\t\t\tconst cached = memo.get(values);\n\t\t\t\tif (cached !== undefined) {\n\t\t\t\t\tresult = cached;\n\t\t\t\t} else {\n\t\t\t\t\t// Raced: a `SolveFn` that ignores its signal must not pin the throttle's slot past\n\t\t\t\t\t// the deadline. Its late settlement is dropped.\n\t\t\t\t\tconst solved = await raceAbort(onSolve(values, signal), signal);\n\t\t\t\t\t// Stamped here rather than in the SolveFn: a memo hit never calls it, so values\n\t\t\t\t\t// captured inside would pair the served result with whatever solved last.\n\t\t\t\t\tresult = { ...solved, values };\n\t\t\t\t\t// An abort is not an answer for these inputs, so the same values must solve again.\n\t\t\t\t\tif (!result.aborted && startedIn === generation) memo.set(values, result);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tif (startedIn !== generation) return;\n\t\t\t\tif (signal.aborted) return reportStopped(signal);\n\t\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\t\treturn getReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t\t}\n\t\t\tif (startedIn !== generation) return;\n\t\t\tif (signal.aborted) return reportStopped(signal);\n\t\t\tgetReporter().report(result);\n\t\t},\n\t\t// The throttle is generic, so the solve deadline is what bounds one of its runs.\n\t\t{ runDeadlineMs: solveDeadlineMs, onChange }\n\t);\n\n\tfunction cancel() {\n\t\t// Dropping the fetch alone stops the Compute child: the server closes the solve and the\n\t\t// plugin's next callback gets a 410. The explicit cancel is for a server that keeps the\n\t\t// solve open, and loses the race harmlessly (404) when the fetch abort closes it first.\n\t\tthrottle.cancel();\n\t\tevents?.cancelCurrent?.();\n\t}\n\n\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel,\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tget hasQueued() {\n\t\t\treturn throttle.hasPending;\n\t\t},\n\t\treset() {\n\t\t\tgeneration += 1;\n\t\t\tcancel();\n\t\t\tmemo.clear();\n\t\t},\n\t\t...(events ? { onEvent: (listener) => events.subscribe(listener) } : {})\n\t} satisfies SolveDriver;\n}\n","// The cloud half of the live channel: one SSE stream per tab, carrying every solve this tab\n// runs. The stream id is minted here rather than by the server so a solve request can name it\n// before the stream has even connected.\n//\n// Opened when created, not on first subscribe: the server binds a solve to the stream only if\n// the stream is already registered, and a lazily opened one was rarely connected by the time\n// the first solve ran.\n\nimport type { SolveEvent } from '@selvajs/schemas';\nimport type { SolveEventSource } from '../drivers/driver.js';\n\nconst MIN_RECONNECT_MS = 1_000;\nconst MAX_RECONNECT_MS = 30_000;\n\nexport interface SolveEventStreamOptions {\n\t/** The SSE endpoint, e.g. `/api/v1/solve-events`. `streamId` is appended as a query param. */\n\tendpoint: string;\n\t/** Builds the cancel URL for a solve, e.g. `(id) => \\`/api/v1/solve/${id}/cancel\\``. */\n\tcancelEndpoint: (solveId: string) => string;\n\tfetch?: typeof fetch;\n}\n\nexport interface SolveEventStream extends SolveEventSource {\n\t/** Send this with every solve request so the server can route that solve's events here. */\n\treadonly streamId: string;\n\t/** The solve the server last reported as started and not yet ended; null between solves. */\n\treadonly currentSolveId: string | null;\n\t/** Final: the stream does not reopen, and a later subscriber hears nothing. */\n\tclose(): void;\n}\n\nfunction mintStreamId(): string {\n\tif (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {\n\t\treturn crypto.randomUUID().replace(/-/g, '');\n\t}\n\treturn Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');\n}\n\nexport function createSolveEventStream(options: SolveEventStreamOptions): SolveEventStream {\n\tconst streamId = mintStreamId();\n\tconst listeners = new Set<(event: SolveEvent) => void>();\n\tconst doFetch = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args));\n\tlet source: EventSource | null = null;\n\tlet currentSolveId: string | null = null;\n\tlet reconnectMs = MIN_RECONNECT_MS;\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n\tfunction open() {\n\t\tif (source || typeof EventSource === 'undefined') return;\n\t\tconst url = `${options.endpoint}${options.endpoint.includes('?') ? '&' : '?'}streamId=${streamId}`;\n\t\tconst opened = new EventSource(url);\n\t\tsource = opened;\n\t\topened.addEventListener('open', () => {\n\t\t\treconnectMs = MIN_RECONNECT_MS;\n\t\t});\n\t\topened.addEventListener('solve', (raw) => {\n\t\t\tlet event: SolveEvent;\n\t\t\ttry {\n\t\t\t\tevent = JSON.parse((raw as MessageEvent<string>).data);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.type === 'solveStarted') currentSolveId = event.solveId;\n\t\t\telse if (event.type === 'solveEnded' && currentSolveId === event.solveId)\n\t\t\t\tcurrentSolveId = null;\n\t\t\tfor (const listener of listeners) listener(event);\n\t\t});\n\t\t// EventSource reconnects after a dropped connection on its own, but a non-200 closes it for\n\t\t// good: one 502 during a deploy would end progress and cancel until a reload.\n\t\topened.addEventListener('error', () => {\n\t\t\tif (opened.readyState !== EventSource.CLOSED || source !== opened) return;\n\t\t\tsource = null;\n\t\t\tcurrentSolveId = null;\n\t\t\treconnectTimer = setTimeout(open, reconnectMs);\n\t\t\treconnectMs = Math.min(reconnectMs * 2, MAX_RECONNECT_MS);\n\t\t});\n\t}\n\n\topen();\n\n\treturn {\n\t\tstreamId,\n\t\tget currentSolveId() {\n\t\t\treturn currentSolveId;\n\t\t},\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tcancelCurrent() {\n\t\t\tconst solveId = currentSolveId;\n\t\t\tif (!solveId) return;\n\t\t\t// Fire-and-forget: the answer arrives as `solveEnded` on the stream, or not at all if\n\t\t\t// the solve finished first, and either way there is nothing to await here.\n\t\t\tvoid doFetch(options.cancelEndpoint(solveId), { method: 'POST', keepalive: true }).catch(\n\t\t\t\t() => {}\n\t\t\t);\n\t\t},\n\t\tclose() {\n\t\t\tif (reconnectTimer) clearTimeout(reconnectTimer);\n\t\t\tsource?.close();\n\t\t\tsource = null;\n\t\t\tlisteners.clear();\n\t\t\tcurrentSolveId = null;\n\t\t}\n\t};\n}\n","/**\n * A ready-made `SolveFn` that POSTs to a `/api/compute`-shaped endpoint (the wire contract\n * `SolveEngine` produces) and unpacks the result, with 429 cooldown, session-expiry detection,\n * a bad-response guard, and abort handling at every await.\n *\n * Mesh decoding is NOT built in: this package stays renderer-agnostic, so a viewer-enabled\n * consumer supplies `meshes.extract` (typically `getThreeObjectsFromComputeResponse` from\n * `@selvajs/visualization/parse`).\n */\n\nimport {\n\tGrasshopperResponseProcessor,\n\ttype GrasshopperComputeResponse\n} from '@selvajs/compute/grasshopper';\nimport type { SolveFn, SolveResult } from '../shared/solve-fn.js';\nimport { decodeOutcome, finalizeResult, outcomeFromComputeMessages } from '../shared/outcome.js';\n\nexport interface ComputeFetchSolveFnOptions<TMesh = 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`. A stored definition's server ignores them. */\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/** The tab's live-event stream id (`createSolveEventStream().streamId`), so the server routes this solve's events to it. */\n\tstreamId?: () => string | null | undefined;\n\t/** Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`. */\n\tmeshes?: {\n\t\textract: (\n\t\t\tresponse: GrasshopperComputeResponse,\n\t\t\topts: { debug: boolean; signal: AbortSignal }\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 and on a solve refused during its cooldown, before the rate-limit Error. */\n\tonRateLimited?: (retryAfterSeconds: number) => void;\n\t/** Called on a 401 or a login-page redirect, before the \"sign in again\" Error. */\n\tonSessionExpired?: () => void;\n\t/** Where the server redirects an expired session. Default `/login`. */\n\tloginPath?: string;\n}\n\n/** Applied to a 429 that names no wait. */\nconst DEFAULT_RETRY_AFTER_S = 5;\n/** A server's hint, not a lock: past this the client tries again anyway. */\nconst MAX_RETRY_AFTER_S = 3600;\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\nfunction rateLimitedError(retryAfterSeconds: number): Error {\n\treturn new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);\n}\n\nfunction sessionExpiredError(): Error {\n\treturn 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\n/** `Retry-After` in either RFC form. Null when it names no usable wait. */\nfunction parseRetryAfter(value: string): number | null {\n\tif (!value.trim()) return null;\n\tconst seconds = Number(value);\n\tif (!Number.isNaN(seconds)) return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;\n\tconst at = Date.parse(value);\n\treturn Number.isNaN(at) ? null : Math.max(0, (at - Date.now()) / 1000);\n}\n\nfunction readRetryAfter(header: string | null, bodyHint: unknown): number | null {\n\tconst seconds =\n\t\t(header === null ? null : parseRetryAfter(header)) ??\n\t\t(typeof bodyHint === 'number' ? parseRetryAfter(String(bodyHint)) : null);\n\treturn seconds === null ? null : Math.min(seconds, MAX_RETRY_AFTER_S);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction parseErrorBody(text: string): { message?: string; retryAfter?: unknown } {\n\ttry {\n\t\tconst body: unknown = JSON.parse(text);\n\t\tif (!isPlainObject(body)) return {};\n\t\treturn {\n\t\t\t...(typeof body.message === 'string' ? { message: body.message } : {}),\n\t\t\tretryAfter: body.retryAfter\n\t\t};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\nexport function createComputeFetchSolveFn<TMesh = unknown>(\n\topts: ComputeFetchSolveFnOptions<TMesh>\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 cooldownUntil = 0;\n\n\tfunction rateLimited(retryAfterSeconds: number): Error {\n\t\topts.onRateLimited?.(retryAfterSeconds);\n\t\treturn rateLimitedError(retryAfterSeconds);\n\t}\n\n\treturn async (values, signal): Promise<SolveResult<TMesh, GrasshopperComputeResponse>> => {\n\t\tconst remainingMs = cooldownUntil - Date.now();\n\t\tif (remainingMs > 0) throw rateLimited(remainingMs / 1000);\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\t\tconst streamId = opts.streamId?.();\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\t...(streamId ? { streamId } : {})\n\t\t});\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\t// Also how a redirect to a cross-origin sign-in looks from here, so an expired session\n\t\t\t// stays a possibility.\n\t\t\tthrow new Error(\n\t\t\t\t'Could not reach the server. Check your connection, or reload the page if your session expired.',\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 =\n\t\t\tres.redirected && new URL(res.url).pathname === (opts.loginPath ?? '/login');\n\t\tif (res.status === 401 || redirectedToLogin) {\n\t\t\topts.onSessionExpired?.();\n\t\t\tthrow sessionExpiredError();\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tconst errorBody = await res.text().catch(() => '');\n\t\t\tconst { message, retryAfter: bodyHint } = parseErrorBody(errorBody);\n\t\t\tif (errorBody && message === undefined) {\n\t\t\t\tlog(`[Compute] unreadable error body (HTTP ${res.status}, ${errorBody.length} chars)`);\n\t\t\t}\n\t\t\t// Any status may ask for a wait: a shed 503 says the same thing as a 429, and ignoring it\n\t\t\t// would hit a saturated server hardest at the moment it asks clients to back off.\n\t\t\tconst retryAfter = readRetryAfter(res.headers.get('Retry-After'), bodyHint);\n\t\t\tif (retryAfter !== null) cooldownUntil = Date.now() + retryAfter * 1000;\n\t\t\tif (res.status === 503 || res.status === 504) {\n\t\t\t\t// The server names the failure class (unreachable, bad API key,\n\t\t\t\t// rate-limited, timed out) — show it rather than a blanket \"offline\".\n\t\t\t\tthrow new Error(\n\t\t\t\t\tmessage || 'Compute server is offline or unreachable. Please try again later.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (res.status === 429) {\n\t\t\t\tconst seconds = retryAfter ?? DEFAULT_RETRY_AFTER_S;\n\t\t\t\tcooldownUntil = Date.now() + seconds * 1000;\n\t\t\t\tthrow rateLimited(seconds);\n\t\t\t}\n\t\t\tthrow new Error(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\tlet bodyText: string;\n\t\ttry {\n\t\t\tbodyText = await res.text();\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\tlog('[Compute] solve aborted mid-download — empty result discarded');\n\t\t\t\treturn { outputs: {} };\n\t\t\t}\n\t\t\tthrow new Error('The connection dropped while receiving the result. Please try again.', {\n\t\t\t\tcause: err\n\t\t\t});\n\t\t}\n\t\tconst downloadMs = performance.now() - downloadStart;\n\t\tconst bodyChars = bodyText.length;\n\n\t\tconst invalidResponse = (cause?: unknown) =>\n\t\t\tnew Error('Received an invalid response from the server. Reload the page and try again.', {\n\t\t\t\tcause\n\t\t\t});\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(bodyText);\n\t\t} catch (err) {\n\t\t\tthrow invalidResponse(err);\n\t\t}\n\t\tif (!isPlainObject(parsed)) throw invalidResponse();\n\t\tconst solved = parsed as unknown as GrasshopperComputeResponse;\n\t\tconst processor = new GrasshopperResponseProcessor(solved);\n\n\t\t// The plugin's own verdict when the fork returned one; otherwise recovered from the\n\t\t// markers in Compute's flattened strings (stock server, or a plugin that predates it).\n\t\tconst outcome =\n\t\t\tdecodeOutcome(solved.selva?.outcome) ??\n\t\t\toutcomeFromComputeMessages(solved.errors ?? [], solved.warnings ?? []);\n\t\t// Decided before parsing: an older fork still ships a rejected solve's outputs, and the\n\t\t// geometry alone can be megabytes the client would only throw away.\n\t\tconst rejected = outcome.blocked || outcome.aborted;\n\n\t\tconst meshes =\n\t\t\topts.meshes && !rejected\n\t\t\t\t? await opts.meshes.extract(processor.response, { debug, signal })\n\t\t\t\t: [];\n\n\t\tconst resultOutputs: Record<string, unknown> = {};\n\t\tfor (const o of rejected ? [] : 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 valuesBytes = JSON.stringify(values).length;\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 = bodyChars / (1024 * 1024);\n\t\t\tconst size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bodyChars / 1024).toFixed(0)} KB`;\n\t\t\tconst mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1000)).toFixed(1) : '∞';\n\t\t\tconst heapBytes = (performance as Performance & { memory?: { usedJSHeapSize: number } })\n\t\t\t\t.memory?.usedJSHeapSize;\n\t\t\tconst heap =\n\t\t\t\theapBytes !== undefined ? ` | heap=${(heapBytes / (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: serverTiming.rhino_cache\n\t\t\t\t\t\t? \"HIT — Rhino.Compute's result cache, solved nothing\"\n\t\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 finalizeResult({ outputs: resultOutputs, meshes, source: solved }, outcome);\n\t};\n}\n"],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ interface SolveLimits {
|
|
|
19
19
|
* answers a stale pointer with `definition_not_cached`: one that doesn't returns empty geometry.
|
|
20
20
|
*/
|
|
21
21
|
reuseDefinitionCache: boolean;
|
|
22
|
+
/** The server's deployment (`ServerLimits.deploymentId`); another one may solve differently. */
|
|
23
|
+
deploymentId: string | null;
|
|
22
24
|
}
|
|
23
25
|
/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */
|
|
24
26
|
interface ResolvedServer {
|
package/dist/server.js
CHANGED
|
@@ -90,16 +90,18 @@ function solveLimitsFor(reported) {
|
|
|
90
90
|
if (!reported) return {
|
|
91
91
|
solveDeadlineMs: DEFAULT_SOLVE_DEADLINE_MS,
|
|
92
92
|
maxRequestBytes: null,
|
|
93
|
-
reuseDefinitionCache: false
|
|
93
|
+
reuseDefinitionCache: false,
|
|
94
|
+
deploymentId: null
|
|
94
95
|
};
|
|
95
96
|
return {
|
|
96
97
|
solveDeadlineMs: Math.min(reported.timeoutSeconds * 1e3 + DEADLINE_SLACK_MS, MAX_SOLVE_DEADLINE_MS),
|
|
97
98
|
maxRequestBytes: reported.maxRequestBytes,
|
|
98
|
-
reuseDefinitionCache: reported.signalsStaleDefinition
|
|
99
|
+
reuseDefinitionCache: reported.signalsStaleDefinition,
|
|
100
|
+
deploymentId: reported.deploymentId ?? null
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
function sameSolveLimits(a, b) {
|
|
102
|
-
return a.solveDeadlineMs === b.solveDeadlineMs && a.maxRequestBytes === b.maxRequestBytes && a.reuseDefinitionCache === b.reuseDefinitionCache;
|
|
104
|
+
return a.solveDeadlineMs === b.solveDeadlineMs && a.maxRequestBytes === b.maxRequestBytes && a.reuseDefinitionCache === b.reuseDefinitionCache && a.deploymentId === b.deploymentId;
|
|
103
105
|
}
|
|
104
106
|
// src/server/client-cache.ts
|
|
105
107
|
const DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;
|
|
@@ -109,6 +111,18 @@ const DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;
|
|
|
109
111
|
*/
|
|
110
112
|
const LIMITS_RECHECK_MS = 3e5;
|
|
111
113
|
/**
|
|
114
|
+
* How stale the server's `deploymentId` may be before a warm client whose solve cache could
|
|
115
|
+
* answer is handed out again. A cache hit never reaches the server, so riding on solve completion
|
|
116
|
+
* would let a redeployed plugin's old results be served until the next miss.
|
|
117
|
+
*/
|
|
118
|
+
const DEPLOYMENT_RECHECK_MS = 3e4;
|
|
119
|
+
/**
|
|
120
|
+
* Longest a request waits on that check. The proxy answers `/limits` itself, in milliseconds; an
|
|
121
|
+
* unreachable server must not hold a request that the cache could answer. The check finishes
|
|
122
|
+
* behind it and retires the client if the deployment changed.
|
|
123
|
+
*/
|
|
124
|
+
const DEPLOYMENT_CHECK_WAIT_MS = 1e3;
|
|
125
|
+
/**
|
|
112
126
|
* How long a failed build is remembered before the next `getClient` retries the
|
|
113
127
|
* handshake. Without this, every solve against a down server pays the full probe
|
|
114
128
|
* ladder again, so a user clicking Solve twice waits twice.
|
|
@@ -128,10 +142,12 @@ function describeSettledSolve(result) {
|
|
|
128
142
|
}
|
|
129
143
|
function createClientCache(config) {
|
|
130
144
|
const maxWarmComputeServers = config.maxWarmComputeServers ?? DEFAULT_MAX_WARM_COMPUTE_SERVERS;
|
|
145
|
+
const cacheEnabled = config.responseCacheMaxBytes > 0;
|
|
131
146
|
const cache = /* @__PURE__ */ new Map();
|
|
132
147
|
const pending = /* @__PURE__ */ new Map();
|
|
133
148
|
const failed = /* @__PURE__ */ new Map();
|
|
134
149
|
const draining = /* @__PURE__ */ new Set();
|
|
150
|
+
const deploymentChecks = /* @__PURE__ */ new WeakMap();
|
|
135
151
|
if (config.debug === "verbose") enableDebugLogging();
|
|
136
152
|
const debugLog = (message) => {
|
|
137
153
|
if (config.debug) config.onDebugLog?.(message);
|
|
@@ -176,22 +192,25 @@ function createClientCache(config) {
|
|
|
176
192
|
if (definitionGuid) clientConfig.headers = { "X-Selva-Definition": definitionGuid };
|
|
177
193
|
const client = await GrasshopperClient.create(clientConfig);
|
|
178
194
|
const probe = createConcurrencyProbe(client, debugLog);
|
|
179
|
-
const
|
|
180
|
-
const [
|
|
195
|
+
const readReported = async () => await Promise.resolve(client.serverStats?.getLimits?.()).catch(() => null) ?? null;
|
|
196
|
+
const [reported] = await Promise.all([readReported(), probe.refresh()]);
|
|
197
|
+
const limits = solveLimitsFor(reported);
|
|
181
198
|
debugLog(`[Compute/client-cache] server ${server.id}: deadline ${limits.solveDeadlineMs}ms, request cap ${limits.maxRequestBytes ?? "not reported"}, pointer reuse ${limits.reuseDefinitionCache}`);
|
|
182
199
|
let limitsCheckedAt = Date.now();
|
|
183
|
-
let
|
|
200
|
+
let recheck = null;
|
|
201
|
+
/** Re-reads the limits; true when they changed, which retires this client. */
|
|
202
|
+
const recheckLimits = () => recheck ??= readReported().then((next) => {
|
|
203
|
+
limitsCheckedAt = Date.now();
|
|
204
|
+
if (next === null && reported !== null) return false;
|
|
205
|
+
if (sameSolveLimits(solveLimitsFor(next), limits)) return false;
|
|
206
|
+
debugLog(`[Compute/client-cache] server ${server.id} limits changed — rebuilding`);
|
|
207
|
+
if (cache.get(server.id) === entry) evictEntry(server.id);
|
|
208
|
+
return true;
|
|
209
|
+
}).finally(() => {
|
|
210
|
+
recheck = null;
|
|
211
|
+
});
|
|
184
212
|
const recheckLimitsIfDue = () => {
|
|
185
|
-
if (
|
|
186
|
-
checkingLimits = true;
|
|
187
|
-
readLimits().then((next) => {
|
|
188
|
-
limitsCheckedAt = Date.now();
|
|
189
|
-
if (sameSolveLimits(next, limits)) return;
|
|
190
|
-
debugLog(`[Compute/client-cache] server ${server.id} limits changed — rebuilding`);
|
|
191
|
-
if (cache.get(server.id) === entry) evictEntry(server.id);
|
|
192
|
-
}).finally(() => {
|
|
193
|
-
checkingLimits = false;
|
|
194
|
-
});
|
|
213
|
+
if (Date.now() - limitsCheckedAt >= LIMITS_RECHECK_MS) recheckLimits();
|
|
195
214
|
};
|
|
196
215
|
const scheduler = client.createScheduler({
|
|
197
216
|
mode: "queue",
|
|
@@ -199,7 +218,7 @@ function createClientCache(config) {
|
|
|
199
218
|
maxQueueDepth: config.maxQueueDepth || void 0,
|
|
200
219
|
queueWaitMs: config.queueWaitMs || void 0,
|
|
201
220
|
timeoutMs: limits.solveDeadlineMs,
|
|
202
|
-
cache:
|
|
221
|
+
cache: cacheEnabled ? { maxBytes: config.responseCacheMaxBytes } : false,
|
|
203
222
|
reuseServerDefinitionCache: limits.reuseDefinitionCache,
|
|
204
223
|
onSettle: (_ctx, result) => {
|
|
205
224
|
queueMicrotask(() => finishDraining(entry));
|
|
@@ -216,6 +235,18 @@ function createClientCache(config) {
|
|
|
216
235
|
scheduler,
|
|
217
236
|
limits
|
|
218
237
|
};
|
|
238
|
+
deploymentChecks.set(entry, async () => {
|
|
239
|
+
if (Date.now() - limitsCheckedAt < DEPLOYMENT_RECHECK_MS) return true;
|
|
240
|
+
let timer;
|
|
241
|
+
const waited = new Promise((resolve) => {
|
|
242
|
+
timer = setTimeout(() => resolve(false), DEPLOYMENT_CHECK_WAIT_MS);
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
return !await Promise.race([recheckLimits(), waited]);
|
|
246
|
+
} finally {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
219
250
|
return entry;
|
|
220
251
|
}
|
|
221
252
|
function evictEntry(id) {
|
|
@@ -224,14 +255,15 @@ function createClientCache(config) {
|
|
|
224
255
|
cache.delete(id);
|
|
225
256
|
retire(entry);
|
|
226
257
|
}
|
|
227
|
-
|
|
258
|
+
const self = {
|
|
228
259
|
async getClient(server, opts) {
|
|
229
260
|
const key = server.id;
|
|
230
261
|
const existing = cache.get(key);
|
|
231
262
|
if (existing) {
|
|
232
263
|
cache.delete(key);
|
|
233
264
|
cache.set(key, existing);
|
|
234
|
-
return existing;
|
|
265
|
+
if (!cacheEnabled || await deploymentChecks.get(existing)?.() !== false) return existing;
|
|
266
|
+
return self.getClient(server, opts);
|
|
235
267
|
}
|
|
236
268
|
const inFlight = pending.get(key);
|
|
237
269
|
if (inFlight) return inFlight.promise;
|
|
@@ -325,6 +357,7 @@ function createClientCache(config) {
|
|
|
325
357
|
failed.clear();
|
|
326
358
|
}
|
|
327
359
|
};
|
|
360
|
+
return self;
|
|
328
361
|
}
|
|
329
362
|
// src/server/definition-byte-cache.ts
|
|
330
363
|
/**
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["undiciFetch"],"sources":["../src/server/concurrency-probe.ts","../src/server/long-fetch.ts","../src/server/server-limits.ts","../src/server/client-cache.ts","../src/server/definition-byte-cache.ts","../src/server/response-encoding.ts","../src/server/server-timing.ts","../src/server/solve-cache-single-flight.ts","../src/server/transform-input.ts","../src/server/solve-pipeline.ts","../src/server/solve-engine.ts"],"sourcesContent":["import type { GrasshopperClient } from '@selvajs/compute/grasshopper';\n\n/**\n * Fallback concurrency while the first probe is still in flight, and the value\n * kept whenever a probe fails or reports 0 — never send more than a single\n * solve to a server whose real capacity is unknown.\n */\nconst FALLBACK_CONCURRENCY = 1;\n\n/**\n * How stale the compute server's child count may get before a solve triggers a\n * re-read. Short enough that a resized pool is picked up within a few solves,\n * long enough that a busy server is probed rarely — the probe rides on solve\n * completion, so an idle server is never polled at all.\n */\nconst CONCURRENCY_PROBE_INTERVAL_MS = 5 * 60 * 1000;\n\nexport interface ConcurrencyProbe {\n\treadonly maxConcurrent: number;\n\t/** One blocking read at connect, so the first solves already use the right cap. */\n\trefresh(): Promise<void>;\n\t/** Piggybacks on real traffic: at most once per window, and never on the request path. */\n\trefreshIfDue(scheduler: { setMaxConcurrent(n: number): void }): void;\n}\n\n/**\n * rhino.compute hands out children in strict rotation and never reports \"busy\", so nothing\n * downstream enforces a concurrency cap — send more than it has children and requests silently\n * double up on one. Rhino.Compute is the only source of truth for its own child count, so this\n * always probes rather than trusting an operator-supplied guess.\n *\n * The count is not static: children exit on their own under `-idlespan`, and `/launch-child` +\n * `/recycle-children` resize the pool at runtime. So the probe re-runs after solves rather than\n * only at connect.\n */\nexport function createConcurrencyProbe(\n\tclient: Pick<GrasshopperClient, 'serverStats'>,\n\tdebugLog: (message: string) => void\n): ConcurrencyProbe {\n\tlet maxConcurrent = FALLBACK_CONCURRENCY;\n\tlet lastProbeAt = 0;\n\tlet probing = false;\n\n\t/** `initialize: false` keeps this from spawning children on an idle server, so a cold one reports 0. */\n\tasync function refresh(scheduler?: { setMaxConcurrent(n: number): void }) {\n\t\tif (probing) return;\n\t\tprobing = true;\n\t\ttry {\n\t\t\tconst children = await Promise.resolve(\n\t\t\t\tclient.serverStats?.getActiveChildren({ initialize: false })\n\t\t\t).catch(() => null);\n\t\t\tlastProbeAt = Date.now();\n\t\t\tconst next = !children || children <= 0 ? FALLBACK_CONCURRENCY : children;\n\t\t\tif (next === maxConcurrent) return;\n\t\t\tdebugLog(\n\t\t\t\t`[Compute/client-cache] concurrency ${maxConcurrent} → ${next} (server child count)`\n\t\t\t);\n\t\t\tmaxConcurrent = next;\n\t\t\tscheduler?.setMaxConcurrent(next);\n\t\t} finally {\n\t\t\tprobing = false;\n\t\t}\n\t}\n\n\treturn {\n\t\tget maxConcurrent() {\n\t\t\treturn maxConcurrent;\n\t\t},\n\t\trefresh: () => refresh(),\n\t\trefreshIfDue(scheduler) {\n\t\t\tif (Date.now() - lastProbeAt >= CONCURRENCY_PROBE_INTERVAL_MS) void refresh(scheduler);\n\t\t}\n\t};\n}\n","import { Agent, fetch as undiciFetch } from 'undici';\n\n/** undici's default `headersTimeout` and `bodyTimeout`, which `timeoutMs` can't extend. */\nconst UNDICI_DEFAULT_LIMIT_MS = 300_000;\n\n/** Both off, not raised: the scheduler's `timeoutMs` stays the one deadline on a solve. */\nconst unlimited = new Agent({ headersTimeout: 0, bodyTimeout: 0 });\n\n// npm undici's own fetch, not the global one: a dispatcher has to match the undici that calls it,\n// and Node's bundled copy is a different version.\n//\n// A call with no signal has no deadline of its own (`getIO` is one), so it keeps undici's limit:\n// switching the timeouts off for it would let a hung server hang a page load forever.\nconst unlimitedFetch = ((input: string, init?: { signal?: AbortSignal }) =>\n\tundiciFetch(input, {\n\t\t...init,\n\t\tsignal: init?.signal ?? AbortSignal.timeout(UNDICI_DEFAULT_LIMIT_MS),\n\t\tdispatcher: unlimited\n\t})) as unknown as typeof fetch;\n\n/** The fetch for a solve deadline: `undefined` means the global one reaches it. */\nexport function fetchForDeadline(deadlineMs: number): typeof fetch | undefined {\n\treturn deadlineMs >= UNDICI_DEFAULT_LIMIT_MS ? unlimitedFetch : undefined;\n}\n","import type { ServerLimits } from '@selvajs/compute/grasshopper';\n\n/** Rhino.Compute's own default `--timeout`, for a server that can't report its own. */\nexport const DEFAULT_SOLVE_DEADLINE_MS = 100_000;\n\n/**\n * Ceiling on any solve deadline, whatever the server reports. Bounds how long a request, its\n * live-event token and a delegated session stay open; past an hour a solve belongs in a job.\n */\nexport const MAX_SOLVE_DEADLINE_MS = 60 * 60_000;\n\n/** Selva waits this much past the server's timeout, so Compute's own error arrives first. */\nconst DEADLINE_SLACK_MS = 5_000;\n\n/** What Selva holds a solve to on one compute server. */\nexport interface SolveLimits {\n\tsolveDeadlineMs: number;\n\t/** `null` when the server didn't say: its own 413 is then the only check. */\n\tmaxRequestBytes: number | null;\n\t/**\n\t * Send a pointer to the server's cached definition instead of the `.gh`. Only on a server that\n\t * answers a stale pointer with `definition_not_cached`: one that doesn't returns empty geometry.\n\t */\n\treuseDefinitionCache: boolean;\n}\n\nexport function solveLimitsFor(reported: ServerLimits | null): SolveLimits {\n\tif (!reported) {\n\t\treturn {\n\t\t\tsolveDeadlineMs: DEFAULT_SOLVE_DEADLINE_MS,\n\t\t\tmaxRequestBytes: null,\n\t\t\treuseDefinitionCache: false\n\t\t};\n\t}\n\treturn {\n\t\tsolveDeadlineMs: Math.min(\n\t\t\treported.timeoutSeconds * 1000 + DEADLINE_SLACK_MS,\n\t\t\tMAX_SOLVE_DEADLINE_MS\n\t\t),\n\t\tmaxRequestBytes: reported.maxRequestBytes,\n\t\treuseDefinitionCache: reported.signalsStaleDefinition\n\t};\n}\n\nexport function sameSolveLimits(a: SolveLimits, b: SolveLimits): boolean {\n\treturn (\n\t\ta.solveDeadlineMs === b.solveDeadlineMs &&\n\t\ta.maxRequestBytes === b.maxRequestBytes &&\n\t\ta.reuseDefinitionCache === b.reuseDefinitionCache\n\t);\n}\n","import {\n\tGrasshopperClient,\n\ttype GrasshopperComputeConfig,\n\ttype SolveScheduler\n} from '@selvajs/compute/grasshopper';\nimport { ComputeError, enableDebugLogging, ErrorCodes } from '@selvajs/compute/core';\n\nimport { createConcurrencyProbe } from './concurrency-probe';\nimport { fetchForDeadline } from './long-fetch';\nimport {\n\tMAX_SOLVE_DEADLINE_MS,\n\tsameSolveLimits,\n\tsolveLimitsFor,\n\ttype SolveLimits\n} from './server-limits';\n\n// ============================================================================\n// Shared compute client cache\n// ============================================================================\n//\n// One warm `GrasshopperClient` (+ its `SolveScheduler`) per distinct compute\n// server, keyed by the server's `id`, never its URL — a rotated URL/apiKey\n// keeps the same key with stale connection details, so the config-write path\n// MUST call `evict(id)` when a server's config changes.\n//\n// Per-server LRU, not a single shared client: definitions can pin different\n// servers, so two definitions on two servers keep two warm clients, with\n// churn from one-off servers bounded by `maxWarmComputeServers`.\n//\n// Both hot paths share this cache — the solve endpoint uses the entry's\n// `scheduler`; the definition-viewer render path uses the entry's `client`\n// for `getIO` — so a definition that was just solved renders from the same\n// warm client instead of re-handshaking Rhino.Compute per page load.\n\n/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */\nexport interface ResolvedServer {\n\tid: string;\n\tserverUrl: string;\n\t/** Sent as the `RhinoComputeKey` header. */\n\tapiKey?: string;\n}\n\nexport interface CachedClient {\n\tclient: GrasshopperClient;\n\tscheduler: SolveScheduler;\n\t/** Read from the server when the client was built; a changed server rebuilds the client. */\n\tlimits: SolveLimits;\n}\n\n/**\n * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs\n * (`onDebugLog`), `'verbose'` also enables full lib-level request/response\n * dumps (incl. geometry) — a superset of `true`, not an independent setting.\n */\nexport type ClientCacheDebug = boolean | 'verbose';\n\n/** Config injected by the consuming app, so this module stays env-agnostic and testable. */\nexport interface ClientCacheConfig {\n\tcachesolve: boolean;\n\t/**\n\t * Max solves that may wait in the FIFO queue, excluding in-flight solves\n\t * (capped at `maxConcurrent`, itself driven by the server's probed child\n\t * count). `0` = unbounded (`ComputeLimits.computeMaxQueueDepth`); a full\n\t * queue rejects new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Max ms a solve may sit queued before executing; `0` = no deadline\n\t * (`ComputeLimits.computeQueueWaitMs`). Too long a wait rejects with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache. Applies per warm\n\t * client — worst-case heap is this × `maxWarmComputeServers`. `0` disables\n\t * the cache (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\t/**\n\t * Largest compute response body read; past it the download is cancelled (`too_large`).\n\t * Default: no limit.\n\t */\n\tmaxResponseBytes?: number;\n\tdebug: ClientCacheDebug;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. */\n\tmaxWarmComputeServers?: number;\n\t/** Sink for the debug lines. `onDebugLog` only fires when `debug` is not `false`. */\n\tonDebugLog?: (message: string) => void;\n}\n\nexport interface ClientCache {\n\t/**\n\t * Get (or create) the warm client + scheduler for a resolved compute server, keyed by its `id`.\n\t *\n\t * @throws {ComputeError} `ABORTED` when `evict` or `disposeAll` overtakes the build.\n\t */\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient>;\n\t/**\n\t * Drops the warm client for `id`, so the next request rebuilds, and disposes it once idle. A\n\t * build in flight for `id` is abandoned.\n\t */\n\tevict(id: string): void;\n\t/** Solve-cache counters summed across every warm client. Counters die with the client that owns them, so totals can fall over time. */\n\tsolveCacheStats(): SolveCacheStats;\n\t/**\n\t * Drop every retained solve result, keeping the warm clients (and their\n\t * connections and server-side definition pointers) intact. Nothing expires\n\t * these on its own — the byte budget is the only other pressure — so this is\n\t * the operator's release valve when a definition's inputs no longer describe\n\t * its output, e.g. it reads an external source that has since changed.\n\t */\n\tclearSolveCaches(): void;\n\t/** Dispose every warm client now, running solves included. Test seam / shutdown hook. */\n\tdisposeAll(): void;\n}\n\nexport interface SolveCacheStats {\n\twarmClients: number;\n\tentries: number;\n\tbytes: number;\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped under size/byte pressure (not replacement or manual clears). */\n\tevictions: number;\n}\n\nconst DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;\n\n/**\n * How often a warm client re-reads its server's limits, riding on solve completion like the\n * concurrency probe. An operator who changes `--timeout` sees it within this window.\n */\nconst LIMITS_RECHECK_MS = 5 * 60 * 1000;\n\n/**\n * How long a failed build is remembered before the next `getClient` retries the\n * handshake. Without this, every solve against a down server pays the full probe\n * ladder again, so a user clicking Solve twice waits twice.\n *\n * Short on purpose: this is a convenience for the rapid-retry case, not a\n * circuit breaker. An operator who starts the compute VM should not have to wait\n * out a long penalty window before the app notices.\n */\nconst FAILED_BUILD_TTL_MS = 5000;\n\n/**\n * A build failure worth replaying to the next caller, with the time it happened\n * so {@link FAILED_BUILD_TTL_MS} can expire it.\n */\ninterface FailedBuild {\n\terror: unknown;\n\tat: number;\n}\n\ninterface PendingBuild {\n\tpromise: Promise<CachedClient>;\n\t/** Set when the connection details this build started from stopped being true. */\n\ttoken: { stale: boolean };\n}\n\ninterface SettledSolve {\n\tfromCache?: boolean;\n\tdurationMs: number;\n\tdefinitionReuploaded?: boolean;\n\tresponse?: { errors?: unknown };\n}\n\nfunction describeSettledSolve(result: SettledSolve): string[] {\n\tconst verdict = result.fromCache\n\t\t? 'HIT — served from cache (no compute call)'\n\t\t: 'miss — went to Rhino.Compute';\n\tconst lines = [`[Compute/selva-cache] ${verdict} (${Math.round(result.durationMs)}ms)`];\n\tif (result.fromCache) return lines;\n\n\tlines.push(\n\t\tresult.definitionReuploaded === true\n\t\t\t? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'\n\t\t\t: result.definitionReuploaded === false\n\t\t\t\t? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'\n\t\t\t\t: '[Compute/def-cache] n/a — reuse disabled or non-reusable definition'\n\t);\n\tconst errors = result.response?.errors;\n\tif (Array.isArray(errors) && errors.length > 0) {\n\t\t// A count: solver text can quote user input, and logs are out of erasure's reach.\n\t\tlines.push(`[Compute/rhino-cache] ${errors.length} GH error(s); not cached by Rhino.Compute.`);\n\t}\n\treturn lines;\n}\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxWarmComputeServers = config.maxWarmComputeServers ?? DEFAULT_MAX_WARM_COMPUTE_SERVERS;\n\tconst cache = new Map<string, CachedClient>();\n\t// Builds in flight, keyed like `cache`. Concurrent `getClient` calls for the\n\t// same id must share ONE build: without this, both miss, both handshake, and\n\t// the loser's entry is overwritten in the map without ever being disposed.\n\tconst pending = new Map<string, PendingBuild>();\n\t// Recent build failures, keyed like `cache`. Cleared by `evict` so a config\n\t// fix takes effect immediately rather than waiting out the TTL.\n\tconst failed = new Map<string, FailedBuild>();\n\t// Out of `cache`, waiting for their running solves to finish.\n\tconst draining = new Set<CachedClient>();\n\n\tif (config.debug === 'verbose') enableDebugLogging();\n\n\tconst debugLog = (message: string) => {\n\t\tif (config.debug) config.onDebugLog?.(message);\n\t};\n\n\tfunction isIdle({ scheduler }: CachedClient) {\n\t\treturn scheduler.inFlightCount === 0 && scheduler.queueDepth === 0;\n\t}\n\n\t/** Disposing a busy scheduler aborts its solves, so a config write would fail them all. */\n\tfunction retire(entry: CachedClient) {\n\t\tif (isIdle(entry)) entry.scheduler.dispose();\n\t\telse draining.add(entry);\n\t}\n\n\tfunction finishDraining(entry: CachedClient) {\n\t\tif (!draining.has(entry) || !isIdle(entry)) return;\n\t\tdraining.delete(entry);\n\t\tentry.scheduler.dispose();\n\t}\n\n\tasync function build(server: ResolvedServer, definitionGuid?: string): Promise<CachedClient> {\n\t\tconst debugVerbose = config.debug === 'verbose';\n\t\t// The deadline comes from the server, read only once the client exists: pick the fetch\n\t\t// for the longest one. Calls without a signal keep undici's limits either way.\n\t\tconst longFetch = fetchForDeadline(MAX_SOLVE_DEADLINE_MS);\n\n\t\tconst clientConfig: GrasshopperComputeConfig = {\n\t\t\tserverUrl: server.serverUrl,\n\t\t\tapiKey: server.apiKey,\n\t\t\t// Lib-level debug is the verbose one; keep it on the verbose flag so the\n\t\t\t// concise cache logs can run without the noise.\n\t\t\tdebug: debugVerbose,\n\t\t\tcachesolve: config.cachesolve,\n\t\t\tmaxResponseBytes: config.maxResponseBytes,\n\t\t\t...(longFetch && { fetch: longFetch })\n\t\t};\n\n\t\tif (config.debug) {\n\t\t\tclientConfig.onServerTiming = (t) => {\n\t\t\t\tif (t.cacheHit) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t'[Compute/rhino-cache] solve-cache (cachesolve): HIT (cached result, solve skipped)'\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst decode = t.decode ?? 0;\n\t\t\t\tconst solve = t.solve ?? 0;\n\t\t\t\tconst encode = t.encode ?? 0;\n\t\t\t\t// Heuristic threshold: a genuine parse is many ms; a reused definition is\n\t\t\t\t// sub-millisecond. Only meaningful relative to the first (cold) solve.\n\t\t\t\tconst defCache =\n\t\t\t\t\tdecode <= 1 ? 'HIT (definition reused, no re-upload/parse)' : 'miss (full parse)';\n\t\t\t\tconst solveCache = config.cachesolve ? 'miss (solved fresh)' : 'off';\n\t\t\t\tconst ran = t.recomputed ? ` | ran ${t.recomputed.ran}/${t.recomputed.total} objects` : '';\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/rhino-cache] decode=${decode}ms solve=${solve}ms encode=${encode}ms ` +\n\t\t\t\t\t\t`| definition-cache: ${defCache} | solve-cache (cachesolve): ${solveCache}${ran}`\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\n\t\t// Baked at client-create time because the scheduler has no per-request\n\t\t// header hook.\n\t\tif (definitionGuid) {\n\t\t\tclientConfig.headers = { 'X-Selva-Definition': definitionGuid };\n\t\t}\n\n\t\tconst client = await GrasshopperClient.create(clientConfig);\n\n\t\tconst probe = createConcurrencyProbe(client, debugLog);\n\t\tconst readLimits = async () =>\n\t\t\tsolveLimitsFor(\n\t\t\t\t(await Promise.resolve(client.serverStats?.getLimits?.()).catch(() => null)) ?? null\n\t\t\t);\n\t\tconst [limits] = await Promise.all([readLimits(), probe.refresh()]);\n\t\tdebugLog(\n\t\t\t`[Compute/client-cache] server ${server.id}: deadline ${limits.solveDeadlineMs}ms, ` +\n\t\t\t\t`request cap ${limits.maxRequestBytes ?? 'not reported'}, pointer reuse ${limits.reuseDefinitionCache}`\n\t\t);\n\t\tlet limitsCheckedAt = Date.now();\n\t\tlet checkingLimits = false;\n\t\tconst recheckLimitsIfDue = () => {\n\t\t\tif (checkingLimits || Date.now() - limitsCheckedAt < LIMITS_RECHECK_MS) return;\n\t\t\tcheckingLimits = true;\n\t\t\tvoid readLimits()\n\t\t\t\t.then((next) => {\n\t\t\t\t\tlimitsCheckedAt = Date.now();\n\t\t\t\t\tif (sameSolveLimits(next, limits)) return;\n\t\t\t\t\tdebugLog(`[Compute/client-cache] server ${server.id} limits changed — rebuilding`);\n\t\t\t\t\tif (cache.get(server.id) === entry) evictEntry(server.id);\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\tcheckingLimits = false;\n\t\t\t\t});\n\t\t};\n\n\t\tconst scheduler = client.createScheduler({\n\t\t\tmode: 'queue',\n\t\t\t// Queue mode defaults maxConcurrent to 1, serializing every solve on\n\t\t\t// this server through a single slot while the compute VM's other\n\t\t\t// compute.geometry children idle — always pass it explicitly.\n\t\t\tmaxConcurrent: probe.maxConcurrent,\n\t\t\t// The scheduler treats undefined as unbounded/no-deadline, so map our\n\t\t\t// `0`-means-off convention to undefined rather than passing 0 (which\n\t\t\t// would reject EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: limits.solveDeadlineMs,\n\t\t\t// The byte budget is the only eviction pressure. A solve is a pure\n\t\t\t// function of (definition, inputs), both immutable, so a retained\n\t\t\t// result can never go stale — expiring one only forces a paid re-solve\n\t\t\t// of the identical answer.\n\t\t\tcache: config.responseCacheMaxBytes > 0 ? { maxBytes: config.responseCacheMaxBytes } : false,\n\t\t\treuseServerDefinitionCache: limits.reuseDefinitionCache,\n\t\t\tonSettle: (_ctx, result) => {\n\t\t\t\t// Deferred: this hook fires before the scheduler drops the solve from its in-flight set.\n\t\t\t\tqueueMicrotask(() => finishDraining(entry));\n\t\t\t\tif (result.status !== 'success') return;\n\t\t\t\t// A cache hit means we never reached the server, so it says nothing about the pool.\n\t\t\t\t// `scheduler` is safe despite being referenced from inside its own initializer:\n\t\t\t\t// onSettle only ever fires during a solve, long after binding.\n\t\t\t\tif (result.fromCache !== true) {\n\t\t\t\t\tprobe.refreshIfDue(scheduler);\n\t\t\t\t\trecheckLimitsIfDue();\n\t\t\t\t}\n\t\t\t\tif (config.debug) describeSettledSolve(result).forEach(debugLog);\n\t\t\t}\n\t\t});\n\t\tconst entry: CachedClient = { client, scheduler, limits };\n\t\treturn entry;\n\t}\n\n\tfunction evictEntry(id: string) {\n\t\tconst entry = cache.get(id);\n\t\tif (!entry) return;\n\t\tcache.delete(id);\n\t\tretire(entry);\n\t}\n\n\treturn {\n\t\tasync getClient(server, opts): Promise<CachedClient> {\n\t\t\tconst key = server.id;\n\t\t\tconst existing = cache.get(key);\n\t\t\tif (existing) {\n\t\t\t\t// Refresh LRU position.\n\t\t\t\tcache.delete(key);\n\t\t\t\tcache.set(key, existing);\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst inFlight = pending.get(key);\n\t\t\tif (inFlight) return inFlight.promise;\n\n\t\t\t// Replay a recent failure instead of re-running the probe ladder. The\n\t\t\t// original error is rethrown verbatim so the caller still sees the real\n\t\t\t// cause (refused / unauthorized / timeout), not a generic cache miss.\n\t\t\tconst recentFailure = failed.get(key);\n\t\t\tif (recentFailure) {\n\t\t\t\tif (Date.now() - recentFailure.at < FAILED_BUILD_TTL_MS) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t`[Compute/client-cache] server ${key} failed to build <${FAILED_BUILD_TTL_MS}ms ago — replaying error`\n\t\t\t\t\t);\n\t\t\t\t\tthrow recentFailure.error;\n\t\t\t\t}\n\t\t\t\tfailed.delete(key);\n\t\t\t}\n\n\t\t\t// A build is a Rhino.Compute handshake — expensive, and repeated builds for\n\t\t\t// the same id mean churn (LRU thrash or config-rotation evictions).\n\t\t\tdebugLog(`[Compute/client-cache] miss — building warm client for server ${key}`);\n\t\t\tconst token = { stale: false };\n\t\t\tconst promise = (async () => {\n\t\t\t\tlet entry: CachedClient;\n\t\t\t\ttry {\n\t\t\t\t\tentry = await build(server, opts?.definitionGuid);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (!token.stale) failed.set(key, { error: err, at: Date.now() });\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\t// Installing it would carry the old URL or key past the eviction, with nothing left to\n\t\t\t\t// evict it again. Its caller holds a client built from those same details, so it fails too.\n\t\t\t\tif (token.stale) {\n\t\t\t\t\tentry.scheduler.dispose();\n\t\t\t\t\tthrow new ComputeError(\n\t\t\t\t\t\t`Compute server ${key} changed while connecting.`,\n\t\t\t\t\t\tErrorCodes.ABORTED\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (cache.size >= maxWarmComputeServers) {\n\t\t\t\t\tconst oldestKey = cache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/client-cache] LRU evicted warm client for server ${oldestKey} (cap ${maxWarmComputeServers})`\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst oldest = cache.get(oldestKey);\n\t\t\t\t\t\tcache.delete(oldestKey);\n\t\t\t\t\t\tif (oldest) retire(oldest);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcache.set(key, entry);\n\t\t\t\treturn entry;\n\t\t\t})().finally(() => {\n\t\t\t\tif (pending.get(key)?.token === token) pending.delete(key);\n\t\t\t});\n\t\t\tpending.set(key, { promise, token });\n\t\t\treturn promise;\n\t\t},\n\n\t\tevict(id): void {\n\t\t\t// Drop any remembered failure too: `evict` means the connection details\n\t\t\t// changed, so the reason the last build failed no longer applies.\n\t\t\tfailed.delete(id);\n\t\t\tconst build = pending.get(id);\n\t\t\tif (build) {\n\t\t\t\tbuild.token.stale = true;\n\t\t\t\tpending.delete(id);\n\t\t\t}\n\t\t\tif (cache.has(id)) {\n\t\t\t\tdebugLog(`[Compute/client-cache] evicted warm client for server ${id} (config change)`);\n\t\t\t\tevictEntry(id);\n\t\t\t}\n\t\t},\n\n\t\tsolveCacheStats(): SolveCacheStats {\n\t\t\tconst total: SolveCacheStats = {\n\t\t\t\twarmClients: cache.size,\n\t\t\t\tentries: 0,\n\t\t\t\tbytes: 0,\n\t\t\t\thits: 0,\n\t\t\t\tmisses: 0,\n\t\t\t\tevictions: 0\n\t\t\t};\n\t\t\tfor (const entry of cache.values()) {\n\t\t\t\tconst s = entry.scheduler.cacheStats();\n\t\t\t\ttotal.entries += s.entries;\n\t\t\t\ttotal.bytes += s.bytes;\n\t\t\t\ttotal.hits += s.hits;\n\t\t\t\ttotal.misses += s.misses;\n\t\t\t\ttotal.evictions += s.evictions;\n\t\t\t}\n\t\t\treturn total;\n\t\t},\n\n\t\tclearSolveCaches(): void {\n\t\t\tdebugLog(`[Compute/client-cache] cleared solve caches on ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.clearCache();\n\t\t},\n\n\t\tdisposeAll(): void {\n\t\t\tdebugLog(`[Compute/client-cache] disposing all ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of [...cache.values(), ...draining]) entry.scheduler.dispose();\n\t\t\tfor (const build of pending.values()) build.token.stale = true;\n\t\t\tpending.clear();\n\t\t\tcache.clear();\n\t\t\tdraining.clear();\n\t\t\tfailed.clear();\n\t\t}\n\t};\n}\n","/**\n * In-process cache of `.gh` definition bytes, keyed by immutable **version id**.\n *\n * Turns an eager `storage.get(fileKey)` on every solve request into a lazy\n * `DefinitionRef.load()`: the scheduler calls `load()` only when an upload is\n * unavoidable (a pointer-known re-solve never touches storage or this cache).\n *\n * The key MUST be the version id, never a `fileKey` — a delete-latest-then-\n * reupload can reuse a `fileKey` for different content, which would silently\n * serve one version's bytes for another with no diagnostic. Callers pass\n * `version.id`, and the returned `DefinitionRef.key` is that same id, so the\n * scheduler's own result/pointer cache keys on the same immutable identity.\n *\n * Eviction is LRU by total byte budget, not entry count — definitions range\n * from tens of KB to hundreds of MB, so a count cap would either pin\n * gigabytes or evict uselessly. `COMPUTE_DEFINITION_CACHE_MB` sets the\n * budget (see `createDefinitionByteCache`). No TTL: version ids are\n * immutable, so a cached entry can never go stale. An entry larger than the\n * whole budget is served but never retained.\n */\n\nimport { raceAbort } from '../shared/abort.js';\n\n/**\n * Per-`getOrLoad` outcome, observable after the scheduler has (or hasn't)\n * called `load()` — lets the solve pipeline emit a `def_bytes` Server-Timing\n * verdict (`skipped` / `hit` / `miss`).\n */\nexport interface ByteRefOutcome {\n\tloaded: boolean;\n\tfromCache: boolean;\n}\n\n/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */\nexport interface ByteCacheRef {\n\tkey: string;\n\tload: (signal?: AbortSignal) => Promise<Uint8Array>;\n\toutcome: ByteRefOutcome;\n}\n\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\t/** `load` gets a signal that fires once every caller waiting on this read has gone. */\n\tgetOrLoad(versionId: string, load: (signal: AbortSignal) => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\tclear(): void;\n}\n\n/** A `ByteCacheRef` carries `outcome`, which a plain `DefinitionRef` never has. */\nexport function isByteCacheRef(source: unknown): source is ByteCacheRef {\n\treturn typeof source === 'object' && source !== null && 'outcome' in source;\n}\n\n/** One storage read, shared by every caller that misses while it runs. */\ninterface Loading {\n\tpromise: Promise<Uint8Array>;\n\tcontroller: AbortController;\n\twaiters: number;\n}\n\n/**\n * @param maxBytes total retained-byte budget; `0` (or negative) disables caching\n * entirely — every `load()` calls the loader and nothing is retained.\n */\nexport function createDefinitionByteCache(maxBytes: number): DefinitionByteCache {\n\tconst budget = Math.max(0, Math.floor(maxBytes));\n\t// Map iteration order is insertion order, so it doubles as LRU order:\n\t// re-inserting a key on access moves it to the most-recently-used end.\n\tconst cache = new Map<string, Uint8Array>();\n\tconst loading = new Map<string, Loading>();\n\tlet retainedBytes = 0;\n\tconst stats = { hits: 0, misses: 0, evictions: 0 };\n\n\tfunction evictToFit(): void {\n\t\tfor (const [key, bytes] of cache) {\n\t\t\tif (retainedBytes <= budget) break;\n\t\t\tcache.delete(key);\n\t\t\tretainedBytes -= bytes.byteLength;\n\t\t\tstats.evictions += 1;\n\t\t}\n\t}\n\n\tfunction store(key: string, bytes: Uint8Array): void {\n\t\tif (budget === 0) return;\n\t\t// Caching an entry bigger than the budget would evict everything\n\t\t// (including itself) on the next evictToFit — serve it, cache nothing.\n\t\tif (bytes.byteLength > budget) return;\n\t\tcache.set(key, bytes);\n\t\tretainedBytes += bytes.byteLength;\n\t\tevictToFit();\n\t}\n\n\tfunction startLoad(key: string, load: (signal: AbortSignal) => Promise<Uint8Array>): Loading {\n\t\tconst controller = new AbortController();\n\t\tconst entry: Loading = {\n\t\t\tcontroller,\n\t\t\twaiters: 0,\n\t\t\tpromise: (async () => {\n\t\t\t\tconst bytes = await load(controller.signal);\n\t\t\t\tstore(key, bytes);\n\t\t\t\treturn bytes;\n\t\t\t})().finally(() => {\n\t\t\t\tif (loading.get(key) === entry) loading.delete(key);\n\t\t\t})\n\t\t};\n\t\tloading.set(key, entry);\n\t\treturn entry;\n\t}\n\n\t/**\n\t * A caller gone stops waiting, but the read runs on for whoever else is on it. A caller with no\n\t * signal can't leave, so it stays counted and the read is never aborted under it.\n\t */\n\tasync function wait(entry: Loading, signal?: AbortSignal): Promise<Uint8Array> {\n\t\tentry.waiters += 1;\n\t\tif (!signal) return entry.promise;\n\t\ttry {\n\t\t\treturn await raceAbort(entry.promise, signal);\n\t\t} finally {\n\t\t\tentry.waiters -= 1;\n\t\t\tif (entry.waiters === 0 && signal.aborted) entry.controller.abort();\n\t\t}\n\t}\n\n\treturn {\n\t\tgetOrLoad(versionId, load) {\n\t\t\tconst outcome: ByteRefOutcome = { loaded: false, fromCache: false };\n\t\t\treturn {\n\t\t\t\tkey: versionId,\n\t\t\t\toutcome,\n\t\t\t\tload: async function read(signal): Promise<Uint8Array> {\n\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\tconst hit = cache.get(versionId);\n\t\t\t\t\tif (hit) {\n\t\t\t\t\t\tcache.delete(versionId);\n\t\t\t\t\t\tcache.set(versionId, hit);\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn hit;\n\t\t\t\t\t}\n\t\t\t\t\tconst running = loading.get(versionId);\n\t\t\t\t\tif (running?.controller.signal.aborted) {\n\t\t\t\t\t\t// Abandoned, and a store that ignores its signal may never finish. Waiting it out\n\t\t\t\t\t\t// rather than starting a second read keeps a hung store to one read per version.\n\t\t\t\t\t\tconst woundDown = running.promise.then(\n\t\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t\t\t() => undefined\n\t\t\t\t\t\t);\n\t\t\t\t\t\tawait (signal ? raceAbort(woundDown, signal) : woundDown);\n\t\t\t\t\t\treturn read(signal);\n\t\t\t\t\t}\n\t\t\t\t\tif (running) {\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn wait(running, signal);\n\t\t\t\t\t}\n\t\t\t\t\tstats.misses += 1;\n\t\t\t\t\treturn wait(startLoad(versionId, load), signal);\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tstats() {\n\t\t\treturn {\n\t\t\t\thits: stats.hits,\n\t\t\t\tmisses: stats.misses,\n\t\t\t\tevictions: stats.evictions,\n\t\t\t\tentries: cache.size,\n\t\t\t\tbytes: retainedBytes\n\t\t\t};\n\t\t},\n\t\tclear() {\n\t\t\tcache.clear();\n\t\t\tretainedBytes = 0;\n\t\t}\n\t};\n}\n","/**\n * Turns one solve's response into each caller's wire form. Runs after single-flight, not inside\n * it: coalesced callers can want different encodings, and each reports its own timing.\n */\n\nimport { gzip } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport type { ServerTiming } from '@selvajs/compute/core';\nimport type { GrasshopperComputeResponse } from '@selvajs/compute/grasshopper';\nimport type { CacheVerdict, DefBytesVerdict } from './server-timing.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n/**\n * Display output is base64 of already-deflated bytes, where higher levels find nothing more:\n * level 1 lands within 1% of the default's size, in up to a fifth of the time.\n */\nconst GZIP_LEVEL = 1;\n\n/** Below this gzip isn't worth the CPU for the wire savings. */\nconst GZIP_MIN_BYTES = 1024;\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Envelope\n// ============================================================================\n\n/** What the `Server-Timing` header carries for one caller, as data: read this, don't parse that. */\nexport interface SolvePhaseMetrics {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n\t/** The compute server's own timing; null when it reports none. */\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** Absent for raw-bytes solves. */\n\tdefBytes?: DefBytesVerdict;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\nexport interface EncodedBody {\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\tserializedBytes: number;\n\tserializeMs: number;\n\tgzipMs: number;\n}\n\nexport type TooLarge = 'too_large';\n\nexport function buildEnvelope(\n\tencoded: EncodedBody,\n\tresult: GrasshopperComputeResponse,\n\tserverTiming: string,\n\tmetrics: SolvePhaseMetrics\n): SolveEnvelope {\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\tconst { body, encoding } = encoded;\n\tif (encoding === 'gzip') {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t}\n\theaders['Content-Length'] = String(\n\t\ttypeof body === 'string' ? Buffer.byteLength(body) : body.byteLength\n\t);\n\treturn { body, ...(encoding ? { encoding } : {}), headers, result, metrics };\n}\n\n// ============================================================================\n// Encoding\n// ============================================================================\n\n/** Whether an `Accept-Encoding` header takes gzip. `gzip;q=0` is a client refusing it. */\nexport function acceptsGzip(acceptEncoding: string): boolean {\n\treturn acceptEncoding.split(',').some((part) => {\n\t\tconst [coding, ...params] = part.split(';').map((s) => s.trim().toLowerCase());\n\t\tif (coding !== 'gzip') return false;\n\t\tconst q = params.find((p) => p.startsWith('q='));\n\t\treturn q === undefined || Number(q.slice(2)) > 0;\n\t});\n}\n\ninterface GzippedBody {\n\tbody: Uint8Array;\n\tserializedBytes: number;\n}\n\n/**\n * A scheduler cache hit returns the same response object, so a repeat solve reuses its bytes rather\n * than paying to serialize and compress them again. Entries live exactly as long as the response.\n * Only the gzip form is kept: browsers always take it, and the JSON string would double the memory.\n * Holds the promise, so callers arriving during the compression share it.\n */\nconst gzippedBodies = new WeakMap<object, Promise<GzippedBody | TooLarge>>();\n\nfunction gzipOnce(response: object, json: string, serializedBytes: number) {\n\tconst pending = gzipAsync(Buffer.from(json), { level: GZIP_LEVEL }).then((body) => ({\n\t\tbody,\n\t\tserializedBytes\n\t}));\n\tgzippedBodies.set(response, pending);\n\t// A failed compression must not stay memoized while the response is cached.\n\tpending.catch(() => gzippedBodies.delete(response));\n\treturn pending;\n}\n\nexport interface ResponseEncoder {\n\t/** Buffered, not streamed, so Content-Length is known upfront and a connection cut mid-transfer fails hard instead of silently truncating the JSON. */\n\tencode(acceptEncoding: string): Promise<EncodedBody | TooLarge>;\n}\n\n/**\n * One per solve. The JSON is kept here rather than beside the gzip body: it only has to outlive the\n * callers of this flight, who would otherwise each stringify the same response and hold a copy.\n */\nexport function createResponseEncoder(response: object, maxBytes: number): ResponseEncoder {\n\tlet serialized: { json: string; bytes: number } | TooLarge | undefined;\n\n\tfunction serialize() {\n\t\tif (serialized !== undefined) return serialized;\n\t\ttry {\n\t\t\tconst json = JSON.stringify(response);\n\t\t\tconst bytes = Buffer.byteLength(json);\n\t\t\tserialized = bytes > maxBytes ? 'too_large' : { json, bytes };\n\t\t} catch (err) {\n\t\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t\t// here. Treat it the same as the byte cap.\n\t\t\tif (!(err instanceof RangeError)) throw err;\n\t\t\tserialized = 'too_large';\n\t\t}\n\t\treturn serialized;\n\t}\n\n\treturn {\n\t\tasync encode(acceptEncoding) {\n\t\t\tconst wantsGzip = acceptsGzip(acceptEncoding);\n\t\t\tlet gzipped = wantsGzip ? gzippedBodies.get(response) : undefined;\n\t\t\tlet serializeMs = 0;\n\n\t\t\tif (!gzipped) {\n\t\t\t\tconst serializeStart = performance.now();\n\t\t\t\tconst plain = serialize();\n\t\t\t\tserializeMs = performance.now() - serializeStart;\n\t\t\t\tif (plain === 'too_large') {\n\t\t\t\t\tif (wantsGzip) gzippedBodies.set(response, Promise.resolve('too_large'));\n\t\t\t\t\treturn 'too_large';\n\t\t\t\t}\n\t\t\t\tif (!wantsGzip || plain.bytes <= GZIP_MIN_BYTES) {\n\t\t\t\t\treturn { body: plain.json, serializedBytes: plain.bytes, serializeMs, gzipMs: 0 };\n\t\t\t\t}\n\t\t\t\tgzipped = gzipOnce(response, plain.json, plain.bytes);\n\t\t\t}\n\n\t\t\tconst gzipStart = performance.now();\n\t\t\tconst verdict = await gzipped;\n\t\t\tif (verdict === 'too_large') return verdict;\n\t\t\treturn {\n\t\t\t\tbody: verdict.body,\n\t\t\t\tencoding: 'gzip',\n\t\t\t\tserializedBytes: verdict.serializedBytes,\n\t\t\t\tserializeMs,\n\t\t\t\tgzipMs: performance.now() - gzipStart\n\t\t\t};\n\t\t}\n\t};\n}\n","import type { ServerTiming } from '@selvajs/compute/core';\n\n/** A pre-solve prep sub-phase: `[label, ms]`, surfaced as a `p_*` Server-Timing entry. */\nexport type PrepMark = [string, number];\n\nexport interface CacheVerdict {\n\tfromCache: boolean;\n\tdefinitionReuploaded?: boolean;\n}\n\n/**\n * skipped = scheduler never materialized bytes (pointer-known solve, the whole point of this\n * path); hit = warm byte cache; miss = fell through to storage.\n */\nexport type DefBytesVerdict = 'skipped' | 'hit' | 'miss';\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** Absent for raw-bytes solves. */\n\tdefBytes?: DefBytesVerdict;\n\tprepMarks: PrepMark[];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own timing\n * (VektorNode fork), `rhino_*` is time on the compute server and `compute_link`\n * is everything between it (network + queue wait). `rhino_cache` is 1 when the\n * compute server answered from its own result cache.\n */\nexport function buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst decode = rhino.decode ?? 0;\n\t\tconst solve = rhino.solve ?? 0;\n\t\tconst encode = rhino.encode ?? 0;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - decode - solve - encode);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}` +\n\t\t\t`, rhino_cache;dur=${rhino.cacheHit ? 1 : 0}`;\n\t\tif (rhino.gh !== undefined) header += `, rhino_gh;dur=${rhino.gh.toFixed(1)}`;\n\t\tif (rhino.recomputed) {\n\t\t\theader += `, rhino_recomputed;desc=\"${rhino.recomputed.ran}/${rhino.recomputed.total}\"`;\n\t\t}\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\tif (settle.definitionReuploaded !== undefined) {\n\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t}\n\n\tif (parts.defBytes) header += `, def_bytes;desc=${parts.defBytes}`;\n\n\treturn header;\n}\n","/**\n * In-process single-flight — dogpile protection for every solve.\n *\n * Always on, not gated behind a result cache being configured: the dogpile is worst\n * when nothing else is caching, since N concurrent identical solves each pay a full\n * Rhino round trip (the scheduler doesn't coalesce in-flight requests itself, and\n * Rhino's `cachesolve` still costs a round trip per repeat).\n *\n * Per app instance. No cross-instance lease (Redis `SET NX`) yet.\n *\n * The shared promise resolves to one value for every caller, so `work` must return\n * something safe to share by reference.\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Coalesces concurrent calls under the same key: the first caller runs `work`, later\n\t * callers await the same promise, and the key frees as soon as it settles.\n\t *\n\t * The flight belongs to all its callers. It aborts `work`'s signal when the last one's\n\t * `callerSignal` fires, and frees the key right then, so a caller arriving after that starts\n\t * a fresh flight instead of joining one that is dying. A caller that leaves earlier still\n\t * resolves with the flight: check its own signal before using the result.\n\t */\n\trun<T>(\n\t\tkey: string,\n\t\twork: (signal: AbortSignal) => Promise<T>,\n\t\tcallerSignal: AbortSignal\n\t): Promise<T>;\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\tonJoin?: (key: string) => void;\n}\n\ninterface Flight {\n\tpromise: Promise<unknown>;\n\tcontroller: AbortController;\n\tcallers: number;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\tconst inflight = new Map<string, Flight>();\n\n\tfunction free(key: string, flight: Flight) {\n\t\t// Guard against deleting a key a later caller has already re-taken.\n\t\tif (inflight.get(key) === flight) inflight.delete(key);\n\t}\n\n\tfunction attend<T>(key: string, flight: Flight, signal: AbortSignal): Promise<T> {\n\t\tflight.callers += 1;\n\t\t// `once: true` doesn't detach on a normal completion, and the caller's signal outlives\n\t\t// this call, so without the `finally` every solve leaks a listener onto it.\n\t\tconst leave = () => {\n\t\t\tflight.callers -= 1;\n\t\t\tif (flight.callers > 0) return;\n\t\t\tfree(key, flight);\n\t\t\tflight.controller.abort();\n\t\t};\n\t\tsignal.addEventListener('abort', leave, { once: true });\n\t\treturn (flight.promise as Promise<T>).finally(() => signal.removeEventListener('abort', leave));\n\t}\n\n\treturn {\n\t\trun<T>(key: string, work: (signal: AbortSignal) => Promise<T>, callerSignal: AbortSignal) {\n\t\t\tconst existing = inflight.get(key);\n\t\t\tif (existing) {\n\t\t\t\ttry {\n\t\t\t\t\toptions.onJoin?.(key);\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Telemetry must not fail a solve that is otherwise fine.\n\t\t\t\t\tconsole.error('[Solve/single-flight] onJoin threw:', err);\n\t\t\t\t}\n\t\t\t\treturn attend<T>(key, existing, callerSignal);\n\t\t\t}\n\n\t\t\tconst controller = new AbortController();\n\t\t\tconst flight: Flight = {\n\t\t\t\tcontroller,\n\t\t\t\tcallers: 0,\n\t\t\t\t// Wrapped so a synchronous throw in `work` still rejects instead of escaping `run`.\n\t\t\t\tpromise: (async () => work(controller.signal))().finally(() => free(key, flight))\n\t\t\t};\n\t\t\tinflight.set(key, flight);\n\t\t\treturn attend<T>(key, flight, callerSignal);\n\t\t},\n\t\tinFlight(): number {\n\t\t\treturn inflight.size;\n\t\t}\n\t};\n}\n","import { TreeBuilder, type DataTree } from '@selvajs/compute/grasshopper';\nimport { getInputItems, type UISchema } from '@selvajs/schemas';\nimport type { SolveInput } from '../shared/solve-input.js';\n\n/**\n * What `SolveEngine.solve` takes as `inputs`, from a schema the host stored. The bounds live on\n * the number widgets in the layout, not on `schema.inputs`, so without them nothing clamps.\n */\nexport function solveInputsFromSchema(schema: UISchema): SolveInput[] {\n\tconst bounds = new Map<string, { minimum?: number; maximum?: number }>();\n\tfor (const item of getInputItems(schema)) {\n\t\tif (item.widgetType === 'number' && item.config) bounds.set(item.paramId, item.config);\n\t}\n\treturn schema.inputs.map((input) => {\n\t\tconst widget = bounds.get(input.id);\n\t\treturn widget ? { ...input, minimum: widget.minimum, maximum: widget.maximum } : input;\n\t});\n}\n\n/** The input tree a solve sends: each input's value, or its schema default when it has none. */\nexport function buildSolveInputTree(\n\tinputs: SolveInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromValues(\n\t\tinputs\n\t\t\t// Stored schemas are validated at upload only, so an older one can lack `paramType`.\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input) => ({\n\t\t\t\tparamName: input.nickname,\n\t\t\t\tparamType: input.paramType,\n\t\t\t\t// `hasOwn`: an input id of `constructor` would read the inherited function.\n\t\t\t\tvalue: (Object.hasOwn(values, input.id) ? values[input.id] : undefined) ?? input.default,\n\t\t\t\tminimum: input.minimum,\n\t\t\t\tmaximum: input.maximum\n\t\t\t}))\n\t);\n}\n","/**\n * Runs one solve against a warm scheduler and classifies how it ended: a solved response, or an\n * expected failure as a value instead of a throw. This is the part single-flight shares, so\n * nothing here depends on who is asking.\n */\n\nimport {\n\tErrorCodes,\n\tComputeError,\n\ttype ServerTiming,\n\ttype SolveDefinition\n} from '@selvajs/compute/core';\nimport type {\n\tDataTree,\n\tGrasshopperComputeResponse,\n\tSelvaEventTarget\n} from '@selvajs/compute/grasshopper';\nimport type { CachedClient } from './client-cache.js';\nimport { isByteCacheRef } from './definition-byte-cache.js';\nimport {\n\tcreateResponseEncoder,\n\ttype ResponseEncoder,\n\ttype SolveEnvelope\n} from './response-encoding.js';\nimport type { CacheVerdict, DefBytesVerdict } from './server-timing.js';\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\tinputTree: DataTree[];\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Per-solve live-event callback the compute server hands to the definition; absent when the caller opened no stream. */\n\tselvaEvents?: SelvaEventTarget;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also tells that apart from an abort nobody asked for.\n\t */\n\tsignal: AbortSignal;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure:\n * `SolveEngine.toWebResponse` maps each to a status. `durationMs` on error variants is the solve\n * wall time up to the failure, for the metric record.\n */\nexport type SolveEngineOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| SolveFailure;\n\nexport type SolveFailure =\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** The compute server refused the request body (its `RHINO_COMPUTE_MAX_REQUEST_SIZE`). */\n\t| { kind: 'request_too_large'; durationMs: number; message: string }\n\t/**\n\t * Retryable: no result, through no fault of the request, and `retryAfterSeconds` is a backoff\n\t * hint. Scheduler backpressure rejected the solve before it ran (`queue_full`,\n\t * `queue_timeout`), or its client was shut down under it (`interrupted`).\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout' | 'interrupted';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n/** One finished solve, shared by every caller coalesced onto it. */\nexport interface SolvedFlight {\n\tkind: 'solved';\n\tresponse: GrasshopperComputeResponse;\n\tencoder: ResponseEncoder;\n\tsolveMs: number;\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** The verdict of the ref that ran: a coalesced caller's own ref never loads anything. */\n\tdefBytes?: DefBytesVerdict;\n}\n\nfunction shed(\n\tdurationMs: number,\n\treason: 'queue_full' | 'queue_timeout' | 'interrupted',\n\tmessage: string\n): SolveFailure {\n\treturn { kind: 'shed', durationMs, reason, retryAfterSeconds: 1, message };\n}\n\n/** The solve lost its client, and no caller asked for that: a config write or a shutdown. */\nexport function interrupted(durationMs: number): SolveFailure {\n\treturn shed(durationMs, 'interrupted', 'The solve was interrupted. Retry shortly.');\n}\n\nfunction classifyFailure(err: unknown, durationMs: number, args: SolvePipelineArgs): SolveFailure {\n\tif (!(err instanceof ComputeError)) return { kind: 'compute_error', durationMs, error: err };\n\tif (err.statusCode === 413) {\n\t\tconst cap = args.client.limits.maxRequestBytes;\n\t\treturn {\n\t\t\tkind: 'request_too_large',\n\t\t\tdurationMs,\n\t\t\tmessage:\n\t\t\t\t`The inputs are larger than the compute server accepts` +\n\t\t\t\t(cap ? ` (${Math.round(cap / (1024 * 1024))} MB)` : '') +\n\t\t\t\t'. Its admin can raise RHINO_COMPUTE_MAX_REQUEST_SIZE.'\n\t\t};\n\t}\n\tswitch (err.code) {\n\t\tcase ErrorCodes.TIMEOUT_ERROR:\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.client.limits.solveDeadlineMs / 1000)}s deadline set by the compute server.`\n\t\t\t};\n\t\tcase ErrorCodes.ABORTED:\n\t\t\treturn args.signal.aborted ? { kind: 'client_abort', durationMs } : interrupted(durationMs);\n\t\tcase ErrorCodes.RESPONSE_TOO_LARGE:\n\t\t\treturn { kind: 'too_large' };\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tcase ErrorCodes.QUEUE_FULL:\n\t\t\treturn shed(durationMs, 'queue_full', 'Compute server is at capacity. Retry shortly.');\n\t\tcase ErrorCodes.QUEUE_TIMEOUT:\n\t\t\treturn shed(\n\t\t\t\tdurationMs,\n\t\t\t\t'queue_timeout',\n\t\t\t\t'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t);\n\t\tdefault:\n\t\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n}\n\nfunction defBytesVerdict(source: SolveDefinition): DefBytesVerdict | undefined {\n\tif (!isByteCacheRef(source)) return undefined;\n\tconst { loaded, fromCache } = source.outcome;\n\treturn !loaded ? 'skipped' : fromCache ? 'hit' : 'miss';\n}\n\nexport async function runSolvePipeline(\n\targs: SolvePipelineArgs\n): Promise<SolvedFlight | SolveFailure> {\n\tlet rhino: ServerTiming | null = null;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tconst detail = await args.client.scheduler.solveDetailed(\n\t\t\targs.definitionSource,\n\t\t\targs.inputTree,\n\t\t\t{\n\t\t\t\tsignal: args.signal,\n\t\t\t\tselvaevents: args.selvaEvents,\n\t\t\t\tonServerTiming: (t) => {\n\t\t\t\t\trhino = t;\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tconst defBytes = defBytesVerdict(args.definitionSource);\n\t\treturn {\n\t\t\tkind: 'solved',\n\t\t\tresponse: detail.response,\n\t\t\tencoder: createResponseEncoder(detail.response, args.responseMaxBytes),\n\t\t\tsolveMs: performance.now() - solveStart,\n\t\t\trhino,\n\t\t\tsettle: { fromCache: detail.fromCache, definitionReuploaded: detail.definitionReuploaded },\n\t\t\t...(defBytes ? { defBytes } : {})\n\t\t};\n\t} catch (err) {\n\t\treturn classifyFailure(err, performance.now() - solveStart, args);\n\t}\n}\n","/**\n * Facade over the warm-client cache, definition-byte cache, single-flight\n * coalescing, and the solve pipeline — the primitives an app route would\n * otherwise wire up by hand.\n *\n * Doesn't read env itself: `@selvajs/server` owns `resolveComputeLimits`, and\n * this package stays independent of it. Callers resolve their own limits and\n * pass in the fields below.\n *\n * Stays out of auth, DB reads, share tokens, rate limiting, metrics, and\n * compute-server selection — all app policy, supplied per-call via\n * `SolveEngineSolveArgs.server`.\n */\n\nimport {\n\tComputeError,\n\tErrorCodes,\n\tisDefinitionRef,\n\ttype DefinitionRef,\n\ttype SolveDefinition\n} from '@selvajs/compute/core';\nimport { stableStringify, type SelvaEventTarget } from '@selvajs/compute/grasshopper';\nimport {\n\tcreateClientCache,\n\ttype CachedClient,\n\ttype ClientCache,\n\ttype ClientCacheDebug,\n\ttype ResolvedServer,\n\ttype SolveCacheStats\n} from './client-cache.js';\nimport {\n\tcreateDefinitionByteCache,\n\tisByteCacheRef,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype DefinitionByteCache\n} from './definition-byte-cache.js';\nimport { buildEnvelope } from './response-encoding.js';\nimport { buildServerTiming, type PrepMark } from './server-timing.js';\nimport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight\n} from './solve-cache-single-flight.js';\nimport { buildSolveInputTree } from './transform-input.js';\nimport { interrupted, runSolvePipeline, type SolveEngineOutcome } from './solve-pipeline.js';\nimport type { SolveInput } from '../shared/solve-input.js';\n\n/**\n * Subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs.\n * Pass the resolved object through as-is — extra fields are ignored.\n */\nexport interface SolveEngineLimits {\n\tcomputeResponseMaxBytes: number;\n\tcomputeServerCachesolve: boolean;\n\tcomputeMaxQueueDepth: number;\n\tcomputeQueueWaitMs: number;\n\tcomputeDefinitionCacheBytes: number;\n\tcomputeSolveCacheBytes: number;\n}\n\n/** A deadline of 0 means no deadline, and a `NaN` budget retains without limit, both silently. */\nfunction assertValidLimits(limits: SolveEngineLimits): void {\n\tconst check = (name: keyof SolveEngineLimits, min: number) => {\n\t\tconst value = limits[name] as number;\n\t\tif (!Number.isFinite(value) || value < min) {\n\t\t\tthrow new RangeError(`SolveEngine: limits.${name} must be a number >= ${min}, got ${value}.`);\n\t\t}\n\t};\n\tcheck('computeResponseMaxBytes', 1);\n\tcheck('computeMaxQueueDepth', 0);\n\tcheck('computeQueueWaitMs', 0);\n\tcheck('computeDefinitionCacheBytes', 0);\n\tcheck('computeSolveCacheBytes', 0);\n}\n\nexport interface SolveEngineOptions {\n\tlimits: SolveEngineLimits;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. */\n\tmaxWarmComputeServers?: number;\n\t/** Concise cache/timing logs when truthy; `'verbose'` also dumps full lib-level requests/responses. Default off. */\n\tdebug?: ClientCacheDebug;\n\t/** Sink for the debug lines. Without it they go nowhere. */\n\tonDebugLog?: (message: string) => void;\n\t/** Fired when a caller joins an already-in-flight solve instead of running its own. */\n\tonSolveCoalesced?: (key: string) => void;\n}\n\n/**\n * A `ByteCacheRef` obtained ahead of time from `engine.definitionRef()` (e.g. to\n * read bytes for schema extraction before solving) is passed through as-is.\n */\nexport type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef;\n\nexport interface SolveEngineSolveArgs {\n\tserver: ResolvedServer;\n\tdefinitionSource: SolveEngineDefinitionSource;\n\t/**\n\t * Coalesce-key identity for a raw `Uint8Array`/`string` source, which has\n\t * no natural identity the way a `DefinitionRef`'s `.key` does. Required in\n\t * that case — `solve()` throws rather than silently hashing the bytes.\n\t * Ignored for the other source forms.\n\t */\n\tdefinitionKey?: string;\n\t/**\n\t * The definition's inputs, from the schema you stored, never from the request: `id`, `nickname`,\n\t * `paramType` and the bounds decide what reaches the definition, so a body that supplies them\n\t * can drop a limit or fill a slot your host reserved.\n\t */\n\tinputs: SolveInput[];\n\tvalues: Record<string, unknown>;\n\tsignal: AbortSignal;\n\tacceptEncoding?: string;\n\t/**\n\t * Stamped as `X-Selva-Definition` when this solve builds the server's warm client. The header\n\t * is fixed per client, so later solves on that server carry the first definition's guid.\n\t */\n\tdefinitionGuid?: string;\n\tloadStartMs?: number;\n\tdefLoadMs?: number;\n\tprepMarks?: PrepMark[];\n\t/** Per-solve live-event callback. A solve carrying one runs its own compute call, never a shared one. */\n\tselvaEvents?: SelvaEventTarget;\n}\n\nexport interface SolveEngineStats {\n\tclient: SolveCacheStats;\n\tdefinitionBytes: ByteCacheStats;\n\tcoalescing: { inFlight: number };\n}\n\nexport class SolveEngine {\n\tprivate readonly limits: SolveEngineLimits;\n\tprivate readonly clientCache: ClientCache;\n\tprivate readonly byteCache: DefinitionByteCache;\n\tprivate readonly singleFlight: SolveCacheSingleFlight;\n\tprivate readonly clientGenerations = new WeakMap<CachedClient, number>();\n\tprivate lastClientGeneration = 0;\n\n\tconstructor(options: SolveEngineOptions) {\n\t\tassertValidLimits(options.limits);\n\t\tthis.limits = options.limits;\n\n\t\tthis.clientCache = createClientCache({\n\t\t\tcachesolve: this.limits.computeServerCachesolve,\n\t\t\tmaxQueueDepth: this.limits.computeMaxQueueDepth,\n\t\t\tqueueWaitMs: this.limits.computeQueueWaitMs,\n\t\t\tresponseCacheMaxBytes: this.limits.computeSolveCacheBytes,\n\t\t\tmaxResponseBytes: this.limits.computeResponseMaxBytes,\n\t\t\tdebug: options.debug ?? false,\n\t\t\tmaxWarmComputeServers: options.maxWarmComputeServers,\n\t\t\tonDebugLog: options.onDebugLog\n\t\t});\n\n\t\tthis.byteCache = createDefinitionByteCache(this.limits.computeDefinitionCacheBytes);\n\n\t\tthis.singleFlight = createSolveCacheSingleFlight({\n\t\t\tonJoin: options.onSolveCoalesced\n\t\t});\n\t}\n\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient> {\n\t\treturn this.clientCache.getClient(server, opts);\n\t}\n\n\tdefinitionRef(\n\t\tversionId: string,\n\t\tload: (signal: AbortSignal) => Promise<Uint8Array>\n\t): ByteCacheRef {\n\t\treturn this.byteCache.getOrLoad(versionId, load);\n\t}\n\n\t/**\n\t * Drops the warm client for `id`; call it when the server's URL or key changes. Solves already\n\t * running finish on the old connection details first, so a revoked key stays in use until then.\n\t */\n\tevictServer(id: string): void {\n\t\tthis.clientCache.evict(id);\n\t}\n\n\tclearSolveCaches(): void {\n\t\tthis.clientCache.clearSolveCaches();\n\t}\n\n\t/**\n\t * Drop every retained `.gh` byte entry. Safe at any time: entries key on an\n\t * immutable version id, so clearing costs a storage re-read and nothing else.\n\t */\n\tclearDefinitionCache(): void {\n\t\tthis.byteCache.clear();\n\t}\n\n\tstats(): SolveEngineStats {\n\t\treturn {\n\t\t\tclient: this.clientCache.solveCacheStats(),\n\t\t\tdefinitionBytes: this.byteCache.stats(),\n\t\t\tcoalescing: { inFlight: this.singleFlight.inFlight() }\n\t\t};\n\t}\n\n\tasync solve(args: SolveEngineSolveArgs): Promise<SolveEngineOutcome> {\n\t\tconst { definitionSource, identity } = this.resolveDefinitionSource(args);\n\t\tconst startedAt = performance.now();\n\t\tconst elapsed = () => performance.now() - startedAt;\n\n\t\t// `addEventListener` never fires for a signal that already aborted, so a client gone during\n\t\t// the host's own awaits would otherwise still pay for a solve.\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: 0 };\n\n\t\tlet client: CachedClient;\n\t\ttry {\n\t\t\tclient = await this.clientCache.getClient(args.server, {\n\t\t\t\tdefinitionGuid: args.definitionGuid\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\t\t\tif (error instanceof ComputeError && error.code === ErrorCodes.ABORTED) {\n\t\t\t\treturn interrupted(elapsed());\n\t\t\t}\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\n\t\tconst treeBuildStart = performance.now();\n\t\tlet inputTree;\n\t\ttry {\n\t\t\tinputTree = buildSolveInputTree(args.inputs, args.values);\n\t\t} catch (error) {\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t\t// A live solve never shares a flight: the request carries its owner's callback, through which\n\t\t// that owner's Esc aborts the Grasshopper solve. Shared, one user's cancel would abort everyone\n\t\t// who joined, and the joiners would get no progress events.\n\t\tconst live = args.selvaEvents ? `:live:${args.selvaEvents.solveId}` : '';\n\t\tconst flight = await this.singleFlight.run(\n\t\t\t`${identity}:${args.server.id}:${this.clientGeneration(client)}:${stableStringify(inputTree)}${live}`,\n\t\t\t(signal) =>\n\t\t\t\trunSolvePipeline({\n\t\t\t\t\tdefinitionSource,\n\t\t\t\t\tinputTree,\n\t\t\t\t\tclient,\n\t\t\t\t\tresponseMaxBytes: this.limits.computeResponseMaxBytes,\n\t\t\t\t\tselvaEvents: args.selvaEvents,\n\t\t\t\t\tsignal\n\t\t\t\t}),\n\t\t\targs.signal\n\t\t);\n\n\t\t// Before encoding: a caller that left a shared flight must not stringify for nobody.\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\t\tif (flight.kind !== 'solved') return flight;\n\n\t\tlet encoded;\n\t\ttry {\n\t\t\tencoded = await flight.encoder.encode(args.acceptEncoding ?? '');\n\t\t} catch (error) {\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tif (encoded === 'too_large') return { kind: 'too_large' };\n\n\t\tconst { body, serializedBytes, serializeMs, gzipMs } = encoded;\n\t\tconst { solveMs, response } = flight;\n\t\tconst serverTotalMs = performance.now() - (args.loadStartMs ?? startedAt);\n\t\tconst timing = {\n\t\t\tdefLoadMs: args.defLoadMs ?? 0,\n\t\t\ttreeBuildMs,\n\t\t\tsolveMs,\n\t\t\tserializeMs,\n\t\t\tgzipMs,\n\t\t\tserverTotalMs,\n\t\t\trhino: flight.rhino,\n\t\t\tsettle: flight.settle,\n\t\t\t...(flight.defBytes ? { defBytes: flight.defBytes } : {})\n\t\t};\n\t\tconst serverTiming = buildServerTiming({ ...timing, prepMarks: args.prepMarks ?? [] });\n\n\t\treturn {\n\t\t\tkind: 'ok',\n\t\t\tenvelope: buildEnvelope(encoded, response, serverTiming, {\n\t\t\t\t...timing,\n\t\t\t\tserializedBytes,\n\t\t\t\tcompressedBytes: encoded.encoding === 'gzip' ? (body as Uint8Array).byteLength : null\n\t\t\t}),\n\t\t\tsolveMs,\n\t\t\terrorCount: response.errors?.length ?? 0,\n\t\t\twarningCount: response.warnings?.length ?? 0\n\t\t};\n\t}\n\n\t/**\n\t * The response for an outcome; the mapping from outcome to status lives here. Throws\n\t * `compute_error`'s error: what it means for the caller is the host's to decide.\n\t */\n\ttoWebResponse(outcome: SolveEngineOutcome): Response {\n\t\tconst fail = (status: number, body: { message: string; retryAfter?: number }) =>\n\t\t\tnew Response(JSON.stringify(body), {\n\t\t\t\tstatus,\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t...(body.retryAfter !== undefined ? { 'Retry-After': String(body.retryAfter) } : {})\n\t\t\t\t}\n\t\t\t});\n\n\t\tswitch (outcome.kind) {\n\t\t\tcase 'ok':\n\t\t\t\t// The cast is a TS gap: `Uint8Array<ArrayBufferLike>` is not a body type, though it works.\n\t\t\t\treturn new Response(outcome.envelope.body as ConstructorParameters<typeof Response>[0], {\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: outcome.envelope.headers\n\t\t\t\t});\n\t\t\tcase 'timeout':\n\t\t\t\treturn fail(504, { message: outcome.message });\n\t\t\tcase 'client_abort':\n\t\t\t\treturn fail(499, { message: 'Client closed request' });\n\t\t\tcase 'too_large':\n\t\t\t\treturn fail(413, {\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'Solve result is too large to return. This usually means a file output exceeds the supported size.'\n\t\t\t\t});\n\t\t\tcase 'request_too_large':\n\t\t\t\treturn fail(413, { message: outcome.message });\n\t\t\tcase 'shed':\n\t\t\t\treturn fail(503, { message: outcome.message, retryAfter: outcome.retryAfterSeconds });\n\t\t\tcase 'compute_error':\n\t\t\t\tthrow outcome.error;\n\t\t}\n\t}\n\n\t/**\n\t * Part of the coalesce key: after an eviction the id stays and the client changes, and a new\n\t * caller joining a draining flight would get an answer from the old URL or key.\n\t */\n\tprivate clientGeneration(client: CachedClient): number {\n\t\tlet generation = this.clientGenerations.get(client);\n\t\tif (generation === undefined) {\n\t\t\tgeneration = ++this.lastClientGeneration;\n\t\t\tthis.clientGenerations.set(client, generation);\n\t\t}\n\t\treturn generation;\n\t}\n\n\tprivate resolveDefinitionSource(args: SolveEngineSolveArgs): {\n\t\tdefinitionSource: SolveDefinition;\n\t\tidentity: string;\n\t} {\n\t\tconst source = args.definitionSource;\n\n\t\tif (isByteCacheRef(source) || isDefinitionRef(source)) {\n\t\t\treturn { definitionSource: source, identity: source.key };\n\t\t}\n\n\t\t// Raw Uint8Array | string — no natural identity; require the caller's own.\n\t\tif (!args.definitionKey) {\n\t\t\tthrow new Error(\n\t\t\t\t'SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.'\n\t\t\t);\n\t\t}\n\t\treturn { definitionSource: source, identity: args.definitionKey };\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;AAOA,MAAM,uBAAuB;;;;;;;AAQ7B,MAAM,gCAAgC;;;;;;;;;;;AAoBtC,SAAgB,uBACf,QACA,UACmB;CACnB,IAAI,gBAAgB;CACpB,IAAI,cAAc;CAClB,IAAI,UAAU;;CAGd,eAAe,QAAQ,WAAmD;EACzE,IAAI,SAAS;EACb,UAAU;EACV,IAAI;GACH,MAAM,WAAW,MAAM,QAAQ,QAC9B,OAAO,aAAa,kBAAkB,EAAE,YAAY,MAAM,CAAC,CAC5D,CAAC,CAAC,YAAY,IAAI;GAClB,cAAc,KAAK,IAAI;GACvB,MAAM,OAAO,CAAC,YAAY,YAAY,IAAI,uBAAuB;GACjE,IAAI,SAAS,eAAe;GAC5B,SACC,sCAAsC,cAAc,KAAK,KAAK,sBAC/D;GACA,gBAAgB;GAChB,WAAW,iBAAiB,IAAI;EACjC,UAAU;GACT,UAAU;EACX;CACD;CAEA,OAAO;EACN,IAAI,gBAAgB;GACnB,OAAO;EACR;EACA,eAAe,QAAQ;EACvB,aAAa,WAAW;GACvB,IAAI,KAAK,IAAI,IAAI,eAAe,+BAA+B,QAAa,SAAS;EACtF;CACD;AACD;;;ACtEA,MAAM,0BAA0B;;AAGhC,MAAM,YAAY,IAAI,MAAM;CAAE,gBAAgB;CAAG,aAAa;AAAE,CAAC;AAOjE,MAAM,mBAAmB,OAAe,SACvCA,MAAY,OAAO;CAClB,GAAG;CACH,QAAQ,MAAM,UAAU,YAAY,QAAQ,uBAAuB;CACnE,YAAY;AACb,CAAC;;AAGF,SAAgB,iBAAiB,YAA8C;CAC9E,OAAO,cAAc,0BAA0B,iBAAiB,KAAA;AACjE;;;ACpBA,MAAa,4BAA4B;;;;;AAMzC,MAAa,wBAAwB;;AAGrC,MAAM,oBAAoB;AAc1B,SAAgB,eAAe,UAA4C;CAC1E,IAAI,CAAC,UACJ,OAAO;EACN,iBAAiB;EACjB,iBAAiB;EACjB,sBAAsB;CACvB;CAED,OAAO;EACN,iBAAiB,KAAK,IACrB,SAAS,iBAAiB,MAAO,mBACjC,qBACD;EACA,iBAAiB,SAAS;EAC1B,sBAAsB,SAAS;CAChC;AACD;AAEA,SAAgB,gBAAgB,GAAgB,GAAyB;CACxE,OACC,EAAE,oBAAoB,EAAE,mBACxB,EAAE,oBAAoB,EAAE,mBACxB,EAAE,yBAAyB,EAAE;AAE/B;;AC4EA,MAAM,mCAAmC;;;;;AAMzC,MAAM,oBAAoB;;;;;;;;;;AAW1B,MAAM,sBAAsB;AAwB5B,SAAS,qBAAqB,QAAgC;CAI7D,MAAM,QAAQ,CAAC,yBAHC,OAAO,YACpB,+CACA,+BAC6C,IAAI,KAAK,MAAM,OAAO,UAAU,EAAE,IAAI;CACtF,IAAI,OAAO,WAAW,OAAO;CAE7B,MAAM,KACL,OAAO,yBAAyB,OAC7B,+EACA,OAAO,yBAAyB,QAC/B,+EACA,qEACL;CACA,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,GAE5C,MAAM,KAAK,yBAAyB,OAAO,OAAO,2CAA2C;CAE9F,OAAO;AACR;AAEA,SAAgB,kBAAkB,QAAwC;CACzE,MAAM,wBAAwB,OAAO,yBAAyB;CAC9D,MAAM,wBAAQ,IAAI,IAA0B;CAI5C,MAAM,0BAAU,IAAI,IAA0B;CAG9C,MAAM,yBAAS,IAAI,IAAyB;CAE5C,MAAM,2BAAW,IAAI,IAAkB;CAEvC,IAAI,OAAO,UAAU,WAAW,mBAAmB;CAEnD,MAAM,YAAY,YAAoB;EACrC,IAAI,OAAO,OAAO,OAAO,aAAa,OAAO;CAC9C;CAEA,SAAS,OAAO,EAAE,aAA2B;EAC5C,OAAO,UAAU,kBAAkB,KAAK,UAAU,eAAe;CAClE;;CAGA,SAAS,OAAO,OAAqB;EACpC,IAAI,OAAO,KAAK,GAAG,MAAM,UAAU,QAAQ;OACtC,SAAS,IAAI,KAAK;CACxB;CAEA,SAAS,eAAe,OAAqB;EAC5C,IAAI,CAAC,SAAS,IAAI,KAAK,KAAK,CAAC,OAAO,KAAK,GAAG;EAC5C,SAAS,OAAO,KAAK;EACrB,MAAM,UAAU,QAAQ;CACzB;CAEA,eAAe,MAAM,QAAwB,gBAAgD;EAC5F,MAAM,eAAe,OAAO,UAAU;EAGtC,MAAM,YAAY,iBAAiB,qBAAqB;EAExD,MAAM,eAAyC;GAC9C,WAAW,OAAO;GAClB,QAAQ,OAAO;GAGf,OAAO;GACP,YAAY,OAAO;GACnB,kBAAkB,OAAO;GACzB,GAAI,aAAa,EAAE,OAAO,UAAU;EACrC;EAEA,IAAI,OAAO,OACV,aAAa,kBAAkB,MAAM;GACpC,IAAI,EAAE,UAAU;IACf,SACC,oFACD;IACA;GACD;GACA,MAAM,SAAS,EAAE,UAAU;GAC3B,MAAM,QAAQ,EAAE,SAAS;GACzB,MAAM,SAAS,EAAE,UAAU;GAG3B,MAAM,WACL,UAAU,IAAI,gDAAgD;GAC/D,MAAM,aAAa,OAAO,aAAa,wBAAwB;GAC/D,MAAM,MAAM,EAAE,aAAa,UAAU,EAAE,WAAW,IAAI,GAAG,EAAE,WAAW,MAAM,YAAY;GACxF,SACC,gCAAgC,OAAO,WAAW,MAAM,YAAY,OAAO,yBACnD,SAAS,+BAA+B,aAAa,KAC9E;EACD;EAKD,IAAI,gBACH,aAAa,UAAU,EAAE,sBAAsB,eAAe;EAG/D,MAAM,SAAS,MAAM,kBAAkB,OAAO,YAAY;EAE1D,MAAM,QAAQ,uBAAuB,QAAQ,QAAQ;EACrD,MAAM,aAAa,YAClB,eACE,MAAM,QAAQ,QAAQ,OAAO,aAAa,YAAY,CAAC,CAAC,CAAC,YAAY,IAAI,KAAM,IACjF;EACD,MAAM,CAAC,UAAU,MAAM,QAAQ,IAAI,CAAC,WAAW,GAAG,MAAM,QAAQ,CAAC,CAAC;EAClE,SACC,iCAAiC,OAAO,GAAG,aAAa,OAAO,gBAAgB,kBAC/D,OAAO,mBAAmB,eAAe,kBAAkB,OAAO,sBACnF;EACA,IAAI,kBAAkB,KAAK,IAAI;EAC/B,IAAI,iBAAiB;EACrB,MAAM,2BAA2B;GAChC,IAAI,kBAAkB,KAAK,IAAI,IAAI,kBAAkB,mBAAmB;GACxE,iBAAiB;GACjB,WAAgB,CAAC,CACf,MAAM,SAAS;IACf,kBAAkB,KAAK,IAAI;IAC3B,IAAI,gBAAgB,MAAM,MAAM,GAAG;IACnC,SAAS,iCAAiC,OAAO,GAAG,6BAA6B;IACjF,IAAI,MAAM,IAAI,OAAO,EAAE,MAAM,OAAO,WAAW,OAAO,EAAE;GACzD,CAAC,CAAC,CACD,cAAc;IACd,iBAAiB;GAClB,CAAC;EACH;EAEA,MAAM,YAAY,OAAO,gBAAgB;GACxC,MAAM;GAIN,eAAe,MAAM;GAIrB,eAAe,OAAO,iBAAiB,KAAA;GACvC,aAAa,OAAO,eAAe,KAAA;GACnC,WAAW,OAAO;GAKlB,OAAO,OAAO,wBAAwB,IAAI,EAAE,UAAU,OAAO,sBAAsB,IAAI;GACvF,4BAA4B,OAAO;GACnC,WAAW,MAAM,WAAW;IAE3B,qBAAqB,eAAe,KAAK,CAAC;IAC1C,IAAI,OAAO,WAAW,WAAW;IAIjC,IAAI,OAAO,cAAc,MAAM;KAC9B,MAAM,aAAa,SAAS;KAC5B,mBAAmB;IACpB;IACA,IAAI,OAAO,OAAO,qBAAqB,MAAM,CAAC,CAAC,QAAQ,QAAQ;GAChE;EACD,CAAC;EACD,MAAM,QAAsB;GAAE;GAAQ;GAAW;EAAO;EACxD,OAAO;CACR;CAEA,SAAS,WAAW,IAAY;EAC/B,MAAM,QAAQ,MAAM,IAAI,EAAE;EAC1B,IAAI,CAAC,OAAO;EACZ,MAAM,OAAO,EAAE;EACf,OAAO,KAAK;CACb;CAEA,OAAO;EACN,MAAM,UAAU,QAAQ,MAA6B;GACpD,MAAM,MAAM,OAAO;GACnB,MAAM,WAAW,MAAM,IAAI,GAAG;GAC9B,IAAI,UAAU;IAEb,MAAM,OAAO,GAAG;IAChB,MAAM,IAAI,KAAK,QAAQ;IACvB,OAAO;GACR;GAEA,MAAM,WAAW,QAAQ,IAAI,GAAG;GAChC,IAAI,UAAU,OAAO,SAAS;GAK9B,MAAM,gBAAgB,OAAO,IAAI,GAAG;GACpC,IAAI,eAAe;IAClB,IAAI,KAAK,IAAI,IAAI,cAAc,KAAK,qBAAqB;KACxD,SACC,iCAAiC,IAAI,oBAAoB,oBAAoB,yBAC9E;KACA,MAAM,cAAc;IACrB;IACA,OAAO,OAAO,GAAG;GAClB;GAIA,SAAS,iEAAiE,KAAK;GAC/E,MAAM,QAAQ,EAAE,OAAO,MAAM;GAC7B,MAAM,WAAW,YAAY;IAC5B,IAAI;IACJ,IAAI;KACH,QAAQ,MAAM,MAAM,QAAQ,MAAM,cAAc;IACjD,SAAS,KAAK;KACb,IAAI,CAAC,MAAM,OAAO,OAAO,IAAI,KAAK;MAAE,OAAO;MAAK,IAAI,KAAK,IAAI;KAAE,CAAC;KAChE,MAAM;IACP;IAIA,IAAI,MAAM,OAAO;KAChB,MAAM,UAAU,QAAQ;KACxB,MAAM,IAAI,aACT,kBAAkB,IAAI,6BACtB,WAAW,OACZ;IACD;IAEA,IAAI,MAAM,QAAQ,uBAAuB;KACxC,MAAM,YAAY,MAAM,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;KACtC,IAAI,cAAc,KAAA,GAAW;MAC5B,SACC,6DAA6D,UAAU,QAAQ,sBAAsB,EACtG;MACA,MAAM,SAAS,MAAM,IAAI,SAAS;MAClC,MAAM,OAAO,SAAS;MACtB,IAAI,QAAQ,OAAO,MAAM;KAC1B;IACD;IAEA,MAAM,IAAI,KAAK,KAAK;IACpB,OAAO;GACR,EAAA,CAAG,CAAC,CAAC,cAAc;IAClB,IAAI,QAAQ,IAAI,GAAG,CAAC,EAAE,UAAU,OAAO,QAAQ,OAAO,GAAG;GAC1D,CAAC;GACD,QAAQ,IAAI,KAAK;IAAE;IAAS;GAAM,CAAC;GACnC,OAAO;EACR;EAEA,MAAM,IAAU;GAGf,OAAO,OAAO,EAAE;GAChB,MAAM,QAAQ,QAAQ,IAAI,EAAE;GAC5B,IAAI,OAAO;IACV,MAAM,MAAM,QAAQ;IACpB,QAAQ,OAAO,EAAE;GAClB;GACA,IAAI,MAAM,IAAI,EAAE,GAAG;IAClB,SAAS,yDAAyD,GAAG,iBAAiB;IACtF,WAAW,EAAE;GACd;EACD;EAEA,kBAAmC;GAClC,MAAM,QAAyB;IAC9B,aAAa,MAAM;IACnB,SAAS;IACT,OAAO;IACP,MAAM;IACN,QAAQ;IACR,WAAW;GACZ;GACA,KAAK,MAAM,SAAS,MAAM,OAAO,GAAG;IACnC,MAAM,IAAI,MAAM,UAAU,WAAW;IACrC,MAAM,WAAW,EAAE;IACnB,MAAM,SAAS,EAAE;IACjB,MAAM,QAAQ,EAAE;IAChB,MAAM,UAAU,EAAE;IAClB,MAAM,aAAa,EAAE;GACtB;GACA,OAAO;EACR;EAEA,mBAAyB;GACxB,SAAS,kDAAkD,MAAM,KAAK,gBAAgB;GACtF,KAAK,MAAM,SAAS,MAAM,OAAO,GAAG,MAAM,UAAU,WAAW;EAChE;EAEA,aAAmB;GAClB,SAAS,wCAAwC,MAAM,KAAK,gBAAgB;GAC5E,KAAK,MAAM,SAAS,CAAC,GAAG,MAAM,OAAO,GAAG,GAAG,QAAQ,GAAG,MAAM,UAAU,QAAQ;GAC9E,KAAK,MAAM,SAAS,QAAQ,OAAO,GAAG,MAAM,MAAM,QAAQ;GAC1D,QAAQ,MAAM;GACd,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,OAAO,MAAM;EACd;CACD;AACD;;;;;;;;;;;;;;;;;;;;;;;ACzZA,SAAgB,eAAe,QAAyC;CACvE,OAAO,OAAO,WAAW,YAAY,WAAW,QAAQ,aAAa;AACtE;;;;;AAaA,SAAgB,0BAA0B,UAAuC;CAChF,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC;CAG/C,MAAM,wBAAQ,IAAI,IAAwB;CAC1C,MAAM,0BAAU,IAAI,IAAqB;CACzC,IAAI,gBAAgB;CACpB,MAAM,QAAQ;EAAE,MAAM;EAAG,QAAQ;EAAG,WAAW;CAAE;CAEjD,SAAS,aAAmB;EAC3B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO;GACjC,IAAI,iBAAiB,QAAQ;GAC7B,MAAM,OAAO,GAAG;GAChB,iBAAiB,MAAM;GACvB,MAAM,aAAa;EACpB;CACD;CAEA,SAAS,MAAM,KAAa,OAAyB;EACpD,IAAI,WAAW,GAAG;EAGlB,IAAI,MAAM,aAAa,QAAQ;EAC/B,MAAM,IAAI,KAAK,KAAK;EACpB,iBAAiB,MAAM;EACvB,WAAW;CACZ;CAEA,SAAS,UAAU,KAAa,MAA6D;EAC5F,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,QAAiB;GACtB;GACA,SAAS;GACT,UAAU,YAAY;IACrB,MAAM,QAAQ,MAAM,KAAK,WAAW,MAAM;IAC1C,MAAM,KAAK,KAAK;IAChB,OAAO;GACR,EAAA,CAAG,CAAC,CAAC,cAAc;IAClB,IAAI,QAAQ,IAAI,GAAG,MAAM,OAAO,QAAQ,OAAO,GAAG;GACnD,CAAC;EACF;EACA,QAAQ,IAAI,KAAK,KAAK;EACtB,OAAO;CACR;;;;;CAMA,eAAe,KAAK,OAAgB,QAA2C;EAC9E,MAAM,WAAW;EACjB,IAAI,CAAC,QAAQ,OAAO,MAAM;EAC1B,IAAI;GACH,OAAO,MAAM,UAAU,MAAM,SAAS,MAAM;EAC7C,UAAU;GACT,MAAM,WAAW;GACjB,IAAI,MAAM,YAAY,KAAK,OAAO,SAAS,MAAM,WAAW,MAAM;EACnE;CACD;CAEA,OAAO;EACN,UAAU,WAAW,MAAM;GAC1B,MAAM,UAA0B;IAAE,QAAQ;IAAO,WAAW;GAAM;GAClE,OAAO;IACN,KAAK;IACL;IACA,MAAM,eAAe,KAAK,QAA6B;KACtD,QAAQ,SAAS;KACjB,MAAM,MAAM,MAAM,IAAI,SAAS;KAC/B,IAAI,KAAK;MACR,MAAM,OAAO,SAAS;MACtB,MAAM,IAAI,WAAW,GAAG;MACxB,MAAM,QAAQ;MACd,QAAQ,YAAY;MACpB,OAAO;KACR;KACA,MAAM,UAAU,QAAQ,IAAI,SAAS;KACrC,IAAI,SAAS,WAAW,OAAO,SAAS;MAGvC,MAAM,YAAY,QAAQ,QAAQ,WAC3B,KAAA,SACA,KAAA,CACP;MACA,OAAO,SAAS,UAAU,WAAW,MAAM,IAAI;MAC/C,OAAO,KAAK,MAAM;KACnB;KACA,IAAI,SAAS;MACZ,MAAM,QAAQ;MACd,QAAQ,YAAY;MACpB,OAAO,KAAK,SAAS,MAAM;KAC5B;KACA,MAAM,UAAU;KAChB,OAAO,KAAK,UAAU,WAAW,IAAI,GAAG,MAAM;IAC/C;GACD;EACD;EACA,QAAQ;GACP,OAAO;IACN,MAAM,MAAM;IACZ,QAAQ,MAAM;IACd,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,OAAO;GACR;EACD;EACA,QAAQ;GACP,MAAM,MAAM;GACZ,gBAAgB;EACjB;CACD;AACD;;;;;;AC1KA,MAAM,YAAY,UAAU,IAAI;;;;;AAMhC,MAAM,aAAa;;AAGnB,MAAM,iBAAiB;;AAOvB,MAAa,2BAA2B;AACxC,MAAa,yBAAyB;AA4CtC,SAAgB,cACf,SACA,QACA,cACA,SACgB;CAChB,MAAM,UAAkC;EACvC,gBAAgB;EAChB,iBAAiB;GAChB,yBAAyB,OAAA,CAA+B;EACzD,MAAM;CACP;CACA,MAAM,EAAE,MAAM,aAAa;CAC3B,IAAI,aAAa,QAGhB,QAAQ,sBAAsB;CAE/B,QAAQ,oBAAoB,OAC3B,OAAO,SAAS,WAAW,OAAO,WAAW,IAAI,IAAI,KAAK,UAC3D;CACA,OAAO;EAAE;EAAM,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAAI;EAAS;EAAQ;CAAQ;AAC5E;;AAOA,SAAgB,YAAY,gBAAiC;CAC5D,OAAO,eAAe,MAAM,GAAG,CAAC,CAAC,MAAM,SAAS;EAC/C,MAAM,CAAC,QAAQ,GAAG,UAAU,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,YAAY,CAAC;EAC7E,IAAI,WAAW,QAAQ,OAAO;EAC9B,MAAM,IAAI,OAAO,MAAM,MAAM,EAAE,WAAW,IAAI,CAAC;EAC/C,OAAO,MAAM,KAAA,KAAa,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI;CAChD,CAAC;AACF;;;;;;;AAaA,MAAM,gCAAgB,IAAI,QAAiD;AAE3E,SAAS,SAAS,UAAkB,MAAc,iBAAyB;CAC1E,MAAM,UAAU,UAAU,OAAO,KAAK,IAAI,GAAG,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,MAAM,UAAU;EACnF;EACA;CACD,EAAE;CACF,cAAc,IAAI,UAAU,OAAO;CAEnC,QAAQ,YAAY,cAAc,OAAO,QAAQ,CAAC;CAClD,OAAO;AACR;;;;;AAWA,SAAgB,sBAAsB,UAAkB,UAAmC;CAC1F,IAAI;CAEJ,SAAS,YAAY;EACpB,IAAI,eAAe,KAAA,GAAW,OAAO;EACrC,IAAI;GACH,MAAM,OAAO,KAAK,UAAU,QAAQ;GACpC,MAAM,QAAQ,OAAO,WAAW,IAAI;GACpC,aAAa,QAAQ,WAAW,cAAc;IAAE;IAAM;GAAM;EAC7D,SAAS,KAAK;GAGb,IAAI,EAAE,eAAe,aAAa,MAAM;GACxC,aAAa;EACd;EACA,OAAO;CACR;CAEA,OAAO,EACN,MAAM,OAAO,gBAAgB;EAC5B,MAAM,YAAY,YAAY,cAAc;EAC5C,IAAI,UAAU,YAAY,cAAc,IAAI,QAAQ,IAAI,KAAA;EACxD,IAAI,cAAc;EAElB,IAAI,CAAC,SAAS;GACb,MAAM,iBAAiB,YAAY,IAAI;GACvC,MAAM,QAAQ,UAAU;GACxB,cAAc,YAAY,IAAI,IAAI;GAClC,IAAI,UAAU,aAAa;IAC1B,IAAI,WAAW,cAAc,IAAI,UAAU,QAAQ,QAAQ,WAAW,CAAC;IACvE,OAAO;GACR;GACA,IAAI,CAAC,aAAa,MAAM,SAAS,gBAChC,OAAO;IAAE,MAAM,MAAM;IAAM,iBAAiB,MAAM;IAAO;IAAa,QAAQ;GAAE;GAEjF,UAAU,SAAS,UAAU,MAAM,MAAM,MAAM,KAAK;EACrD;EAEA,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,UAAU,MAAM;EACtB,IAAI,YAAY,aAAa,OAAO;EACpC,OAAO;GACN,MAAM,QAAQ;GACd,UAAU;GACV,iBAAiB,QAAQ;GACzB;GACA,QAAQ,YAAY,IAAI,IAAI;EAC7B;CACD,EACD;AACD;;;;;;;;;AC7JA,SAAgB,kBAAkB,OAAkC;CACnE,IAAI,SACH,YAAY,MAAM,UAAU,QAAQ,CAAC,EAAE,aAC3B,MAAM,YAAY,QAAQ,CAAC,EAAE,cAC5B,MAAM,QAAQ,QAAQ,CAAC,EAAE,kBACrB,MAAM,YAAY,QAAQ,CAAC,EAAE,aAClC,MAAM,OAAO,QAAQ,CAAC,EAAE,cACvB,MAAM,cAAc,QAAQ,CAAC;CAE3C,MAAM,QAAQ,MAAM;CACpB,IAAI,OAAO;EACV,MAAM,SAAS,MAAM,UAAU;EAC/B,MAAM,QAAQ,MAAM,SAAS;EAC7B,MAAM,SAAS,MAAM,UAAU;EAC/B,MAAM,gBAAgB,KAAK,IAAI,GAAG,MAAM,UAAU,SAAS,QAAQ,MAAM;EACzE,UACC,sBAAsB,OAAO,QAAQ,CAAC,EAAA,oBACjB,MAAM,QAAQ,CAAC,EAAA,qBACd,OAAO,QAAQ,CAAC,EAAA,qBAChB,cAAc,QAAQ,CAAC,EAAA,oBACxB,MAAM,WAAW,IAAI;EAC3C,IAAI,MAAM,OAAO,KAAA,GAAW,UAAU,kBAAkB,MAAM,GAAG,QAAQ,CAAC;EAC1E,IAAI,MAAM,YACT,UAAU,4BAA4B,MAAM,WAAW,IAAI,GAAG,MAAM,WAAW,MAAM;CAEvF;CAEA,KAAK,MAAM,CAAC,OAAO,OAAO,MAAM,WAC/B,UAAU,OAAO,MAAM,OAAO,GAAG,QAAQ,CAAC;CAG3C,MAAM,SAAS,MAAM;CACrB,UAAU,qBAAqB,OAAO,YAAY,IAAI;CACtD,IAAI,OAAO,yBAAyB,KAAA,GACnC,UAAU,sBAAsB,OAAO,uBAAuB,IAAI;CAGnE,IAAI,MAAM,UAAU,UAAU,oBAAoB,MAAM;CAExD,OAAO;AACR;;ACnCA,SAAgB,6BACf,UAAyC,CAAC,GACjB;CACzB,MAAM,2BAAW,IAAI,IAAoB;CAEzC,SAAS,KAAK,KAAa,QAAgB;EAE1C,IAAI,SAAS,IAAI,GAAG,MAAM,QAAQ,SAAS,OAAO,GAAG;CACtD;CAEA,SAAS,OAAU,KAAa,QAAgB,QAAiC;EAChF,OAAO,WAAW;EAGlB,MAAM,cAAc;GACnB,OAAO,WAAW;GAClB,IAAI,OAAO,UAAU,GAAG;GACxB,KAAK,KAAK,MAAM;GAChB,OAAO,WAAW,MAAM;EACzB;EACA,OAAO,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;EACtD,OAAQ,OAAO,QAAuB,cAAc,OAAO,oBAAoB,SAAS,KAAK,CAAC;CAC/F;CAEA,OAAO;EACN,IAAO,KAAa,MAA2C,cAA2B;GACzF,MAAM,WAAW,SAAS,IAAI,GAAG;GACjC,IAAI,UAAU;IACb,IAAI;KACH,QAAQ,SAAS,GAAG;IACrB,SAAS,KAAK;KAEb,QAAQ,MAAM,uCAAuC,GAAG;IACzD;IACA,OAAO,OAAU,KAAK,UAAU,YAAY;GAC7C;GAEA,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,SAAiB;IACtB;IACA,SAAS;IAET,UAAU,YAAY,KAAK,WAAW,MAAM,EAAA,CAAG,CAAC,CAAC,cAAc,KAAK,KAAK,MAAM,CAAC;GACjF;GACA,SAAS,IAAI,KAAK,MAAM;GACxB,OAAO,OAAU,KAAK,QAAQ,YAAY;EAC3C;EACA,WAAmB;GAClB,OAAO,SAAS;EACjB;CACD;AACD;;;;;;ACrFA,SAAgB,sBAAsB,QAAgC;CACrE,MAAM,yBAAS,IAAI,IAAoD;CACvE,KAAK,MAAM,QAAQ,cAAc,MAAM,GACtC,IAAI,KAAK,eAAe,YAAY,KAAK,QAAQ,OAAO,IAAI,KAAK,SAAS,KAAK,MAAM;CAEtF,OAAO,OAAO,OAAO,KAAK,UAAU;EACnC,MAAM,SAAS,OAAO,IAAI,MAAM,EAAE;EAClC,OAAO,SAAS;GAAE,GAAG;GAAO,SAAS,OAAO;GAAS,SAAS,OAAO;EAAQ,IAAI;CAClF,CAAC;AACF;;AAGA,SAAgB,oBACf,QACA,QACa;CACb,OAAO,YAAY,WAClB,OAEE,QAAQ,UAAU,MAAM,SAAS,CAAC,CAClC,KAAK,WAAW;EAChB,WAAW,MAAM;EACjB,WAAW,MAAM;EAEjB,QAAQ,OAAO,OAAO,QAAQ,MAAM,EAAE,IAAI,OAAO,MAAM,MAAM,KAAA,MAAc,MAAM;EACjF,SAAS,MAAM;EACf,SAAS,MAAM;CAChB,EAAE,CACJ;AACD;;;;;;;ACuDA,SAAS,KACR,YACA,QACA,SACe;CACf,OAAO;EAAE,MAAM;EAAQ;EAAY;EAAQ,mBAAmB;EAAG;CAAQ;AAC1E;;AAGA,SAAgB,YAAY,YAAkC;CAC7D,OAAO,KAAK,YAAY,eAAe,2CAA2C;AACnF;AAEA,SAAS,gBAAgB,KAAc,YAAoB,MAAuC;CACjG,IAAI,EAAE,eAAe,eAAe,OAAO;EAAE,MAAM;EAAiB;EAAY,OAAO;CAAI;CAC3F,IAAI,IAAI,eAAe,KAAK;EAC3B,MAAM,MAAM,KAAK,OAAO,OAAO;EAC/B,OAAO;GACN,MAAM;GACN;GACA,SACC,2DACC,MAAM,KAAK,KAAK,MAAM,MAAO,OAAY,EAAE,QAAQ,MACpD;EACF;CACD;CACA,QAAQ,IAAI,MAAZ;EACC,KAAK,WAAW,eACf,OAAO;GACN,MAAM;GACN;GACA,SAAS,sBAAsB,KAAK,MAAM,KAAK,OAAO,OAAO,kBAAkB,GAAI,EAAE;EACtF;EACD,KAAK,WAAW,SACf,OAAO,KAAK,OAAO,UAAU;GAAE,MAAM;GAAgB;EAAW,IAAI,YAAY,UAAU;EAC3F,KAAK,WAAW,oBACf,OAAO,EAAE,MAAM,YAAY;EAG5B,KAAK,WAAW,YACf,OAAO,KAAK,YAAY,cAAc,+CAA+C;EACtF,KAAK,WAAW,eACf,OAAO,KACN,YACA,iBACA,8DACD;EACD,SACC,OAAO;GAAE,MAAM;GAAiB;GAAY,OAAO;EAAI;CACzD;AACD;AAEA,SAAS,gBAAgB,QAAsD;CAC9E,IAAI,CAAC,eAAe,MAAM,GAAG,OAAO,KAAA;CACpC,MAAM,EAAE,QAAQ,cAAc,OAAO;CACrC,OAAO,CAAC,SAAS,YAAY,YAAY,QAAQ;AAClD;AAEA,eAAsB,iBACrB,MACuC;CACvC,IAAI,QAA6B;CACjC,MAAM,aAAa,YAAY,IAAI;CACnC,IAAI;EACH,MAAM,SAAS,MAAM,KAAK,OAAO,UAAU,cAC1C,KAAK,kBACL,KAAK,WACL;GACC,QAAQ,KAAK;GACb,aAAa,KAAK;GAClB,iBAAiB,MAAM;IACtB,QAAQ;GACT;EACD,CACD;EACA,MAAM,WAAW,gBAAgB,KAAK,gBAAgB;EACtD,OAAO;GACN,MAAM;GACN,UAAU,OAAO;GACjB,SAAS,sBAAsB,OAAO,UAAU,KAAK,gBAAgB;GACrE,SAAS,YAAY,IAAI,IAAI;GAC7B;GACA,QAAQ;IAAE,WAAW,OAAO;IAAW,sBAAsB,OAAO;GAAqB;GACzF,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAChC;CACD,SAAS,KAAK;EACb,OAAO,gBAAgB,KAAK,YAAY,IAAI,IAAI,YAAY,IAAI;CACjE;AACD;;;;;;;;;;;;;;;;ACvHA,SAAS,kBAAkB,QAAiC;CAC3D,MAAM,SAAS,MAA+B,QAAgB;EAC7D,MAAM,QAAQ,OAAO;EACrB,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,KACtC,MAAM,IAAI,WAAW,uBAAuB,KAAK,uBAAuB,IAAI,QAAQ,MAAM,EAAE;CAE9F;CACA,MAAM,2BAA2B,CAAC;CAClC,MAAM,wBAAwB,CAAC;CAC/B,MAAM,sBAAsB,CAAC;CAC7B,MAAM,+BAA+B,CAAC;CACtC,MAAM,0BAA0B,CAAC;AAClC;AAyDA,IAAa,cAAb,MAAyB;CACxB;CACA;CACA;CACA;CACA,oCAAqC,IAAI,QAA8B;CACvE,uBAA+B;CAE/B,YAAY,SAA6B;EACxC,kBAAkB,QAAQ,MAAM;EAChC,KAAK,SAAS,QAAQ;EAEtB,KAAK,cAAc,kBAAkB;GACpC,YAAY,KAAK,OAAO;GACxB,eAAe,KAAK,OAAO;GAC3B,aAAa,KAAK,OAAO;GACzB,uBAAuB,KAAK,OAAO;GACnC,kBAAkB,KAAK,OAAO;GAC9B,OAAO,QAAQ,SAAS;GACxB,uBAAuB,QAAQ;GAC/B,YAAY,QAAQ;EACrB,CAAC;EAED,KAAK,YAAY,0BAA0B,KAAK,OAAO,2BAA2B;EAElF,KAAK,eAAe,6BAA6B,EAChD,QAAQ,QAAQ,iBACjB,CAAC;CACF;CAEA,UAAU,QAAwB,MAA2D;EAC5F,OAAO,KAAK,YAAY,UAAU,QAAQ,IAAI;CAC/C;CAEA,cACC,WACA,MACe;EACf,OAAO,KAAK,UAAU,UAAU,WAAW,IAAI;CAChD;;;;;CAMA,YAAY,IAAkB;EAC7B,KAAK,YAAY,MAAM,EAAE;CAC1B;CAEA,mBAAyB;EACxB,KAAK,YAAY,iBAAiB;CACnC;;;;;CAMA,uBAA6B;EAC5B,KAAK,UAAU,MAAM;CACtB;CAEA,QAA0B;EACzB,OAAO;GACN,QAAQ,KAAK,YAAY,gBAAgB;GACzC,iBAAiB,KAAK,UAAU,MAAM;GACtC,YAAY,EAAE,UAAU,KAAK,aAAa,SAAS,EAAE;EACtD;CACD;CAEA,MAAM,MAAM,MAAyD;EACpE,MAAM,EAAE,kBAAkB,aAAa,KAAK,wBAAwB,IAAI;EACxE,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,gBAAgB,YAAY,IAAI,IAAI;EAI1C,IAAI,KAAK,OAAO,SAAS,OAAO;GAAE,MAAM;GAAgB,YAAY;EAAE;EAEtE,IAAI;EACJ,IAAI;GACH,SAAS,MAAM,KAAK,YAAY,UAAU,KAAK,QAAQ,EACtD,gBAAgB,KAAK,eACtB,CAAC;EACF,SAAS,OAAO;GACf,IAAI,KAAK,OAAO,SAAS,OAAO;IAAE,MAAM;IAAgB,YAAY,QAAQ;GAAE;GAC9E,IAAI,iBAAiB,gBAAgB,MAAM,SAAS,WAAW,SAC9D,OAAO,YAAY,QAAQ,CAAC;GAE7B,OAAO;IAAE,MAAM;IAAiB,YAAY,QAAQ;IAAG;GAAM;EAC9D;EACA,IAAI,KAAK,OAAO,SAAS,OAAO;GAAE,MAAM;GAAgB,YAAY,QAAQ;EAAE;EAE9E,MAAM,iBAAiB,YAAY,IAAI;EACvC,IAAI;EACJ,IAAI;GACH,YAAY,oBAAoB,KAAK,QAAQ,KAAK,MAAM;EACzD,SAAS,OAAO;GACf,OAAO;IAAE,MAAM;IAAiB,YAAY,QAAQ;IAAG;GAAM;EAC9D;EACA,MAAM,cAAc,YAAY,IAAI,IAAI;EAKxC,MAAM,OAAO,KAAK,cAAc,SAAS,KAAK,YAAY,YAAY;EACtE,MAAM,SAAS,MAAM,KAAK,aAAa,IACtC,GAAG,SAAS,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,iBAAiB,MAAM,EAAE,GAAG,gBAAgB,SAAS,IAAI,SAC9F,WACA,iBAAiB;GAChB;GACA;GACA;GACA,kBAAkB,KAAK,OAAO;GAC9B,aAAa,KAAK;GAClB;EACD,CAAC,GACF,KAAK,MACN;EAGA,IAAI,KAAK,OAAO,SAAS,OAAO;GAAE,MAAM;GAAgB,YAAY,QAAQ;EAAE;EAC9E,IAAI,OAAO,SAAS,UAAU,OAAO;EAErC,IAAI;EACJ,IAAI;GACH,UAAU,MAAM,OAAO,QAAQ,OAAO,KAAK,kBAAkB,EAAE;EAChE,SAAS,OAAO;GACf,OAAO;IAAE,MAAM;IAAiB,YAAY,QAAQ;IAAG;GAAM;EAC9D;EACA,IAAI,YAAY,aAAa,OAAO,EAAE,MAAM,YAAY;EAExD,MAAM,EAAE,MAAM,iBAAiB,aAAa,WAAW;EACvD,MAAM,EAAE,SAAS,aAAa;EAC9B,MAAM,gBAAgB,YAAY,IAAI,KAAK,KAAK,eAAe;EAC/D,MAAM,SAAS;GACd,WAAW,KAAK,aAAa;GAC7B;GACA;GACA;GACA;GACA;GACA,OAAO,OAAO;GACd,QAAQ,OAAO;GACf,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,IAAI,CAAC;EACxD;EACA,MAAM,eAAe,kBAAkB;GAAE,GAAG;GAAQ,WAAW,KAAK,aAAa,CAAC;EAAE,CAAC;EAErF,OAAO;GACN,MAAM;GACN,UAAU,cAAc,SAAS,UAAU,cAAc;IACxD,GAAG;IACH;IACA,iBAAiB,QAAQ,aAAa,SAAU,KAAoB,aAAa;GAClF,CAAC;GACD;GACA,YAAY,SAAS,QAAQ,UAAU;GACvC,cAAc,SAAS,UAAU,UAAU;EAC5C;CACD;;;;;CAMA,cAAc,SAAuC;EACpD,MAAM,QAAQ,QAAgB,SAC7B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;GAClC;GACA,SAAS;IACR,gBAAgB;IAChB,GAAI,KAAK,eAAe,KAAA,IAAY,EAAE,eAAe,OAAO,KAAK,UAAU,EAAE,IAAI,CAAC;GACnF;EACD,CAAC;EAEF,QAAQ,QAAQ,MAAhB;GACC,KAAK,MAEJ,OAAO,IAAI,SAAS,QAAQ,SAAS,MAAmD;IACvF,QAAQ;IACR,SAAS,QAAQ,SAAS;GAC3B,CAAC;GACF,KAAK,WACJ,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ,QAAQ,CAAC;GAC9C,KAAK,gBACJ,OAAO,KAAK,KAAK,EAAE,SAAS,wBAAwB,CAAC;GACtD,KAAK,aACJ,OAAO,KAAK,KAAK,EAChB,SACC,oGACF,CAAC;GACF,KAAK,qBACJ,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ,QAAQ,CAAC;GAC9C,KAAK,QACJ,OAAO,KAAK,KAAK;IAAE,SAAS,QAAQ;IAAS,YAAY,QAAQ;GAAkB,CAAC;GACrF,KAAK,iBACJ,MAAM,QAAQ;EAChB;CACD;;;;;CAMA,iBAAyB,QAA8B;EACtD,IAAI,aAAa,KAAK,kBAAkB,IAAI,MAAM;EAClD,IAAI,eAAe,KAAA,GAAW;GAC7B,aAAa,EAAE,KAAK;GACpB,KAAK,kBAAkB,IAAI,QAAQ,UAAU;EAC9C;EACA,OAAO;CACR;CAEA,wBAAgC,MAG9B;EACD,MAAM,SAAS,KAAK;EAEpB,IAAI,eAAe,MAAM,KAAK,gBAAgB,MAAM,GACnD,OAAO;GAAE,kBAAkB;GAAQ,UAAU,OAAO;EAAI;EAIzD,IAAI,CAAC,KAAK,eACT,MAAM,IAAI,MACT,gGACD;EAED,OAAO;GAAE,kBAAkB;GAAQ,UAAU,KAAK;EAAc;CACjE;AACD"}
|
|
1
|
+
{"version":3,"file":"server.js","names":["undiciFetch"],"sources":["../src/server/concurrency-probe.ts","../src/server/long-fetch.ts","../src/server/server-limits.ts","../src/server/client-cache.ts","../src/server/definition-byte-cache.ts","../src/server/response-encoding.ts","../src/server/server-timing.ts","../src/server/solve-cache-single-flight.ts","../src/server/transform-input.ts","../src/server/solve-pipeline.ts","../src/server/solve-engine.ts"],"sourcesContent":["import type { GrasshopperClient } from '@selvajs/compute/grasshopper';\n\n/**\n * Fallback concurrency while the first probe is still in flight, and the value\n * kept whenever a probe fails or reports 0 — never send more than a single\n * solve to a server whose real capacity is unknown.\n */\nconst FALLBACK_CONCURRENCY = 1;\n\n/**\n * How stale the compute server's child count may get before a solve triggers a\n * re-read. Short enough that a resized pool is picked up within a few solves,\n * long enough that a busy server is probed rarely — the probe rides on solve\n * completion, so an idle server is never polled at all.\n */\nconst CONCURRENCY_PROBE_INTERVAL_MS = 5 * 60 * 1000;\n\nexport interface ConcurrencyProbe {\n\treadonly maxConcurrent: number;\n\t/** One blocking read at connect, so the first solves already use the right cap. */\n\trefresh(): Promise<void>;\n\t/** Piggybacks on real traffic: at most once per window, and never on the request path. */\n\trefreshIfDue(scheduler: { setMaxConcurrent(n: number): void }): void;\n}\n\n/**\n * rhino.compute hands out children in strict rotation and never reports \"busy\", so nothing\n * downstream enforces a concurrency cap — send more than it has children and requests silently\n * double up on one. Rhino.Compute is the only source of truth for its own child count, so this\n * always probes rather than trusting an operator-supplied guess.\n *\n * The count is not static: children exit on their own under `-idlespan`, and `/launch-child` +\n * `/recycle-children` resize the pool at runtime. So the probe re-runs after solves rather than\n * only at connect.\n */\nexport function createConcurrencyProbe(\n\tclient: Pick<GrasshopperClient, 'serverStats'>,\n\tdebugLog: (message: string) => void\n): ConcurrencyProbe {\n\tlet maxConcurrent = FALLBACK_CONCURRENCY;\n\tlet lastProbeAt = 0;\n\tlet probing = false;\n\n\t/** `initialize: false` keeps this from spawning children on an idle server, so a cold one reports 0. */\n\tasync function refresh(scheduler?: { setMaxConcurrent(n: number): void }) {\n\t\tif (probing) return;\n\t\tprobing = true;\n\t\ttry {\n\t\t\tconst children = await Promise.resolve(\n\t\t\t\tclient.serverStats?.getActiveChildren({ initialize: false })\n\t\t\t).catch(() => null);\n\t\t\tlastProbeAt = Date.now();\n\t\t\tconst next = !children || children <= 0 ? FALLBACK_CONCURRENCY : children;\n\t\t\tif (next === maxConcurrent) return;\n\t\t\tdebugLog(\n\t\t\t\t`[Compute/client-cache] concurrency ${maxConcurrent} → ${next} (server child count)`\n\t\t\t);\n\t\t\tmaxConcurrent = next;\n\t\t\tscheduler?.setMaxConcurrent(next);\n\t\t} finally {\n\t\t\tprobing = false;\n\t\t}\n\t}\n\n\treturn {\n\t\tget maxConcurrent() {\n\t\t\treturn maxConcurrent;\n\t\t},\n\t\trefresh: () => refresh(),\n\t\trefreshIfDue(scheduler) {\n\t\t\tif (Date.now() - lastProbeAt >= CONCURRENCY_PROBE_INTERVAL_MS) void refresh(scheduler);\n\t\t}\n\t};\n}\n","import { Agent, fetch as undiciFetch } from 'undici';\n\n/** undici's default `headersTimeout` and `bodyTimeout`, which `timeoutMs` can't extend. */\nconst UNDICI_DEFAULT_LIMIT_MS = 300_000;\n\n/** Both off, not raised: the scheduler's `timeoutMs` stays the one deadline on a solve. */\nconst unlimited = new Agent({ headersTimeout: 0, bodyTimeout: 0 });\n\n// npm undici's own fetch, not the global one: a dispatcher has to match the undici that calls it,\n// and Node's bundled copy is a different version.\n//\n// A call with no signal has no deadline of its own (`getIO` is one), so it keeps undici's limit:\n// switching the timeouts off for it would let a hung server hang a page load forever.\nconst unlimitedFetch = ((input: string, init?: { signal?: AbortSignal }) =>\n\tundiciFetch(input, {\n\t\t...init,\n\t\tsignal: init?.signal ?? AbortSignal.timeout(UNDICI_DEFAULT_LIMIT_MS),\n\t\tdispatcher: unlimited\n\t})) as unknown as typeof fetch;\n\n/** The fetch for a solve deadline: `undefined` means the global one reaches it. */\nexport function fetchForDeadline(deadlineMs: number): typeof fetch | undefined {\n\treturn deadlineMs >= UNDICI_DEFAULT_LIMIT_MS ? unlimitedFetch : undefined;\n}\n","import type { ServerLimits } from '@selvajs/compute/grasshopper';\n\n/** Rhino.Compute's own default `--timeout`, for a server that can't report its own. */\nexport const DEFAULT_SOLVE_DEADLINE_MS = 100_000;\n\n/**\n * Ceiling on any solve deadline, whatever the server reports. Bounds how long a request, its\n * live-event token and a delegated session stay open; past an hour a solve belongs in a job.\n */\nexport const MAX_SOLVE_DEADLINE_MS = 60 * 60_000;\n\n/** Selva waits this much past the server's timeout, so Compute's own error arrives first. */\nconst DEADLINE_SLACK_MS = 5_000;\n\n/** What Selva holds a solve to on one compute server. */\nexport interface SolveLimits {\n\tsolveDeadlineMs: number;\n\t/** `null` when the server didn't say: its own 413 is then the only check. */\n\tmaxRequestBytes: number | null;\n\t/**\n\t * Send a pointer to the server's cached definition instead of the `.gh`. Only on a server that\n\t * answers a stale pointer with `definition_not_cached`: one that doesn't returns empty geometry.\n\t */\n\treuseDefinitionCache: boolean;\n\t/** The server's deployment (`ServerLimits.deploymentId`); another one may solve differently. */\n\tdeploymentId: string | null;\n}\n\nexport function solveLimitsFor(reported: ServerLimits | null): SolveLimits {\n\tif (!reported) {\n\t\treturn {\n\t\t\tsolveDeadlineMs: DEFAULT_SOLVE_DEADLINE_MS,\n\t\t\tmaxRequestBytes: null,\n\t\t\treuseDefinitionCache: false,\n\t\t\tdeploymentId: null\n\t\t};\n\t}\n\treturn {\n\t\tsolveDeadlineMs: Math.min(\n\t\t\treported.timeoutSeconds * 1000 + DEADLINE_SLACK_MS,\n\t\t\tMAX_SOLVE_DEADLINE_MS\n\t\t),\n\t\tmaxRequestBytes: reported.maxRequestBytes,\n\t\treuseDefinitionCache: reported.signalsStaleDefinition,\n\t\t// An older `@selvajs/compute` leaves it undefined.\n\t\tdeploymentId: reported.deploymentId ?? null\n\t};\n}\n\nexport function sameSolveLimits(a: SolveLimits, b: SolveLimits): boolean {\n\treturn (\n\t\ta.solveDeadlineMs === b.solveDeadlineMs &&\n\t\ta.maxRequestBytes === b.maxRequestBytes &&\n\t\ta.reuseDefinitionCache === b.reuseDefinitionCache &&\n\t\ta.deploymentId === b.deploymentId\n\t);\n}\n","import {\n\tGrasshopperClient,\n\ttype GrasshopperComputeConfig,\n\ttype SolveScheduler\n} from '@selvajs/compute/grasshopper';\nimport { ComputeError, enableDebugLogging, ErrorCodes } from '@selvajs/compute/core';\n\nimport { createConcurrencyProbe } from './concurrency-probe';\nimport { fetchForDeadline } from './long-fetch';\nimport {\n\tMAX_SOLVE_DEADLINE_MS,\n\tsameSolveLimits,\n\tsolveLimitsFor,\n\ttype SolveLimits\n} from './server-limits';\n\n// ============================================================================\n// Shared compute client cache\n// ============================================================================\n//\n// One warm `GrasshopperClient` (+ its `SolveScheduler`) per distinct compute\n// server, keyed by the server's `id`, never its URL — a rotated URL/apiKey\n// keeps the same key with stale connection details, so the config-write path\n// MUST call `evict(id)` when a server's config changes.\n//\n// Per-server LRU, not a single shared client: definitions can pin different\n// servers, so two definitions on two servers keep two warm clients, with\n// churn from one-off servers bounded by `maxWarmComputeServers`.\n//\n// Both hot paths share this cache — the solve endpoint uses the entry's\n// `scheduler`; the definition-viewer render path uses the entry's `client`\n// for `getIO` — so a definition that was just solved renders from the same\n// warm client instead of re-handshaking Rhino.Compute per page load.\n\n/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */\nexport interface ResolvedServer {\n\tid: string;\n\tserverUrl: string;\n\t/** Sent as the `RhinoComputeKey` header. */\n\tapiKey?: string;\n}\n\nexport interface CachedClient {\n\tclient: GrasshopperClient;\n\tscheduler: SolveScheduler;\n\t/** Read from the server when the client was built; a changed server rebuilds the client. */\n\tlimits: SolveLimits;\n}\n\n/**\n * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs\n * (`onDebugLog`), `'verbose'` also enables full lib-level request/response\n * dumps (incl. geometry) — a superset of `true`, not an independent setting.\n */\nexport type ClientCacheDebug = boolean | 'verbose';\n\n/** Config injected by the consuming app, so this module stays env-agnostic and testable. */\nexport interface ClientCacheConfig {\n\tcachesolve: boolean;\n\t/**\n\t * Max solves that may wait in the FIFO queue, excluding in-flight solves\n\t * (capped at `maxConcurrent`, itself driven by the server's probed child\n\t * count). `0` = unbounded (`ComputeLimits.computeMaxQueueDepth`); a full\n\t * queue rejects new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Max ms a solve may sit queued before executing; `0` = no deadline\n\t * (`ComputeLimits.computeQueueWaitMs`). Too long a wait rejects with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache. Applies per warm\n\t * client — worst-case heap is this × `maxWarmComputeServers`. `0` disables\n\t * the cache (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\t/**\n\t * Largest compute response body read; past it the download is cancelled (`too_large`).\n\t * Default: no limit.\n\t */\n\tmaxResponseBytes?: number;\n\tdebug: ClientCacheDebug;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. */\n\tmaxWarmComputeServers?: number;\n\t/** Sink for the debug lines. `onDebugLog` only fires when `debug` is not `false`. */\n\tonDebugLog?: (message: string) => void;\n}\n\nexport interface ClientCache {\n\t/**\n\t * Get (or create) the warm client + scheduler for a resolved compute server, keyed by its `id`.\n\t * With the solve cache on, a warm client last checked over 30 s ago first re-reads the server's\n\t * `/limits` (at most 1 s), and one that reports another deployment is rebuilt with an empty cache.\n\t *\n\t * @throws {ComputeError} `ABORTED` when `evict` or `disposeAll` overtakes the build.\n\t */\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient>;\n\t/**\n\t * Drops the warm client for `id`, so the next request rebuilds, and disposes it once idle. A\n\t * build in flight for `id` is abandoned.\n\t */\n\tevict(id: string): void;\n\t/** Solve-cache counters summed across every warm client. Counters die with the client that owns them, so totals can fall over time. */\n\tsolveCacheStats(): SolveCacheStats;\n\t/**\n\t * Drop every retained solve result, keeping the warm clients (and their\n\t * connections and server-side definition pointers) intact. Nothing expires\n\t * these on its own — the byte budget is the only other pressure — so this is\n\t * the operator's release valve when a definition's inputs no longer describe\n\t * its output, e.g. it reads an external source that has since changed.\n\t */\n\tclearSolveCaches(): void;\n\t/** Dispose every warm client now, running solves included. Test seam / shutdown hook. */\n\tdisposeAll(): void;\n}\n\nexport interface SolveCacheStats {\n\twarmClients: number;\n\tentries: number;\n\tbytes: number;\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped under size/byte pressure (not replacement or manual clears). */\n\tevictions: number;\n}\n\nconst DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;\n\n/**\n * How often a warm client re-reads its server's limits, riding on solve completion like the\n * concurrency probe. An operator who changes `--timeout` sees it within this window.\n */\nconst LIMITS_RECHECK_MS = 5 * 60 * 1000;\n\n/**\n * How stale the server's `deploymentId` may be before a warm client whose solve cache could\n * answer is handed out again. A cache hit never reaches the server, so riding on solve completion\n * would let a redeployed plugin's old results be served until the next miss.\n */\nconst DEPLOYMENT_RECHECK_MS = 30 * 1000;\n\n/**\n * Longest a request waits on that check. The proxy answers `/limits` itself, in milliseconds; an\n * unreachable server must not hold a request that the cache could answer. The check finishes\n * behind it and retires the client if the deployment changed.\n */\nconst DEPLOYMENT_CHECK_WAIT_MS = 1000;\n\n/**\n * How long a failed build is remembered before the next `getClient` retries the\n * handshake. Without this, every solve against a down server pays the full probe\n * ladder again, so a user clicking Solve twice waits twice.\n *\n * Short on purpose: this is a convenience for the rapid-retry case, not a\n * circuit breaker. An operator who starts the compute VM should not have to wait\n * out a long penalty window before the app notices.\n */\nconst FAILED_BUILD_TTL_MS = 5000;\n\n/**\n * A build failure worth replaying to the next caller, with the time it happened\n * so {@link FAILED_BUILD_TTL_MS} can expire it.\n */\ninterface FailedBuild {\n\terror: unknown;\n\tat: number;\n}\n\ninterface PendingBuild {\n\tpromise: Promise<CachedClient>;\n\t/** Set when the connection details this build started from stopped being true. */\n\ttoken: { stale: boolean };\n}\n\ninterface SettledSolve {\n\tfromCache?: boolean;\n\tdurationMs: number;\n\tdefinitionReuploaded?: boolean;\n\tresponse?: { errors?: unknown };\n}\n\nfunction describeSettledSolve(result: SettledSolve): string[] {\n\tconst verdict = result.fromCache\n\t\t? 'HIT — served from cache (no compute call)'\n\t\t: 'miss — went to Rhino.Compute';\n\tconst lines = [`[Compute/selva-cache] ${verdict} (${Math.round(result.durationMs)}ms)`];\n\tif (result.fromCache) return lines;\n\n\tlines.push(\n\t\tresult.definitionReuploaded === true\n\t\t\t? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'\n\t\t\t: result.definitionReuploaded === false\n\t\t\t\t? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'\n\t\t\t\t: '[Compute/def-cache] n/a — reuse disabled or non-reusable definition'\n\t);\n\tconst errors = result.response?.errors;\n\tif (Array.isArray(errors) && errors.length > 0) {\n\t\t// A count: solver text can quote user input, and logs are out of erasure's reach.\n\t\tlines.push(`[Compute/rhino-cache] ${errors.length} GH error(s); not cached by Rhino.Compute.`);\n\t}\n\treturn lines;\n}\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxWarmComputeServers = config.maxWarmComputeServers ?? DEFAULT_MAX_WARM_COMPUTE_SERVERS;\n\tconst cacheEnabled = config.responseCacheMaxBytes > 0;\n\tconst cache = new Map<string, CachedClient>();\n\t// Builds in flight, keyed like `cache`. Concurrent `getClient` calls for the\n\t// same id must share ONE build: without this, both miss, both handshake, and\n\t// the loser's entry is overwritten in the map without ever being disposed.\n\tconst pending = new Map<string, PendingBuild>();\n\t// Recent build failures, keyed like `cache`. Cleared by `evict` so a config\n\t// fix takes effect immediately rather than waiting out the TTL.\n\tconst failed = new Map<string, FailedBuild>();\n\t// Out of `cache`, waiting for their running solves to finish.\n\tconst draining = new Set<CachedClient>();\n\t// Per warm client: resolves false once the server reports another deployment (see\n\t// DEPLOYMENT_RECHECK_MS), true while it may still serve from its solve cache.\n\tconst deploymentChecks = new WeakMap<CachedClient, () => Promise<boolean>>();\n\n\tif (config.debug === 'verbose') enableDebugLogging();\n\n\tconst debugLog = (message: string) => {\n\t\tif (config.debug) config.onDebugLog?.(message);\n\t};\n\n\tfunction isIdle({ scheduler }: CachedClient) {\n\t\treturn scheduler.inFlightCount === 0 && scheduler.queueDepth === 0;\n\t}\n\n\t/** Disposing a busy scheduler aborts its solves, so a config write would fail them all. */\n\tfunction retire(entry: CachedClient) {\n\t\tif (isIdle(entry)) entry.scheduler.dispose();\n\t\telse draining.add(entry);\n\t}\n\n\tfunction finishDraining(entry: CachedClient) {\n\t\tif (!draining.has(entry) || !isIdle(entry)) return;\n\t\tdraining.delete(entry);\n\t\tentry.scheduler.dispose();\n\t}\n\n\tasync function build(server: ResolvedServer, definitionGuid?: string): Promise<CachedClient> {\n\t\tconst debugVerbose = config.debug === 'verbose';\n\t\t// The deadline comes from the server, read only once the client exists: pick the fetch\n\t\t// for the longest one. Calls without a signal keep undici's limits either way.\n\t\tconst longFetch = fetchForDeadline(MAX_SOLVE_DEADLINE_MS);\n\n\t\tconst clientConfig: GrasshopperComputeConfig = {\n\t\t\tserverUrl: server.serverUrl,\n\t\t\tapiKey: server.apiKey,\n\t\t\t// Lib-level debug is the verbose one; keep it on the verbose flag so the\n\t\t\t// concise cache logs can run without the noise.\n\t\t\tdebug: debugVerbose,\n\t\t\tcachesolve: config.cachesolve,\n\t\t\tmaxResponseBytes: config.maxResponseBytes,\n\t\t\t...(longFetch && { fetch: longFetch })\n\t\t};\n\n\t\tif (config.debug) {\n\t\t\tclientConfig.onServerTiming = (t) => {\n\t\t\t\tif (t.cacheHit) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t'[Compute/rhino-cache] solve-cache (cachesolve): HIT (cached result, solve skipped)'\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst decode = t.decode ?? 0;\n\t\t\t\tconst solve = t.solve ?? 0;\n\t\t\t\tconst encode = t.encode ?? 0;\n\t\t\t\t// Heuristic threshold: a genuine parse is many ms; a reused definition is\n\t\t\t\t// sub-millisecond. Only meaningful relative to the first (cold) solve.\n\t\t\t\tconst defCache =\n\t\t\t\t\tdecode <= 1 ? 'HIT (definition reused, no re-upload/parse)' : 'miss (full parse)';\n\t\t\t\tconst solveCache = config.cachesolve ? 'miss (solved fresh)' : 'off';\n\t\t\t\tconst ran = t.recomputed ? ` | ran ${t.recomputed.ran}/${t.recomputed.total} objects` : '';\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/rhino-cache] decode=${decode}ms solve=${solve}ms encode=${encode}ms ` +\n\t\t\t\t\t\t`| definition-cache: ${defCache} | solve-cache (cachesolve): ${solveCache}${ran}`\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\n\t\t// Baked at client-create time because the scheduler has no per-request\n\t\t// header hook.\n\t\tif (definitionGuid) {\n\t\t\tclientConfig.headers = { 'X-Selva-Definition': definitionGuid };\n\t\t}\n\n\t\tconst client = await GrasshopperClient.create(clientConfig);\n\n\t\tconst probe = createConcurrencyProbe(client, debugLog);\n\t\tconst readReported = async () =>\n\t\t\t(await Promise.resolve(client.serverStats?.getLimits?.()).catch(() => null)) ?? null;\n\t\tconst [reported] = await Promise.all([readReported(), probe.refresh()]);\n\t\tconst limits = solveLimitsFor(reported);\n\t\tdebugLog(\n\t\t\t`[Compute/client-cache] server ${server.id}: deadline ${limits.solveDeadlineMs}ms, ` +\n\t\t\t\t`request cap ${limits.maxRequestBytes ?? 'not reported'}, pointer reuse ${limits.reuseDefinitionCache}`\n\t\t);\n\t\tlet limitsCheckedAt = Date.now();\n\t\tlet recheck: Promise<boolean> | null = null;\n\t\t/** Re-reads the limits; true when they changed, which retires this client. */\n\t\tconst recheckLimits = () =>\n\t\t\t(recheck ??= readReported()\n\t\t\t\t.then((next) => {\n\t\t\t\t\tlimitsCheckedAt = Date.now();\n\t\t\t\t\t// Unanswered by a server that did answer: down or restarting, not reconfigured.\n\t\t\t\t\t// Its defaults would switch pointer reuse off until the next recheck.\n\t\t\t\t\tif (next === null && reported !== null) return false;\n\t\t\t\t\tif (sameSolveLimits(solveLimitsFor(next), limits)) return false;\n\t\t\t\t\tdebugLog(`[Compute/client-cache] server ${server.id} limits changed — rebuilding`);\n\t\t\t\t\tif (cache.get(server.id) === entry) evictEntry(server.id);\n\t\t\t\t\treturn true;\n\t\t\t\t})\n\t\t\t\t.finally(() => {\n\t\t\t\t\trecheck = null;\n\t\t\t\t}));\n\t\tconst recheckLimitsIfDue = () => {\n\t\t\tif (Date.now() - limitsCheckedAt >= LIMITS_RECHECK_MS) void recheckLimits();\n\t\t};\n\n\t\tconst scheduler = client.createScheduler({\n\t\t\tmode: 'queue',\n\t\t\t// Queue mode defaults maxConcurrent to 1, serializing every solve on\n\t\t\t// this server through a single slot while the compute VM's other\n\t\t\t// compute.geometry children idle — always pass it explicitly.\n\t\t\tmaxConcurrent: probe.maxConcurrent,\n\t\t\t// The scheduler treats undefined as unbounded/no-deadline, so map our\n\t\t\t// `0`-means-off convention to undefined rather than passing 0 (which\n\t\t\t// would reject EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: limits.solveDeadlineMs,\n\t\t\t// The byte budget is the only eviction pressure. A solve is a pure function of\n\t\t\t// (definition, inputs) on one deployment: expiring a result only forces a paid\n\t\t\t// re-solve of the same answer. A redeployed plugin is another deployment, so the\n\t\t\t// client is retired with its cache (`deploymentChecks`).\n\t\t\tcache: cacheEnabled ? { maxBytes: config.responseCacheMaxBytes } : false,\n\t\t\treuseServerDefinitionCache: limits.reuseDefinitionCache,\n\t\t\tonSettle: (_ctx, result) => {\n\t\t\t\t// Deferred: this hook fires before the scheduler drops the solve from its in-flight set.\n\t\t\t\tqueueMicrotask(() => finishDraining(entry));\n\t\t\t\tif (result.status !== 'success') return;\n\t\t\t\t// A cache hit means we never reached the server, so it says nothing about the pool.\n\t\t\t\t// `scheduler` is safe despite being referenced from inside its own initializer:\n\t\t\t\t// onSettle only ever fires during a solve, long after binding.\n\t\t\t\tif (result.fromCache !== true) {\n\t\t\t\t\tprobe.refreshIfDue(scheduler);\n\t\t\t\t\trecheckLimitsIfDue();\n\t\t\t\t}\n\t\t\t\tif (config.debug) describeSettledSolve(result).forEach(debugLog);\n\t\t\t}\n\t\t});\n\t\tconst entry: CachedClient = { client, scheduler, limits };\n\t\tdeploymentChecks.set(entry, async () => {\n\t\t\tif (Date.now() - limitsCheckedAt < DEPLOYMENT_RECHECK_MS) return true;\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\tconst waited = new Promise<false>((resolve) => {\n\t\t\t\ttimer = setTimeout(() => resolve(false), DEPLOYMENT_CHECK_WAIT_MS);\n\t\t\t});\n\t\t\ttry {\n\t\t\t\treturn !(await Promise.race([recheckLimits(), waited]));\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t}\n\t\t});\n\t\treturn entry;\n\t}\n\n\tfunction evictEntry(id: string) {\n\t\tconst entry = cache.get(id);\n\t\tif (!entry) return;\n\t\tcache.delete(id);\n\t\tretire(entry);\n\t}\n\n\tconst self: ClientCache = {\n\t\tasync getClient(server, opts): Promise<CachedClient> {\n\t\t\tconst key = server.id;\n\t\t\tconst existing = cache.get(key);\n\t\t\tif (existing) {\n\t\t\t\t// Refresh LRU position.\n\t\t\t\tcache.delete(key);\n\t\t\t\tcache.set(key, existing);\n\t\t\t\t// Only a client whose cache could answer waits on its server's deployment.\n\t\t\t\tif (!cacheEnabled || (await deploymentChecks.get(existing)?.()) !== false) return existing;\n\t\t\t\t// Retired by the check: whatever now stands for `key` serves, or a fresh build.\n\t\t\t\treturn self.getClient(server, opts);\n\t\t\t}\n\n\t\t\tconst inFlight = pending.get(key);\n\t\t\tif (inFlight) return inFlight.promise;\n\n\t\t\t// Replay a recent failure instead of re-running the probe ladder. The\n\t\t\t// original error is rethrown verbatim so the caller still sees the real\n\t\t\t// cause (refused / unauthorized / timeout), not a generic cache miss.\n\t\t\tconst recentFailure = failed.get(key);\n\t\t\tif (recentFailure) {\n\t\t\t\tif (Date.now() - recentFailure.at < FAILED_BUILD_TTL_MS) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t`[Compute/client-cache] server ${key} failed to build <${FAILED_BUILD_TTL_MS}ms ago — replaying error`\n\t\t\t\t\t);\n\t\t\t\t\tthrow recentFailure.error;\n\t\t\t\t}\n\t\t\t\tfailed.delete(key);\n\t\t\t}\n\n\t\t\t// A build is a Rhino.Compute handshake — expensive, and repeated builds for\n\t\t\t// the same id mean churn (LRU thrash or config-rotation evictions).\n\t\t\tdebugLog(`[Compute/client-cache] miss — building warm client for server ${key}`);\n\t\t\tconst token = { stale: false };\n\t\t\tconst promise = (async () => {\n\t\t\t\tlet entry: CachedClient;\n\t\t\t\ttry {\n\t\t\t\t\tentry = await build(server, opts?.definitionGuid);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (!token.stale) failed.set(key, { error: err, at: Date.now() });\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\t// Installing it would carry the old URL or key past the eviction, with nothing left to\n\t\t\t\t// evict it again. Its caller holds a client built from those same details, so it fails too.\n\t\t\t\tif (token.stale) {\n\t\t\t\t\tentry.scheduler.dispose();\n\t\t\t\t\tthrow new ComputeError(\n\t\t\t\t\t\t`Compute server ${key} changed while connecting.`,\n\t\t\t\t\t\tErrorCodes.ABORTED\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (cache.size >= maxWarmComputeServers) {\n\t\t\t\t\tconst oldestKey = cache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/client-cache] LRU evicted warm client for server ${oldestKey} (cap ${maxWarmComputeServers})`\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst oldest = cache.get(oldestKey);\n\t\t\t\t\t\tcache.delete(oldestKey);\n\t\t\t\t\t\tif (oldest) retire(oldest);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcache.set(key, entry);\n\t\t\t\treturn entry;\n\t\t\t})().finally(() => {\n\t\t\t\tif (pending.get(key)?.token === token) pending.delete(key);\n\t\t\t});\n\t\t\tpending.set(key, { promise, token });\n\t\t\treturn promise;\n\t\t},\n\n\t\tevict(id): void {\n\t\t\t// Drop any remembered failure too: `evict` means the connection details\n\t\t\t// changed, so the reason the last build failed no longer applies.\n\t\t\tfailed.delete(id);\n\t\t\tconst build = pending.get(id);\n\t\t\tif (build) {\n\t\t\t\tbuild.token.stale = true;\n\t\t\t\tpending.delete(id);\n\t\t\t}\n\t\t\tif (cache.has(id)) {\n\t\t\t\tdebugLog(`[Compute/client-cache] evicted warm client for server ${id} (config change)`);\n\t\t\t\tevictEntry(id);\n\t\t\t}\n\t\t},\n\n\t\tsolveCacheStats(): SolveCacheStats {\n\t\t\tconst total: SolveCacheStats = {\n\t\t\t\twarmClients: cache.size,\n\t\t\t\tentries: 0,\n\t\t\t\tbytes: 0,\n\t\t\t\thits: 0,\n\t\t\t\tmisses: 0,\n\t\t\t\tevictions: 0\n\t\t\t};\n\t\t\tfor (const entry of cache.values()) {\n\t\t\t\tconst s = entry.scheduler.cacheStats();\n\t\t\t\ttotal.entries += s.entries;\n\t\t\t\ttotal.bytes += s.bytes;\n\t\t\t\ttotal.hits += s.hits;\n\t\t\t\ttotal.misses += s.misses;\n\t\t\t\ttotal.evictions += s.evictions;\n\t\t\t}\n\t\t\treturn total;\n\t\t},\n\n\t\tclearSolveCaches(): void {\n\t\t\tdebugLog(`[Compute/client-cache] cleared solve caches on ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.clearCache();\n\t\t},\n\n\t\tdisposeAll(): void {\n\t\t\tdebugLog(`[Compute/client-cache] disposing all ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of [...cache.values(), ...draining]) entry.scheduler.dispose();\n\t\t\tfor (const build of pending.values()) build.token.stale = true;\n\t\t\tpending.clear();\n\t\t\tcache.clear();\n\t\t\tdraining.clear();\n\t\t\tfailed.clear();\n\t\t}\n\t};\n\treturn self;\n}\n","/**\n * In-process cache of `.gh` definition bytes, keyed by immutable **version id**.\n *\n * Turns an eager `storage.get(fileKey)` on every solve request into a lazy\n * `DefinitionRef.load()`: the scheduler calls `load()` only when an upload is\n * unavoidable (a pointer-known re-solve never touches storage or this cache).\n *\n * The key MUST be the version id, never a `fileKey` — a delete-latest-then-\n * reupload can reuse a `fileKey` for different content, which would silently\n * serve one version's bytes for another with no diagnostic. Callers pass\n * `version.id`, and the returned `DefinitionRef.key` is that same id, so the\n * scheduler's own result/pointer cache keys on the same immutable identity.\n *\n * Eviction is LRU by total byte budget, not entry count — definitions range\n * from tens of KB to hundreds of MB, so a count cap would either pin\n * gigabytes or evict uselessly. `COMPUTE_DEFINITION_CACHE_MB` sets the\n * budget (see `createDefinitionByteCache`). No TTL: version ids are\n * immutable, so a cached entry can never go stale. An entry larger than the\n * whole budget is served but never retained.\n */\n\nimport { raceAbort } from '../shared/abort.js';\n\n/**\n * Per-`getOrLoad` outcome, observable after the scheduler has (or hasn't)\n * called `load()` — lets the solve pipeline emit a `def_bytes` Server-Timing\n * verdict (`skipped` / `hit` / `miss`).\n */\nexport interface ByteRefOutcome {\n\tloaded: boolean;\n\tfromCache: boolean;\n}\n\n/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */\nexport interface ByteCacheRef {\n\tkey: string;\n\tload: (signal?: AbortSignal) => Promise<Uint8Array>;\n\toutcome: ByteRefOutcome;\n}\n\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\t/** `load` gets a signal that fires once every caller waiting on this read has gone. */\n\tgetOrLoad(versionId: string, load: (signal: AbortSignal) => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\tclear(): void;\n}\n\n/** A `ByteCacheRef` carries `outcome`, which a plain `DefinitionRef` never has. */\nexport function isByteCacheRef(source: unknown): source is ByteCacheRef {\n\treturn typeof source === 'object' && source !== null && 'outcome' in source;\n}\n\n/** One storage read, shared by every caller that misses while it runs. */\ninterface Loading {\n\tpromise: Promise<Uint8Array>;\n\tcontroller: AbortController;\n\twaiters: number;\n}\n\n/**\n * @param maxBytes total retained-byte budget; `0` (or negative) disables caching\n * entirely — every `load()` calls the loader and nothing is retained.\n */\nexport function createDefinitionByteCache(maxBytes: number): DefinitionByteCache {\n\tconst budget = Math.max(0, Math.floor(maxBytes));\n\t// Map iteration order is insertion order, so it doubles as LRU order:\n\t// re-inserting a key on access moves it to the most-recently-used end.\n\tconst cache = new Map<string, Uint8Array>();\n\tconst loading = new Map<string, Loading>();\n\tlet retainedBytes = 0;\n\tconst stats = { hits: 0, misses: 0, evictions: 0 };\n\n\tfunction evictToFit(): void {\n\t\tfor (const [key, bytes] of cache) {\n\t\t\tif (retainedBytes <= budget) break;\n\t\t\tcache.delete(key);\n\t\t\tretainedBytes -= bytes.byteLength;\n\t\t\tstats.evictions += 1;\n\t\t}\n\t}\n\n\tfunction store(key: string, bytes: Uint8Array): void {\n\t\tif (budget === 0) return;\n\t\t// Caching an entry bigger than the budget would evict everything\n\t\t// (including itself) on the next evictToFit — serve it, cache nothing.\n\t\tif (bytes.byteLength > budget) return;\n\t\tcache.set(key, bytes);\n\t\tretainedBytes += bytes.byteLength;\n\t\tevictToFit();\n\t}\n\n\tfunction startLoad(key: string, load: (signal: AbortSignal) => Promise<Uint8Array>): Loading {\n\t\tconst controller = new AbortController();\n\t\tconst entry: Loading = {\n\t\t\tcontroller,\n\t\t\twaiters: 0,\n\t\t\tpromise: (async () => {\n\t\t\t\tconst bytes = await load(controller.signal);\n\t\t\t\tstore(key, bytes);\n\t\t\t\treturn bytes;\n\t\t\t})().finally(() => {\n\t\t\t\tif (loading.get(key) === entry) loading.delete(key);\n\t\t\t})\n\t\t};\n\t\tloading.set(key, entry);\n\t\treturn entry;\n\t}\n\n\t/**\n\t * A caller gone stops waiting, but the read runs on for whoever else is on it. A caller with no\n\t * signal can't leave, so it stays counted and the read is never aborted under it.\n\t */\n\tasync function wait(entry: Loading, signal?: AbortSignal): Promise<Uint8Array> {\n\t\tentry.waiters += 1;\n\t\tif (!signal) return entry.promise;\n\t\ttry {\n\t\t\treturn await raceAbort(entry.promise, signal);\n\t\t} finally {\n\t\t\tentry.waiters -= 1;\n\t\t\tif (entry.waiters === 0 && signal.aborted) entry.controller.abort();\n\t\t}\n\t}\n\n\treturn {\n\t\tgetOrLoad(versionId, load) {\n\t\t\tconst outcome: ByteRefOutcome = { loaded: false, fromCache: false };\n\t\t\treturn {\n\t\t\t\tkey: versionId,\n\t\t\t\toutcome,\n\t\t\t\tload: async function read(signal): Promise<Uint8Array> {\n\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\tconst hit = cache.get(versionId);\n\t\t\t\t\tif (hit) {\n\t\t\t\t\t\tcache.delete(versionId);\n\t\t\t\t\t\tcache.set(versionId, hit);\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn hit;\n\t\t\t\t\t}\n\t\t\t\t\tconst running = loading.get(versionId);\n\t\t\t\t\tif (running?.controller.signal.aborted) {\n\t\t\t\t\t\t// Abandoned, and a store that ignores its signal may never finish. Waiting it out\n\t\t\t\t\t\t// rather than starting a second read keeps a hung store to one read per version.\n\t\t\t\t\t\tconst woundDown = running.promise.then(\n\t\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t\t\t() => undefined\n\t\t\t\t\t\t);\n\t\t\t\t\t\tawait (signal ? raceAbort(woundDown, signal) : woundDown);\n\t\t\t\t\t\treturn read(signal);\n\t\t\t\t\t}\n\t\t\t\t\tif (running) {\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn wait(running, signal);\n\t\t\t\t\t}\n\t\t\t\t\tstats.misses += 1;\n\t\t\t\t\treturn wait(startLoad(versionId, load), signal);\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tstats() {\n\t\t\treturn {\n\t\t\t\thits: stats.hits,\n\t\t\t\tmisses: stats.misses,\n\t\t\t\tevictions: stats.evictions,\n\t\t\t\tentries: cache.size,\n\t\t\t\tbytes: retainedBytes\n\t\t\t};\n\t\t},\n\t\tclear() {\n\t\t\tcache.clear();\n\t\t\tretainedBytes = 0;\n\t\t}\n\t};\n}\n","/**\n * Turns one solve's response into each caller's wire form. Runs after single-flight, not inside\n * it: coalesced callers can want different encodings, and each reports its own timing.\n */\n\nimport { gzip } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport type { ServerTiming } from '@selvajs/compute/core';\nimport type { GrasshopperComputeResponse } from '@selvajs/compute/grasshopper';\nimport type { CacheVerdict, DefBytesVerdict } from './server-timing.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n/**\n * Display output is base64 of already-deflated bytes, where higher levels find nothing more:\n * level 1 lands within 1% of the default's size, in up to a fifth of the time.\n */\nconst GZIP_LEVEL = 1;\n\n/** Below this gzip isn't worth the CPU for the wire savings. */\nconst GZIP_MIN_BYTES = 1024;\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Envelope\n// ============================================================================\n\n/** What the `Server-Timing` header carries for one caller, as data: read this, don't parse that. */\nexport interface SolvePhaseMetrics {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n\t/** The compute server's own timing; null when it reports none. */\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** Absent for raw-bytes solves. */\n\tdefBytes?: DefBytesVerdict;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\nexport interface EncodedBody {\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\tserializedBytes: number;\n\tserializeMs: number;\n\tgzipMs: number;\n}\n\nexport type TooLarge = 'too_large';\n\nexport function buildEnvelope(\n\tencoded: EncodedBody,\n\tresult: GrasshopperComputeResponse,\n\tserverTiming: string,\n\tmetrics: SolvePhaseMetrics\n): SolveEnvelope {\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\tconst { body, encoding } = encoded;\n\tif (encoding === 'gzip') {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t}\n\theaders['Content-Length'] = String(\n\t\ttypeof body === 'string' ? Buffer.byteLength(body) : body.byteLength\n\t);\n\treturn { body, ...(encoding ? { encoding } : {}), headers, result, metrics };\n}\n\n// ============================================================================\n// Encoding\n// ============================================================================\n\n/** Whether an `Accept-Encoding` header takes gzip. `gzip;q=0` is a client refusing it. */\nexport function acceptsGzip(acceptEncoding: string): boolean {\n\treturn acceptEncoding.split(',').some((part) => {\n\t\tconst [coding, ...params] = part.split(';').map((s) => s.trim().toLowerCase());\n\t\tif (coding !== 'gzip') return false;\n\t\tconst q = params.find((p) => p.startsWith('q='));\n\t\treturn q === undefined || Number(q.slice(2)) > 0;\n\t});\n}\n\ninterface GzippedBody {\n\tbody: Uint8Array;\n\tserializedBytes: number;\n}\n\n/**\n * A scheduler cache hit returns the same response object, so a repeat solve reuses its bytes rather\n * than paying to serialize and compress them again. Entries live exactly as long as the response.\n * Only the gzip form is kept: browsers always take it, and the JSON string would double the memory.\n * Holds the promise, so callers arriving during the compression share it.\n */\nconst gzippedBodies = new WeakMap<object, Promise<GzippedBody | TooLarge>>();\n\nfunction gzipOnce(response: object, json: string, serializedBytes: number) {\n\tconst pending = gzipAsync(Buffer.from(json), { level: GZIP_LEVEL }).then((body) => ({\n\t\tbody,\n\t\tserializedBytes\n\t}));\n\tgzippedBodies.set(response, pending);\n\t// A failed compression must not stay memoized while the response is cached.\n\tpending.catch(() => gzippedBodies.delete(response));\n\treturn pending;\n}\n\nexport interface ResponseEncoder {\n\t/** Buffered, not streamed, so Content-Length is known upfront and a connection cut mid-transfer fails hard instead of silently truncating the JSON. */\n\tencode(acceptEncoding: string): Promise<EncodedBody | TooLarge>;\n}\n\n/**\n * One per solve. The JSON is kept here rather than beside the gzip body: it only has to outlive the\n * callers of this flight, who would otherwise each stringify the same response and hold a copy.\n */\nexport function createResponseEncoder(response: object, maxBytes: number): ResponseEncoder {\n\tlet serialized: { json: string; bytes: number } | TooLarge | undefined;\n\n\tfunction serialize() {\n\t\tif (serialized !== undefined) return serialized;\n\t\ttry {\n\t\t\tconst json = JSON.stringify(response);\n\t\t\tconst bytes = Buffer.byteLength(json);\n\t\t\tserialized = bytes > maxBytes ? 'too_large' : { json, bytes };\n\t\t} catch (err) {\n\t\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t\t// here. Treat it the same as the byte cap.\n\t\t\tif (!(err instanceof RangeError)) throw err;\n\t\t\tserialized = 'too_large';\n\t\t}\n\t\treturn serialized;\n\t}\n\n\treturn {\n\t\tasync encode(acceptEncoding) {\n\t\t\tconst wantsGzip = acceptsGzip(acceptEncoding);\n\t\t\tlet gzipped = wantsGzip ? gzippedBodies.get(response) : undefined;\n\t\t\tlet serializeMs = 0;\n\n\t\t\tif (!gzipped) {\n\t\t\t\tconst serializeStart = performance.now();\n\t\t\t\tconst plain = serialize();\n\t\t\t\tserializeMs = performance.now() - serializeStart;\n\t\t\t\tif (plain === 'too_large') {\n\t\t\t\t\tif (wantsGzip) gzippedBodies.set(response, Promise.resolve('too_large'));\n\t\t\t\t\treturn 'too_large';\n\t\t\t\t}\n\t\t\t\tif (!wantsGzip || plain.bytes <= GZIP_MIN_BYTES) {\n\t\t\t\t\treturn { body: plain.json, serializedBytes: plain.bytes, serializeMs, gzipMs: 0 };\n\t\t\t\t}\n\t\t\t\tgzipped = gzipOnce(response, plain.json, plain.bytes);\n\t\t\t}\n\n\t\t\tconst gzipStart = performance.now();\n\t\t\tconst verdict = await gzipped;\n\t\t\tif (verdict === 'too_large') return verdict;\n\t\t\treturn {\n\t\t\t\tbody: verdict.body,\n\t\t\t\tencoding: 'gzip',\n\t\t\t\tserializedBytes: verdict.serializedBytes,\n\t\t\t\tserializeMs,\n\t\t\t\tgzipMs: performance.now() - gzipStart\n\t\t\t};\n\t\t}\n\t};\n}\n","import type { ServerTiming } from '@selvajs/compute/core';\n\n/** A pre-solve prep sub-phase: `[label, ms]`, surfaced as a `p_*` Server-Timing entry. */\nexport type PrepMark = [string, number];\n\nexport interface CacheVerdict {\n\tfromCache: boolean;\n\tdefinitionReuploaded?: boolean;\n}\n\n/**\n * skipped = scheduler never materialized bytes (pointer-known solve, the whole point of this\n * path); hit = warm byte cache; miss = fell through to storage.\n */\nexport type DefBytesVerdict = 'skipped' | 'hit' | 'miss';\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** Absent for raw-bytes solves. */\n\tdefBytes?: DefBytesVerdict;\n\tprepMarks: PrepMark[];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own timing\n * (VektorNode fork), `rhino_*` is time on the compute server and `compute_link`\n * is everything between it (network + queue wait). `rhino_cache` is 1 when the\n * compute server answered from its own result cache.\n */\nexport function buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst decode = rhino.decode ?? 0;\n\t\tconst solve = rhino.solve ?? 0;\n\t\tconst encode = rhino.encode ?? 0;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - decode - solve - encode);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}` +\n\t\t\t`, rhino_cache;dur=${rhino.cacheHit ? 1 : 0}`;\n\t\tif (rhino.gh !== undefined) header += `, rhino_gh;dur=${rhino.gh.toFixed(1)}`;\n\t\tif (rhino.recomputed) {\n\t\t\theader += `, rhino_recomputed;desc=\"${rhino.recomputed.ran}/${rhino.recomputed.total}\"`;\n\t\t}\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\tif (settle.definitionReuploaded !== undefined) {\n\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t}\n\n\tif (parts.defBytes) header += `, def_bytes;desc=${parts.defBytes}`;\n\n\treturn header;\n}\n","/**\n * In-process single-flight — dogpile protection for every solve.\n *\n * Always on, not gated behind a result cache being configured: the dogpile is worst\n * when nothing else is caching, since N concurrent identical solves each pay a full\n * Rhino round trip (the scheduler doesn't coalesce in-flight requests itself, and\n * Rhino's `cachesolve` still costs a round trip per repeat).\n *\n * Per app instance. No cross-instance lease (Redis `SET NX`) yet.\n *\n * The shared promise resolves to one value for every caller, so `work` must return\n * something safe to share by reference.\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Coalesces concurrent calls under the same key: the first caller runs `work`, later\n\t * callers await the same promise, and the key frees as soon as it settles.\n\t *\n\t * The flight belongs to all its callers. It aborts `work`'s signal when the last one's\n\t * `callerSignal` fires, and frees the key right then, so a caller arriving after that starts\n\t * a fresh flight instead of joining one that is dying. A caller that leaves earlier still\n\t * resolves with the flight: check its own signal before using the result.\n\t */\n\trun<T>(\n\t\tkey: string,\n\t\twork: (signal: AbortSignal) => Promise<T>,\n\t\tcallerSignal: AbortSignal\n\t): Promise<T>;\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\tonJoin?: (key: string) => void;\n}\n\ninterface Flight {\n\tpromise: Promise<unknown>;\n\tcontroller: AbortController;\n\tcallers: number;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\tconst inflight = new Map<string, Flight>();\n\n\tfunction free(key: string, flight: Flight) {\n\t\t// Guard against deleting a key a later caller has already re-taken.\n\t\tif (inflight.get(key) === flight) inflight.delete(key);\n\t}\n\n\tfunction attend<T>(key: string, flight: Flight, signal: AbortSignal): Promise<T> {\n\t\tflight.callers += 1;\n\t\t// `once: true` doesn't detach on a normal completion, and the caller's signal outlives\n\t\t// this call, so without the `finally` every solve leaks a listener onto it.\n\t\tconst leave = () => {\n\t\t\tflight.callers -= 1;\n\t\t\tif (flight.callers > 0) return;\n\t\t\tfree(key, flight);\n\t\t\tflight.controller.abort();\n\t\t};\n\t\tsignal.addEventListener('abort', leave, { once: true });\n\t\treturn (flight.promise as Promise<T>).finally(() => signal.removeEventListener('abort', leave));\n\t}\n\n\treturn {\n\t\trun<T>(key: string, work: (signal: AbortSignal) => Promise<T>, callerSignal: AbortSignal) {\n\t\t\tconst existing = inflight.get(key);\n\t\t\tif (existing) {\n\t\t\t\ttry {\n\t\t\t\t\toptions.onJoin?.(key);\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Telemetry must not fail a solve that is otherwise fine.\n\t\t\t\t\tconsole.error('[Solve/single-flight] onJoin threw:', err);\n\t\t\t\t}\n\t\t\t\treturn attend<T>(key, existing, callerSignal);\n\t\t\t}\n\n\t\t\tconst controller = new AbortController();\n\t\t\tconst flight: Flight = {\n\t\t\t\tcontroller,\n\t\t\t\tcallers: 0,\n\t\t\t\t// Wrapped so a synchronous throw in `work` still rejects instead of escaping `run`.\n\t\t\t\tpromise: (async () => work(controller.signal))().finally(() => free(key, flight))\n\t\t\t};\n\t\t\tinflight.set(key, flight);\n\t\t\treturn attend<T>(key, flight, callerSignal);\n\t\t},\n\t\tinFlight(): number {\n\t\t\treturn inflight.size;\n\t\t}\n\t};\n}\n","import { TreeBuilder, type DataTree } from '@selvajs/compute/grasshopper';\nimport { getInputItems, type UISchema } from '@selvajs/schemas';\nimport type { SolveInput } from '../shared/solve-input.js';\n\n/**\n * What `SolveEngine.solve` takes as `inputs`, from a schema the host stored. The bounds live on\n * the number widgets in the layout, not on `schema.inputs`, so without them nothing clamps.\n */\nexport function solveInputsFromSchema(schema: UISchema): SolveInput[] {\n\tconst bounds = new Map<string, { minimum?: number; maximum?: number }>();\n\tfor (const item of getInputItems(schema)) {\n\t\tif (item.widgetType === 'number' && item.config) bounds.set(item.paramId, item.config);\n\t}\n\treturn schema.inputs.map((input) => {\n\t\tconst widget = bounds.get(input.id);\n\t\treturn widget ? { ...input, minimum: widget.minimum, maximum: widget.maximum } : input;\n\t});\n}\n\n/** The input tree a solve sends: each input's value, or its schema default when it has none. */\nexport function buildSolveInputTree(\n\tinputs: SolveInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromValues(\n\t\tinputs\n\t\t\t// Stored schemas are validated at upload only, so an older one can lack `paramType`.\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input) => ({\n\t\t\t\tparamName: input.nickname,\n\t\t\t\tparamType: input.paramType,\n\t\t\t\t// `hasOwn`: an input id of `constructor` would read the inherited function.\n\t\t\t\tvalue: (Object.hasOwn(values, input.id) ? values[input.id] : undefined) ?? input.default,\n\t\t\t\tminimum: input.minimum,\n\t\t\t\tmaximum: input.maximum\n\t\t\t}))\n\t);\n}\n","/**\n * Runs one solve against a warm scheduler and classifies how it ended: a solved response, or an\n * expected failure as a value instead of a throw. This is the part single-flight shares, so\n * nothing here depends on who is asking.\n */\n\nimport {\n\tErrorCodes,\n\tComputeError,\n\ttype ServerTiming,\n\ttype SolveDefinition\n} from '@selvajs/compute/core';\nimport type {\n\tDataTree,\n\tGrasshopperComputeResponse,\n\tSelvaEventTarget\n} from '@selvajs/compute/grasshopper';\nimport type { CachedClient } from './client-cache.js';\nimport { isByteCacheRef } from './definition-byte-cache.js';\nimport {\n\tcreateResponseEncoder,\n\ttype ResponseEncoder,\n\ttype SolveEnvelope\n} from './response-encoding.js';\nimport type { CacheVerdict, DefBytesVerdict } from './server-timing.js';\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\tinputTree: DataTree[];\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Per-solve live-event callback the compute server hands to the definition; absent when the caller opened no stream. */\n\tselvaEvents?: SelvaEventTarget;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also tells that apart from an abort nobody asked for.\n\t */\n\tsignal: AbortSignal;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure:\n * `SolveEngine.toWebResponse` maps each to a status. `durationMs` on error variants is the solve\n * wall time up to the failure, for the metric record.\n */\nexport type SolveEngineOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| SolveFailure;\n\nexport type SolveFailure =\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** The compute server refused the request body (its `RHINO_COMPUTE_MAX_REQUEST_SIZE`). */\n\t| { kind: 'request_too_large'; durationMs: number; message: string }\n\t/**\n\t * Retryable: no result, through no fault of the request, and `retryAfterSeconds` is a backoff\n\t * hint. Scheduler backpressure rejected the solve before it ran (`queue_full`,\n\t * `queue_timeout`), or its client was shut down under it (`interrupted`).\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout' | 'interrupted';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n/** One finished solve, shared by every caller coalesced onto it. */\nexport interface SolvedFlight {\n\tkind: 'solved';\n\tresponse: GrasshopperComputeResponse;\n\tencoder: ResponseEncoder;\n\tsolveMs: number;\n\trhino: ServerTiming | null;\n\tsettle: CacheVerdict;\n\t/** The verdict of the ref that ran: a coalesced caller's own ref never loads anything. */\n\tdefBytes?: DefBytesVerdict;\n}\n\nfunction shed(\n\tdurationMs: number,\n\treason: 'queue_full' | 'queue_timeout' | 'interrupted',\n\tmessage: string\n): SolveFailure {\n\treturn { kind: 'shed', durationMs, reason, retryAfterSeconds: 1, message };\n}\n\n/** The solve lost its client, and no caller asked for that: a config write or a shutdown. */\nexport function interrupted(durationMs: number): SolveFailure {\n\treturn shed(durationMs, 'interrupted', 'The solve was interrupted. Retry shortly.');\n}\n\nfunction classifyFailure(err: unknown, durationMs: number, args: SolvePipelineArgs): SolveFailure {\n\tif (!(err instanceof ComputeError)) return { kind: 'compute_error', durationMs, error: err };\n\tif (err.statusCode === 413) {\n\t\tconst cap = args.client.limits.maxRequestBytes;\n\t\treturn {\n\t\t\tkind: 'request_too_large',\n\t\t\tdurationMs,\n\t\t\tmessage:\n\t\t\t\t`The inputs are larger than the compute server accepts` +\n\t\t\t\t(cap ? ` (${Math.round(cap / (1024 * 1024))} MB)` : '') +\n\t\t\t\t'. Its admin can raise RHINO_COMPUTE_MAX_REQUEST_SIZE.'\n\t\t};\n\t}\n\tswitch (err.code) {\n\t\tcase ErrorCodes.TIMEOUT_ERROR:\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.client.limits.solveDeadlineMs / 1000)}s deadline set by the compute server.`\n\t\t\t};\n\t\tcase ErrorCodes.ABORTED:\n\t\t\treturn args.signal.aborted ? { kind: 'client_abort', durationMs } : interrupted(durationMs);\n\t\tcase ErrorCodes.RESPONSE_TOO_LARGE:\n\t\t\treturn { kind: 'too_large' };\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tcase ErrorCodes.QUEUE_FULL:\n\t\t\treturn shed(durationMs, 'queue_full', 'Compute server is at capacity. Retry shortly.');\n\t\tcase ErrorCodes.QUEUE_TIMEOUT:\n\t\t\treturn shed(\n\t\t\t\tdurationMs,\n\t\t\t\t'queue_timeout',\n\t\t\t\t'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t);\n\t\tdefault:\n\t\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n}\n\nfunction defBytesVerdict(source: SolveDefinition): DefBytesVerdict | undefined {\n\tif (!isByteCacheRef(source)) return undefined;\n\tconst { loaded, fromCache } = source.outcome;\n\treturn !loaded ? 'skipped' : fromCache ? 'hit' : 'miss';\n}\n\nexport async function runSolvePipeline(\n\targs: SolvePipelineArgs\n): Promise<SolvedFlight | SolveFailure> {\n\tlet rhino: ServerTiming | null = null;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tconst detail = await args.client.scheduler.solveDetailed(\n\t\t\targs.definitionSource,\n\t\t\targs.inputTree,\n\t\t\t{\n\t\t\t\tsignal: args.signal,\n\t\t\t\tselvaevents: args.selvaEvents,\n\t\t\t\tonServerTiming: (t) => {\n\t\t\t\t\trhino = t;\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tconst defBytes = defBytesVerdict(args.definitionSource);\n\t\treturn {\n\t\t\tkind: 'solved',\n\t\t\tresponse: detail.response,\n\t\t\tencoder: createResponseEncoder(detail.response, args.responseMaxBytes),\n\t\t\tsolveMs: performance.now() - solveStart,\n\t\t\trhino,\n\t\t\tsettle: { fromCache: detail.fromCache, definitionReuploaded: detail.definitionReuploaded },\n\t\t\t...(defBytes ? { defBytes } : {})\n\t\t};\n\t} catch (err) {\n\t\treturn classifyFailure(err, performance.now() - solveStart, args);\n\t}\n}\n","/**\n * Facade over the warm-client cache, definition-byte cache, single-flight\n * coalescing, and the solve pipeline — the primitives an app route would\n * otherwise wire up by hand.\n *\n * Doesn't read env itself: `@selvajs/server` owns `resolveComputeLimits`, and\n * this package stays independent of it. Callers resolve their own limits and\n * pass in the fields below.\n *\n * Stays out of auth, DB reads, share tokens, rate limiting, metrics, and\n * compute-server selection — all app policy, supplied per-call via\n * `SolveEngineSolveArgs.server`.\n */\n\nimport {\n\tComputeError,\n\tErrorCodes,\n\tisDefinitionRef,\n\ttype DefinitionRef,\n\ttype SolveDefinition\n} from '@selvajs/compute/core';\nimport { stableStringify, type SelvaEventTarget } from '@selvajs/compute/grasshopper';\nimport {\n\tcreateClientCache,\n\ttype CachedClient,\n\ttype ClientCache,\n\ttype ClientCacheDebug,\n\ttype ResolvedServer,\n\ttype SolveCacheStats\n} from './client-cache.js';\nimport {\n\tcreateDefinitionByteCache,\n\tisByteCacheRef,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype DefinitionByteCache\n} from './definition-byte-cache.js';\nimport { buildEnvelope } from './response-encoding.js';\nimport { buildServerTiming, type PrepMark } from './server-timing.js';\nimport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight\n} from './solve-cache-single-flight.js';\nimport { buildSolveInputTree } from './transform-input.js';\nimport { interrupted, runSolvePipeline, type SolveEngineOutcome } from './solve-pipeline.js';\nimport type { SolveInput } from '../shared/solve-input.js';\n\n/**\n * Subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs.\n * Pass the resolved object through as-is — extra fields are ignored.\n */\nexport interface SolveEngineLimits {\n\tcomputeResponseMaxBytes: number;\n\tcomputeServerCachesolve: boolean;\n\tcomputeMaxQueueDepth: number;\n\tcomputeQueueWaitMs: number;\n\tcomputeDefinitionCacheBytes: number;\n\tcomputeSolveCacheBytes: number;\n}\n\n/** A deadline of 0 means no deadline, and a `NaN` budget retains without limit, both silently. */\nfunction assertValidLimits(limits: SolveEngineLimits): void {\n\tconst check = (name: keyof SolveEngineLimits, min: number) => {\n\t\tconst value = limits[name] as number;\n\t\tif (!Number.isFinite(value) || value < min) {\n\t\t\tthrow new RangeError(`SolveEngine: limits.${name} must be a number >= ${min}, got ${value}.`);\n\t\t}\n\t};\n\tcheck('computeResponseMaxBytes', 1);\n\tcheck('computeMaxQueueDepth', 0);\n\tcheck('computeQueueWaitMs', 0);\n\tcheck('computeDefinitionCacheBytes', 0);\n\tcheck('computeSolveCacheBytes', 0);\n}\n\nexport interface SolveEngineOptions {\n\tlimits: SolveEngineLimits;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. */\n\tmaxWarmComputeServers?: number;\n\t/** Concise cache/timing logs when truthy; `'verbose'` also dumps full lib-level requests/responses. Default off. */\n\tdebug?: ClientCacheDebug;\n\t/** Sink for the debug lines. Without it they go nowhere. */\n\tonDebugLog?: (message: string) => void;\n\t/** Fired when a caller joins an already-in-flight solve instead of running its own. */\n\tonSolveCoalesced?: (key: string) => void;\n}\n\n/**\n * A `ByteCacheRef` obtained ahead of time from `engine.definitionRef()` (e.g. to\n * read bytes for schema extraction before solving) is passed through as-is.\n */\nexport type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef;\n\nexport interface SolveEngineSolveArgs {\n\tserver: ResolvedServer;\n\tdefinitionSource: SolveEngineDefinitionSource;\n\t/**\n\t * Coalesce-key identity for a raw `Uint8Array`/`string` source, which has\n\t * no natural identity the way a `DefinitionRef`'s `.key` does. Required in\n\t * that case — `solve()` throws rather than silently hashing the bytes.\n\t * Ignored for the other source forms.\n\t */\n\tdefinitionKey?: string;\n\t/**\n\t * The definition's inputs, from the schema you stored, never from the request: `id`, `nickname`,\n\t * `paramType` and the bounds decide what reaches the definition, so a body that supplies them\n\t * can drop a limit or fill a slot your host reserved.\n\t */\n\tinputs: SolveInput[];\n\tvalues: Record<string, unknown>;\n\tsignal: AbortSignal;\n\tacceptEncoding?: string;\n\t/**\n\t * Stamped as `X-Selva-Definition` when this solve builds the server's warm client. The header\n\t * is fixed per client, so later solves on that server carry the first definition's guid.\n\t */\n\tdefinitionGuid?: string;\n\tloadStartMs?: number;\n\tdefLoadMs?: number;\n\tprepMarks?: PrepMark[];\n\t/** Per-solve live-event callback. A solve carrying one runs its own compute call, never a shared one. */\n\tselvaEvents?: SelvaEventTarget;\n}\n\nexport interface SolveEngineStats {\n\tclient: SolveCacheStats;\n\tdefinitionBytes: ByteCacheStats;\n\tcoalescing: { inFlight: number };\n}\n\nexport class SolveEngine {\n\tprivate readonly limits: SolveEngineLimits;\n\tprivate readonly clientCache: ClientCache;\n\tprivate readonly byteCache: DefinitionByteCache;\n\tprivate readonly singleFlight: SolveCacheSingleFlight;\n\tprivate readonly clientGenerations = new WeakMap<CachedClient, number>();\n\tprivate lastClientGeneration = 0;\n\n\tconstructor(options: SolveEngineOptions) {\n\t\tassertValidLimits(options.limits);\n\t\tthis.limits = options.limits;\n\n\t\tthis.clientCache = createClientCache({\n\t\t\tcachesolve: this.limits.computeServerCachesolve,\n\t\t\tmaxQueueDepth: this.limits.computeMaxQueueDepth,\n\t\t\tqueueWaitMs: this.limits.computeQueueWaitMs,\n\t\t\tresponseCacheMaxBytes: this.limits.computeSolveCacheBytes,\n\t\t\tmaxResponseBytes: this.limits.computeResponseMaxBytes,\n\t\t\tdebug: options.debug ?? false,\n\t\t\tmaxWarmComputeServers: options.maxWarmComputeServers,\n\t\t\tonDebugLog: options.onDebugLog\n\t\t});\n\n\t\tthis.byteCache = createDefinitionByteCache(this.limits.computeDefinitionCacheBytes);\n\n\t\tthis.singleFlight = createSolveCacheSingleFlight({\n\t\t\tonJoin: options.onSolveCoalesced\n\t\t});\n\t}\n\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient> {\n\t\treturn this.clientCache.getClient(server, opts);\n\t}\n\n\tdefinitionRef(\n\t\tversionId: string,\n\t\tload: (signal: AbortSignal) => Promise<Uint8Array>\n\t): ByteCacheRef {\n\t\treturn this.byteCache.getOrLoad(versionId, load);\n\t}\n\n\t/**\n\t * Drops the warm client for `id`; call it when the server's URL or key changes. Solves already\n\t * running finish on the old connection details first, so a revoked key stays in use until then.\n\t */\n\tevictServer(id: string): void {\n\t\tthis.clientCache.evict(id);\n\t}\n\n\tclearSolveCaches(): void {\n\t\tthis.clientCache.clearSolveCaches();\n\t}\n\n\t/**\n\t * Drop every retained `.gh` byte entry. Safe at any time: entries key on an\n\t * immutable version id, so clearing costs a storage re-read and nothing else.\n\t */\n\tclearDefinitionCache(): void {\n\t\tthis.byteCache.clear();\n\t}\n\n\tstats(): SolveEngineStats {\n\t\treturn {\n\t\t\tclient: this.clientCache.solveCacheStats(),\n\t\t\tdefinitionBytes: this.byteCache.stats(),\n\t\t\tcoalescing: { inFlight: this.singleFlight.inFlight() }\n\t\t};\n\t}\n\n\tasync solve(args: SolveEngineSolveArgs): Promise<SolveEngineOutcome> {\n\t\tconst { definitionSource, identity } = this.resolveDefinitionSource(args);\n\t\tconst startedAt = performance.now();\n\t\tconst elapsed = () => performance.now() - startedAt;\n\n\t\t// `addEventListener` never fires for a signal that already aborted, so a client gone during\n\t\t// the host's own awaits would otherwise still pay for a solve.\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: 0 };\n\n\t\tlet client: CachedClient;\n\t\ttry {\n\t\t\tclient = await this.clientCache.getClient(args.server, {\n\t\t\t\tdefinitionGuid: args.definitionGuid\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\t\t\tif (error instanceof ComputeError && error.code === ErrorCodes.ABORTED) {\n\t\t\t\treturn interrupted(elapsed());\n\t\t\t}\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\n\t\tconst treeBuildStart = performance.now();\n\t\tlet inputTree;\n\t\ttry {\n\t\t\tinputTree = buildSolveInputTree(args.inputs, args.values);\n\t\t} catch (error) {\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t\t// A live solve never shares a flight: the request carries its owner's callback, through which\n\t\t// that owner's Esc aborts the Grasshopper solve. Shared, one user's cancel would abort everyone\n\t\t// who joined, and the joiners would get no progress events.\n\t\tconst live = args.selvaEvents ? `:live:${args.selvaEvents.solveId}` : '';\n\t\tconst flight = await this.singleFlight.run(\n\t\t\t`${identity}:${args.server.id}:${this.clientGeneration(client)}:${stableStringify(inputTree)}${live}`,\n\t\t\t(signal) =>\n\t\t\t\trunSolvePipeline({\n\t\t\t\t\tdefinitionSource,\n\t\t\t\t\tinputTree,\n\t\t\t\t\tclient,\n\t\t\t\t\tresponseMaxBytes: this.limits.computeResponseMaxBytes,\n\t\t\t\t\tselvaEvents: args.selvaEvents,\n\t\t\t\t\tsignal\n\t\t\t\t}),\n\t\t\targs.signal\n\t\t);\n\n\t\t// Before encoding: a caller that left a shared flight must not stringify for nobody.\n\t\tif (args.signal.aborted) return { kind: 'client_abort', durationMs: elapsed() };\n\t\tif (flight.kind !== 'solved') return flight;\n\n\t\tlet encoded;\n\t\ttry {\n\t\t\tencoded = await flight.encoder.encode(args.acceptEncoding ?? '');\n\t\t} catch (error) {\n\t\t\treturn { kind: 'compute_error', durationMs: elapsed(), error };\n\t\t}\n\t\tif (encoded === 'too_large') return { kind: 'too_large' };\n\n\t\tconst { body, serializedBytes, serializeMs, gzipMs } = encoded;\n\t\tconst { solveMs, response } = flight;\n\t\tconst serverTotalMs = performance.now() - (args.loadStartMs ?? startedAt);\n\t\tconst timing = {\n\t\t\tdefLoadMs: args.defLoadMs ?? 0,\n\t\t\ttreeBuildMs,\n\t\t\tsolveMs,\n\t\t\tserializeMs,\n\t\t\tgzipMs,\n\t\t\tserverTotalMs,\n\t\t\trhino: flight.rhino,\n\t\t\tsettle: flight.settle,\n\t\t\t...(flight.defBytes ? { defBytes: flight.defBytes } : {})\n\t\t};\n\t\tconst serverTiming = buildServerTiming({ ...timing, prepMarks: args.prepMarks ?? [] });\n\n\t\treturn {\n\t\t\tkind: 'ok',\n\t\t\tenvelope: buildEnvelope(encoded, response, serverTiming, {\n\t\t\t\t...timing,\n\t\t\t\tserializedBytes,\n\t\t\t\tcompressedBytes: encoded.encoding === 'gzip' ? (body as Uint8Array).byteLength : null\n\t\t\t}),\n\t\t\tsolveMs,\n\t\t\terrorCount: response.errors?.length ?? 0,\n\t\t\twarningCount: response.warnings?.length ?? 0\n\t\t};\n\t}\n\n\t/**\n\t * The response for an outcome; the mapping from outcome to status lives here. Throws\n\t * `compute_error`'s error: what it means for the caller is the host's to decide.\n\t */\n\ttoWebResponse(outcome: SolveEngineOutcome): Response {\n\t\tconst fail = (status: number, body: { message: string; retryAfter?: number }) =>\n\t\t\tnew Response(JSON.stringify(body), {\n\t\t\t\tstatus,\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t...(body.retryAfter !== undefined ? { 'Retry-After': String(body.retryAfter) } : {})\n\t\t\t\t}\n\t\t\t});\n\n\t\tswitch (outcome.kind) {\n\t\t\tcase 'ok':\n\t\t\t\t// The cast is a TS gap: `Uint8Array<ArrayBufferLike>` is not a body type, though it works.\n\t\t\t\treturn new Response(outcome.envelope.body as ConstructorParameters<typeof Response>[0], {\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: outcome.envelope.headers\n\t\t\t\t});\n\t\t\tcase 'timeout':\n\t\t\t\treturn fail(504, { message: outcome.message });\n\t\t\tcase 'client_abort':\n\t\t\t\treturn fail(499, { message: 'Client closed request' });\n\t\t\tcase 'too_large':\n\t\t\t\treturn fail(413, {\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'Solve result is too large to return. This usually means a file output exceeds the supported size.'\n\t\t\t\t});\n\t\t\tcase 'request_too_large':\n\t\t\t\treturn fail(413, { message: outcome.message });\n\t\t\tcase 'shed':\n\t\t\t\treturn fail(503, { message: outcome.message, retryAfter: outcome.retryAfterSeconds });\n\t\t\tcase 'compute_error':\n\t\t\t\tthrow outcome.error;\n\t\t}\n\t}\n\n\t/**\n\t * Part of the coalesce key: after an eviction the id stays and the client changes, and a new\n\t * caller joining a draining flight would get an answer from the old URL or key.\n\t */\n\tprivate clientGeneration(client: CachedClient): number {\n\t\tlet generation = this.clientGenerations.get(client);\n\t\tif (generation === undefined) {\n\t\t\tgeneration = ++this.lastClientGeneration;\n\t\t\tthis.clientGenerations.set(client, generation);\n\t\t}\n\t\treturn generation;\n\t}\n\n\tprivate resolveDefinitionSource(args: SolveEngineSolveArgs): {\n\t\tdefinitionSource: SolveDefinition;\n\t\tidentity: string;\n\t} {\n\t\tconst source = args.definitionSource;\n\n\t\tif (isByteCacheRef(source) || isDefinitionRef(source)) {\n\t\t\treturn { definitionSource: source, identity: source.key };\n\t\t}\n\n\t\t// Raw Uint8Array | string — no natural identity; require the caller's own.\n\t\tif (!args.definitionKey) {\n\t\t\tthrow new Error(\n\t\t\t\t'SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.'\n\t\t\t);\n\t\t}\n\t\treturn { definitionSource: source, identity: args.definitionKey };\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/solve",
|
|
3
|
-
"version": "2.0.0",
|
|
3
|
+
"version": "2.1.0-beta.0",
|
|
4
4
|
"description": "One owner for the Selva solve flow — input change to solve result, on both sides of the wire, with no transport and no UI",
|
|
5
5
|
"author": "VektorNode",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"sideEffects": false,
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"undici": "^8.12.2",
|
|
45
|
-
"@selvajs/compute": "^5.0.
|
|
45
|
+
"@selvajs/compute": "^5.0.1",
|
|
46
46
|
"@selvajs/schemas": "^5.3.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|