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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/solve-pipeline.ts","../src/server/transform-input.ts"],"names":["transformInputParameter","input","value","raw","processed","processInput","effective","gzipAsync","promisify","COMPUTE_CONTRACT_VERSION","COMPUTE_VERSION_HEADER","buildSolveInputTree","inputs","values","TreeBuilder","runSolvePipeline","args","client","signal","treeBuildStart","inputTree","treeBuildMs","settleSeqBefore","rhinoSeqBefore","result","solveStart","err","durationMs","RhinoComputeError","ErrorCodes","solveMs","serializeStart","serialized","serializeMs","worthSendingGzip","compressed","gzipMs","gzipStart","serverTotalMs","settle","rhino","headers","buildServerTiming","metrics","envelope","adaptEnvelopeToEncoding","acceptEncoding","waiterWantsGzip","json","gunzipSync","parts","header"],"mappings":"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","file":"/home/runner/work/selva/selva/packages/solve/dist/server.cjs","sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * framework-free half of a solve and returns a discriminated {@link SolveOutcome}\n * instead of throwing for expected failures. Nothing here touches auth, the\n * database, share tokens, rate limits, or metric sinks — those stay app policy\n * in the route that calls this.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n/** Async gzip — off the event loop, unlike `gzipSync`, which blocks it for multi-MB bodies. */\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from\n\t * the definition-byte cache) whose bytes the scheduler materializes ONLY when\n\t * an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves\n\t * zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/**\n\t * When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome\n\t * so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for\n\t * raw-bytes solves.\n\t */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}. When present\n\t * the pipeline skips its own build and solves this exact object.\n\t *\n\t * Needed whenever the caller coalesces concurrent solves: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */\n\tmaxSolveDurationMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Request abort signal, forwarded to the scheduler so a client disconnect\n\t * cancels the upstream compute call. Its `aborted` flag also disambiguates a\n\t * client disconnect from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/**\n\t * Wall-clock origin (`performance.now()` captured at the top of the request)\n\t * so the pipeline's `load`/`total` phase timings line up with the caller's\n\t * pre-solve prep.\n\t */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/**\n\t * Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`\n\t * Server-Timing entries.\n\t */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * Discriminated result. `ok` carries the envelope; every other variant names an\n * expected failure the transport maps to a status code (the app route maps\n * timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;\n * `compute_error` re-surfaces the original error for the generic 500/503 path).\n * `durationMs` on the error variants is the solve wall time up to the failure,\n * for the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/**\n\t * Scheduler backpressure shed the solve before it executed — the per-server\n\t * queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline\n\t * (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.\n\t * `retryAfterSeconds` is a suggested backoff hint for the client.\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/**\n * Build the transformed input tree — the exact object handed to the scheduler.\n * `runSolvePipeline` calls this itself; a caller only needs it directly to see\n * the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).\n */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\t// --- input tree build ---------------------------------------------------\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// --- solve --------------------------------------------------------------\n\t// Snapshot the shared telemetry sequence counters. The holders live on the\n\t// warm client and are written by every request on this server; with\n\t// maxConcurrentSolves > 1 another request can write them mid-flight. After\n\t// the solve, a slot is attributed to THIS request only when exactly one\n\t// write happened since this snapshot — for solveMeta that single settle is\n\t// necessarily ours (onSettle fires before scheduler.solve() resolves).\n\t// Anything ambiguous is dropped from Server-Timing, never misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// Distinguish timeout (scheduler deadline timer) from client disconnect\n\t\t// (request signal). AbortError with the request signal NOT aborted means\n\t\t// the scheduler's own timeout fired — the solve genuinely timed out.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.maxSolveDurationMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// A full queue or an over-deadline queue wait sheds the solve BEFORE compute\n\t\t// runs. Classify these as `shed` rather than the generic `compute_error` —\n\t\t// they're load signals, not failures, and the client should back off and retry.\n\t\tif (err instanceof RhinoComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\t// --- serialize (size-guarded) ------------------------------------------\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a single string at ~512 MB; an oversized `file` output trips a\n\t\t// RangeError here. Surface it as the same too-large outcome as the byte cap.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// --- gzip ---------------------------------------------------------------\n\t// Compress BEFORE the timing snapshot so its cost is a measured phase (`gzip`\n\t// in Server-Timing, included in `total`). Buffered (not streamed) so\n\t// Content-Length is known and a connection cut mid-transfer fails hard\n\t// instead of truncating the JSON. Skip gzip for bodies under the 1 KB\n\t// break-even — not worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// --- per-request telemetry attribution (see snapshot above) -------------\n\t// settle: ours iff exactly one settle happened since the snapshot. rhino:\n\t// only meaningful when the settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\t// --- Server-Timing envelope ----------------------------------------------\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// A downstream proxy skips re-encoding an already-`Content-Encoding`d body,\n\t\t// so this never double-compresses.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope}. That\n * envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a\n * later waiter with a different `Accept-Encoding` would otherwise get a body\n * labelled with the wrong encoding (`Vary` can't help — this is one object\n * shared across waiters, not a cache lookup). Encoding is deliberately not part\n * of the coalesce key, so mixed clients still coalesce; this adapts the shared\n * result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// The only mismatch that corrupts the response: a gzip body handed to a waiter\n\t// that did not advertise gzip. Gunzip it back to the JSON string and drop the\n\t// encoding headers. Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * Assemble the `Server-Timing` header. The browser reads these to attribute its\n * round-trip: `total` here == the server's headers-to-out wall time, so browser\n * `ttfb − total` ≈ network+send latency. When the compute server reported its\n * own decode/solve/encode (VektorNode fork), `rhino_*` is time ON the compute\n * server and `compute_link` is everything between (network + queue wait). Prep\n * sub-phases ride as `p_*`; cache verdicts as 0/1 `selva_cache`/`def_reupload`.\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// Byte-cache verdict for `DefinitionRef` solves: skipped = the scheduler never\n\t// materialized bytes (pointer-known solve — the whole point of this path), hit\n\t// = warm byte cache, miss = fell through to storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`\n * and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`\n * are absent on `SchemaInput` (they live on the layout item's config), so\n * valueList/file inputs fall back to carrying the selected value with no option\n * list — the same value that reaches Grasshopper.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// Dynamic value lists ride the ValueList contract on the wire — the plugin's\n\t\t// GetDynamicValueListParameter deliberately reuses TypeName \"ValueList\" so the\n\t\t// compute fork's existing ValueList input case assigns it. processInput has no\n\t\t// dynamicValueList handler; without this mapping it falls back to Geometry with\n\t\t// a null default and TreeBuilder DROPS the input, so the user's selection never\n\t\t// reaches the definition (the param then solves on its own stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// Two cases where processInput drops a default we must carry anyway — a\n\t// dropped default means the input is omitted from the solve and the user's\n\t// selection never reaches Grasshopper:\n\t// - a plain array default (multi-select widgets, e.g. a dynamic value list\n\t// checklist) is dropped as \"malformed\".\n\t// - the ValueList fallback drops a scalar default it can't validate against\n\t// a `values` map — and `SchemaInput` never carries the option map (it\n\t// lives on the layout item's config), so every app value-list selection\n\t// lands here.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Scoped to ValueList: for other types a dropped scalar default means\n\t\t\t// the package rejected it as unparseable — restoring it would ship\n\t\t\t// garbage to Grasshopper.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n"]}
1
+ {"version":3,"sources":["../src/server/solve-pipeline.ts","../src/server/transform-input.ts"],"names":["transformInputParameter","input","value","raw","processed","processInput","effective","gzipAsync","promisify","COMPUTE_CONTRACT_VERSION","COMPUTE_VERSION_HEADER","buildSolveInputTree","inputs","values","TreeBuilder","runSolvePipeline","args","client","signal","treeBuildStart","inputTree","treeBuildMs","settleSeqBefore","rhinoSeqBefore","result","solveStart","err","durationMs","RhinoComputeError","ErrorCodes","solveMs","serializeStart","serialized","serializeMs","worthSendingGzip","compressed","gzipMs","gzipStart","serverTotalMs","settle","rhino","headers","buildServerTiming","metrics","envelope","adaptEnvelopeToEncoding","acceptEncoding","waiterWantsGzip","json","gunzipSync","parts","header"],"mappings":"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","file":"/home/runner/work/selva/selva/packages/solve/dist/server.cjs","sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * framework-free half of a solve and returns a discriminated {@link SolveOutcome}\n * instead of throwing for expected failures. Nothing here touches auth, the\n * database, share tokens, rate limits, or metric sinks — those stay app policy\n * in the route that calls this.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n/** Async gzip — off the event loop, unlike `gzipSync`, which blocks it for multi-MB bodies. */\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from\n\t * the definition-byte cache) whose bytes the scheduler materializes ONLY when\n\t * an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves\n\t * zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/**\n\t * When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome\n\t * so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for\n\t * raw-bytes solves.\n\t */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}. When present\n\t * the pipeline skips its own build and solves this exact object.\n\t *\n\t * Needed whenever the caller coalesces concurrent solves: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */\n\tmaxSolveDurationMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Request abort signal, forwarded to the scheduler so a client disconnect\n\t * cancels the upstream compute call. Its `aborted` flag also disambiguates a\n\t * client disconnect from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/**\n\t * Wall-clock origin (`performance.now()` captured at the top of the request)\n\t * so the pipeline's `load`/`total` phase timings line up with the caller's\n\t * pre-solve prep.\n\t */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/**\n\t * Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`\n\t * Server-Timing entries.\n\t */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * Discriminated result. `ok` carries the envelope; every other variant names an\n * expected failure the transport maps to a status code (the app route maps\n * timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;\n * `compute_error` re-surfaces the original error for the generic 500/503 path).\n * `durationMs` on the error variants is the solve wall time up to the failure,\n * for the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/**\n\t * Scheduler backpressure shed the solve before it executed — the per-server\n\t * queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline\n\t * (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.\n\t * `retryAfterSeconds` is a suggested backoff hint for the client.\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/**\n * Build the transformed input tree — the exact object handed to the scheduler.\n * `runSolvePipeline` calls this itself; a caller only needs it directly to see\n * the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).\n */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\t// --- input tree build ---------------------------------------------------\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// --- solve --------------------------------------------------------------\n\t// Snapshot the shared telemetry sequence counters. The holders live on the\n\t// warm client and are written by every request on this server; with the\n\t// scheduler's maxConcurrent > 1 another request can write them mid-flight. After\n\t// the solve, a slot is attributed to THIS request only when exactly one\n\t// write happened since this snapshot — for solveMeta that single settle is\n\t// necessarily ours (onSettle fires before scheduler.solve() resolves).\n\t// Anything ambiguous is dropped from Server-Timing, never misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// Distinguish timeout (scheduler deadline timer) from client disconnect\n\t\t// (request signal). AbortError with the request signal NOT aborted means\n\t\t// the scheduler's own timeout fired — the solve genuinely timed out.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.maxSolveDurationMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// A full queue or an over-deadline queue wait sheds the solve BEFORE compute\n\t\t// runs. Classify these as `shed` rather than the generic `compute_error` —\n\t\t// they're load signals, not failures, and the client should back off and retry.\n\t\tif (err instanceof RhinoComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\t// --- serialize (size-guarded) ------------------------------------------\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a single string at ~512 MB; an oversized `file` output trips a\n\t\t// RangeError here. Surface it as the same too-large outcome as the byte cap.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// --- gzip ---------------------------------------------------------------\n\t// Compress BEFORE the timing snapshot so its cost is a measured phase (`gzip`\n\t// in Server-Timing, included in `total`). Buffered (not streamed) so\n\t// Content-Length is known and a connection cut mid-transfer fails hard\n\t// instead of truncating the JSON. Skip gzip for bodies under the 1 KB\n\t// break-even — not worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// --- per-request telemetry attribution (see snapshot above) -------------\n\t// settle: ours iff exactly one settle happened since the snapshot. rhino:\n\t// only meaningful when the settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\t// --- Server-Timing envelope ----------------------------------------------\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// A downstream proxy skips re-encoding an already-`Content-Encoding`d body,\n\t\t// so this never double-compresses.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope}. That\n * envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a\n * later waiter with a different `Accept-Encoding` would otherwise get a body\n * labelled with the wrong encoding (`Vary` can't help — this is one object\n * shared across waiters, not a cache lookup). Encoding is deliberately not part\n * of the coalesce key, so mixed clients still coalesce; this adapts the shared\n * result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// The only mismatch that corrupts the response: a gzip body handed to a waiter\n\t// that did not advertise gzip. Gunzip it back to the JSON string and drop the\n\t// encoding headers. Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * Assemble the `Server-Timing` header. The browser reads these to attribute its\n * round-trip: `total` here == the server's headers-to-out wall time, so browser\n * `ttfb − total` ≈ network+send latency. When the compute server reported its\n * own decode/solve/encode (VektorNode fork), `rhino_*` is time ON the compute\n * server and `compute_link` is everything between (network + queue wait). Prep\n * sub-phases ride as `p_*`; cache verdicts as 0/1 `selva_cache`/`def_reupload`.\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// Byte-cache verdict for `DefinitionRef` solves: skipped = the scheduler never\n\t// materialized bytes (pointer-known solve — the whole point of this path), hit\n\t// = warm byte cache, miss = fell through to storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`\n * and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`\n * are absent on `SchemaInput` (they live on the layout item's config), so\n * valueList/file inputs fall back to carrying the selected value with no option\n * list — the same value that reaches Grasshopper.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// Dynamic value lists ride the ValueList contract on the wire — the plugin's\n\t\t// GetDynamicValueListParameter deliberately reuses TypeName \"ValueList\" so the\n\t\t// compute fork's existing ValueList input case assigns it. processInput has no\n\t\t// dynamicValueList handler; without this mapping it falls back to Geometry with\n\t\t// a null default and TreeBuilder DROPS the input, so the user's selection never\n\t\t// reaches the definition (the param then solves on its own stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// Two cases where processInput drops a default we must carry anyway — a\n\t// dropped default means the input is omitted from the solve and the user's\n\t// selection never reaches Grasshopper:\n\t// - a plain array default (multi-select widgets, e.g. a dynamic value list\n\t// checklist) is dropped as \"malformed\".\n\t// - the ValueList fallback drops a scalar default it can't validate against\n\t// a `values` map — and `SchemaInput` never carries the option map (it\n\t// lives on the layout item's config), so every app value-list selection\n\t// lands here.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Scoped to ValueList: for other types a dropped scalar default means\n\t\t\t// the package rejected it as unparseable — restoring it would ship\n\t\t\t// garbage to Grasshopper.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n"]}
package/dist/server.d.cts CHANGED
@@ -1,5 +1,7 @@
1
- import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, SolveDefinition, DataTree, InputParam } from '@selvajs/compute';
1
+ import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, SolveDefinition, DataTree, InputParam, DefinitionRef } from '@selvajs/compute';
2
+ export { DefinitionRef, SolveDefinition, isDefinitionRef } from '@selvajs/compute';
2
3
  import { SchemaInput } from '@selvajs/schemas';
4
+ import { ILogger } from '@selvajs/platform';
3
5
  export { S as SolveInput } from './solve-input-CqYtetTA.cjs';
4
6
 
5
7
  /**
@@ -23,7 +25,7 @@ interface CachedClient {
23
25
  scheduler: SolveScheduler;
24
26
  /**
25
27
  * Last Server-Timing decode/solve/encode, written by onServerTiming.
26
- * The scheduler runs up to `maxConcurrentSolves` at once, so `last` alone
28
+ * The scheduler runs up to `maxConcurrent` solves at once, so `last` alone
27
29
  * can't be trusted per-request: callers snapshot `seq` before their solve
28
30
  * and only attribute `last` to themselves if exactly one write happened
29
31
  * since (see the guard in `runSolvePipeline`), dropping it otherwise
@@ -50,6 +52,12 @@ interface CachedClient {
50
52
  seq: number;
51
53
  };
52
54
  }
55
+ /**
56
+ * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs
57
+ * (`onDebugLog`), `'verbose'` also enables full lib-level request/response
58
+ * dumps (incl. geometry) — a superset of `true`, not an independent setting.
59
+ */
60
+ type ClientCacheDebug = boolean | 'verbose';
53
61
  /** Config injected by the consuming app, so this module stays env-agnostic and testable. */
54
62
  interface ClientCacheConfig {
55
63
  /** Per-solve timeout forwarded to the scheduler (`ComputeLimits.maxSolveDurationMs`). */
@@ -60,15 +68,10 @@ interface ClientCacheConfig {
60
68
  cacheerroredsolves: boolean;
61
69
  /** Reference large definitions by server cache key (pointer) instead of re-uploading. */
62
70
  reuseServerDefinitionCache: boolean;
63
- /**
64
- * Max in-flight solves per compute server (scheduler `maxConcurrent`; excess
65
- * FIFO-queues). Size to the server's `compute.geometry` child count
66
- * (`ComputeLimits.computeMaxConcurrentSolves`).
67
- */
68
- maxConcurrentSolves: number;
69
71
  /**
70
72
  * Backpressure — max solves that may WAIT in the FIFO queue (excludes the
71
- * in-flight `maxConcurrentSolves`). `0` = unbounded (`ComputeLimits.
73
+ * in-flight solves, capped at the scheduler's `maxConcurrent`, itself driven
74
+ * by the compute server's probed child count). `0` = unbounded (`ComputeLimits.
72
75
  * computeMaxQueueDepth`); a full queue sheds new solves with `QUEUE_FULL`.
73
76
  */
74
77
  maxQueueDepth: number;
@@ -81,16 +84,14 @@ interface ClientCacheConfig {
81
84
  /**
82
85
  * Byte budget for this client's in-process solve cache, evicted LRU alongside
83
86
  * its entry-count cap. Applies per warm client — total worst-case heap is this
84
- * × `maxCachedClients`. `0` disables the cache entirely
87
+ * × `maxWarmComputeServers`. `0` disables the cache entirely
85
88
  * (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).
86
89
  */
87
90
  responseCacheMaxBytes: number;
88
- /** Concise cache/timing logs. When false, `onDebugLog` is never invoked. */
89
- debug: boolean;
90
- /** VERBOSE lib-level logging (full solve request/response incl. geometry). */
91
- debugVerbose: boolean;
92
- /** Max distinct warm clients before the LRU evicts the oldest. Default 16. */
93
- maxCachedClients?: number;
91
+ /** Debug verbosity. `onDebugLog` is only ever invoked when this is not `false`. */
92
+ debug: ClientCacheDebug;
93
+ /** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */
94
+ maxWarmComputeServers?: number;
94
95
  /** Sink for the concise debug lines (the app wires `console.log`). */
95
96
  onDebugLog?: (message: string) => void;
96
97
  }
@@ -412,4 +413,121 @@ interface SolveCacheSingleFlightOptions {
412
413
  }
413
414
  declare function createSolveCacheSingleFlight(options?: SolveCacheSingleFlightOptions): SolveCacheSingleFlight;
414
415
 
415
- export { type ByteCacheRef, type ByteCacheStats, type ByteRefOutcome, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type CachedClient, type ClientCache, type ClientCacheConfig, type DefinitionByteCache, type PipelineInput, type ResolvedServer, type ServerIdentity, type SolveCacheSingleFlight, type SolveCacheSingleFlightOptions, type SolveCacheStats, type SolveEnvelope, type SolveOutcome, type SolvePhaseMetrics, type SolvePipelineArgs, adaptEnvelopeToEncoding, buildSolveInputTree, createClientCache, createDefinitionByteCache, createSolveCacheSingleFlight, runSolvePipeline, serverIdentity, transformInputParameter };
416
+ /**
417
+ * `SolveEngine` — the facade that owns everything a consumer needs to run
418
+ * interactive Grasshopper solves: the per-server warm-client cache, the
419
+ * definition-byte cache, single-flight coalescing, the pipeline call, and the
420
+ * outcome→HTTP mapping. Composes `createClientCache` + `createDefinitionByteCache`
421
+ * + `createSolveCacheSingleFlight` + `runSolvePipeline` — the same primitives a
422
+ * hand-assembled app route already wires, minus the wiring.
423
+ *
424
+ * Deliberately does NOT read env itself (no `resolveComputeLimits` call here):
425
+ * `@selvajs/server`, which owns that function, depends on `@selvajs/solve`, so
426
+ * the reverse import would be circular. A consumer resolves its own limits
427
+ * (`resolveComputeLimits` from `@selvajs/server/compute`, or any equivalent) and
428
+ * passes the handful of fields this engine actually needs.
429
+ *
430
+ * What stays OUT, on purpose (matches `server/index.ts`'s existing boundary):
431
+ * auth, DB reads, share tokens, rate limiting, metric sinks, and which compute
432
+ * server to use (`resolveServerForOrg`-equivalent) — all app policy, supplied
433
+ * per-call via `SolveEngineSolveArgs.server`.
434
+ */
435
+
436
+ /**
437
+ * The subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs,
438
+ * passed straight through to `createClientCache` — pass the resolved object
439
+ * through, its extra fields are ignored.
440
+ */
441
+ interface SolveEngineLimits {
442
+ maxSolveDurationMs: number;
443
+ computeResponseMaxBytes: number;
444
+ computeReuseDefinitionCache: boolean;
445
+ computeServerCachesolve: boolean;
446
+ computeCacheErroredSolves: boolean;
447
+ computeMaxQueueDepth: number;
448
+ computeQueueWaitMs: number;
449
+ computeDefinitionCacheBytes: number;
450
+ computeSolveCacheBytes: number;
451
+ }
452
+ interface SolveEngineOptions {
453
+ /** Required — the engine does not read env itself (see module doc). */
454
+ limits: SolveEngineLimits;
455
+ logger?: ILogger;
456
+ /** Max distinct warm compute servers before the LRU evicts the oldest. Default 16 (see `createClientCache`). */
457
+ maxWarmComputeServers?: number;
458
+ /** Concise cache/timing logs when truthy; `'verbose'` also enables full lib-level request/response dumps. Default off. */
459
+ debug?: ClientCacheDebug;
460
+ onDebugLog?: (message: string) => void;
461
+ /** Fired when a caller joins an already-in-flight solve instead of running its own (`createSolveCacheSingleFlight`'s `onJoin`). */
462
+ onSolveCoalesced?: (key: string) => void;
463
+ }
464
+ /**
465
+ * `definitionSource` accepts every form `runSolvePipeline` does, plus the
466
+ * `{versionId, load}` sugar that builds (and caches) a `ByteCacheRef` internally.
467
+ * A `ByteCacheRef` obtained from `engine.definitionRef()` ahead of time (e.g. to
468
+ * read its bytes for schema extraction before solving) is accepted directly and
469
+ * NOT re-wrapped — recognized by its `outcome` field, which a plain external
470
+ * `DefinitionRef` never has.
471
+ */
472
+ type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef | {
473
+ versionId: string;
474
+ load: () => Promise<Uint8Array>;
475
+ };
476
+ interface SolveEngineSolveArgs {
477
+ server: ResolvedServer;
478
+ definitionSource: SolveEngineDefinitionSource;
479
+ /**
480
+ * Coalesce-key identity for a raw `Uint8Array`/`string` `definitionSource`,
481
+ * which has no natural identity the way a `DefinitionRef`'s `.key` does.
482
+ * Required in that case — `solve()` throws without it, rather than silently
483
+ * hashing bytes (a different identity convention than the rest of the
484
+ * package, which keys on immutable ids). Ignored for the other source forms.
485
+ */
486
+ definitionKey?: string;
487
+ inputs: PipelineInput[];
488
+ values: Record<string, unknown>;
489
+ signal: AbortSignal;
490
+ acceptEncoding?: string;
491
+ /** Stamped as `X-Selva-Definition` on this solve's client — see `ClientCache.getClient`. */
492
+ definitionGuid?: string;
493
+ loadStartMs?: number;
494
+ defLoadMs?: number;
495
+ prepMarks?: [string, number][];
496
+ }
497
+ interface FrameworkAgnosticResponse {
498
+ status: number;
499
+ headers: Record<string, string>;
500
+ body: string | Uint8Array;
501
+ }
502
+ interface SolveEngineStats {
503
+ client: SolveCacheStats;
504
+ definitionBytes: ByteCacheStats;
505
+ coalescing: {
506
+ inFlight: number;
507
+ };
508
+ }
509
+ declare class SolveEngine {
510
+ private readonly limits;
511
+ private readonly clientCache;
512
+ private readonly byteCache;
513
+ private readonly singleFlight;
514
+ constructor(options: SolveEngineOptions);
515
+ getClient(server: ResolvedServer, opts?: {
516
+ definitionGuid?: string;
517
+ }): Promise<CachedClient>;
518
+ definitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;
519
+ evictServer(id: string): void;
520
+ clearSolveCaches(): void;
521
+ stats(): SolveEngineStats;
522
+ solve(args: SolveEngineSolveArgs): Promise<SolveOutcome>;
523
+ toResponse(outcome: SolveOutcome, opts?: {
524
+ onError?: (status: number, body: {
525
+ message: string;
526
+ retryAfter?: number;
527
+ }) => never;
528
+ }): FrameworkAgnosticResponse;
529
+ toWebResponse(outcome: SolveOutcome): Response;
530
+ private resolveDefinitionSource;
531
+ }
532
+
533
+ export { type ByteCacheRef, type ByteCacheStats, type ByteRefOutcome, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type CachedClient, type ClientCache, type ClientCacheConfig, type DefinitionByteCache, type FrameworkAgnosticResponse, type PipelineInput, type ResolvedServer, type ServerIdentity, type SolveCacheSingleFlight, type SolveCacheSingleFlightOptions, type SolveCacheStats, SolveEngine, type SolveEngineDefinitionSource, type SolveEngineLimits, type SolveEngineOptions, type SolveEngineSolveArgs, type SolveEngineStats, type SolveEnvelope, type SolveOutcome, type SolvePhaseMetrics, type SolvePipelineArgs, adaptEnvelopeToEncoding, buildSolveInputTree, createClientCache, createDefinitionByteCache, createSolveCacheSingleFlight, runSolvePipeline, serverIdentity, transformInputParameter };
package/dist/server.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, SolveDefinition, DataTree, InputParam } from '@selvajs/compute';
1
+ import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, SolveDefinition, DataTree, InputParam, DefinitionRef } from '@selvajs/compute';
2
+ export { DefinitionRef, SolveDefinition, isDefinitionRef } from '@selvajs/compute';
2
3
  import { SchemaInput } from '@selvajs/schemas';
4
+ import { ILogger } from '@selvajs/platform';
3
5
  export { S as SolveInput } from './solve-input-CqYtetTA.js';
4
6
 
5
7
  /**
@@ -23,7 +25,7 @@ interface CachedClient {
23
25
  scheduler: SolveScheduler;
24
26
  /**
25
27
  * Last Server-Timing decode/solve/encode, written by onServerTiming.
26
- * The scheduler runs up to `maxConcurrentSolves` at once, so `last` alone
28
+ * The scheduler runs up to `maxConcurrent` solves at once, so `last` alone
27
29
  * can't be trusted per-request: callers snapshot `seq` before their solve
28
30
  * and only attribute `last` to themselves if exactly one write happened
29
31
  * since (see the guard in `runSolvePipeline`), dropping it otherwise
@@ -50,6 +52,12 @@ interface CachedClient {
50
52
  seq: number;
51
53
  };
52
54
  }
55
+ /**
56
+ * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs
57
+ * (`onDebugLog`), `'verbose'` also enables full lib-level request/response
58
+ * dumps (incl. geometry) — a superset of `true`, not an independent setting.
59
+ */
60
+ type ClientCacheDebug = boolean | 'verbose';
53
61
  /** Config injected by the consuming app, so this module stays env-agnostic and testable. */
54
62
  interface ClientCacheConfig {
55
63
  /** Per-solve timeout forwarded to the scheduler (`ComputeLimits.maxSolveDurationMs`). */
@@ -60,15 +68,10 @@ interface ClientCacheConfig {
60
68
  cacheerroredsolves: boolean;
61
69
  /** Reference large definitions by server cache key (pointer) instead of re-uploading. */
62
70
  reuseServerDefinitionCache: boolean;
63
- /**
64
- * Max in-flight solves per compute server (scheduler `maxConcurrent`; excess
65
- * FIFO-queues). Size to the server's `compute.geometry` child count
66
- * (`ComputeLimits.computeMaxConcurrentSolves`).
67
- */
68
- maxConcurrentSolves: number;
69
71
  /**
70
72
  * Backpressure — max solves that may WAIT in the FIFO queue (excludes the
71
- * in-flight `maxConcurrentSolves`). `0` = unbounded (`ComputeLimits.
73
+ * in-flight solves, capped at the scheduler's `maxConcurrent`, itself driven
74
+ * by the compute server's probed child count). `0` = unbounded (`ComputeLimits.
72
75
  * computeMaxQueueDepth`); a full queue sheds new solves with `QUEUE_FULL`.
73
76
  */
74
77
  maxQueueDepth: number;
@@ -81,16 +84,14 @@ interface ClientCacheConfig {
81
84
  /**
82
85
  * Byte budget for this client's in-process solve cache, evicted LRU alongside
83
86
  * its entry-count cap. Applies per warm client — total worst-case heap is this
84
- * × `maxCachedClients`. `0` disables the cache entirely
87
+ * × `maxWarmComputeServers`. `0` disables the cache entirely
85
88
  * (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).
86
89
  */
87
90
  responseCacheMaxBytes: number;
88
- /** Concise cache/timing logs. When false, `onDebugLog` is never invoked. */
89
- debug: boolean;
90
- /** VERBOSE lib-level logging (full solve request/response incl. geometry). */
91
- debugVerbose: boolean;
92
- /** Max distinct warm clients before the LRU evicts the oldest. Default 16. */
93
- maxCachedClients?: number;
91
+ /** Debug verbosity. `onDebugLog` is only ever invoked when this is not `false`. */
92
+ debug: ClientCacheDebug;
93
+ /** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */
94
+ maxWarmComputeServers?: number;
94
95
  /** Sink for the concise debug lines (the app wires `console.log`). */
95
96
  onDebugLog?: (message: string) => void;
96
97
  }
@@ -412,4 +413,121 @@ interface SolveCacheSingleFlightOptions {
412
413
  }
413
414
  declare function createSolveCacheSingleFlight(options?: SolveCacheSingleFlightOptions): SolveCacheSingleFlight;
414
415
 
415
- export { type ByteCacheRef, type ByteCacheStats, type ByteRefOutcome, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type CachedClient, type ClientCache, type ClientCacheConfig, type DefinitionByteCache, type PipelineInput, type ResolvedServer, type ServerIdentity, type SolveCacheSingleFlight, type SolveCacheSingleFlightOptions, type SolveCacheStats, type SolveEnvelope, type SolveOutcome, type SolvePhaseMetrics, type SolvePipelineArgs, adaptEnvelopeToEncoding, buildSolveInputTree, createClientCache, createDefinitionByteCache, createSolveCacheSingleFlight, runSolvePipeline, serverIdentity, transformInputParameter };
416
+ /**
417
+ * `SolveEngine` — the facade that owns everything a consumer needs to run
418
+ * interactive Grasshopper solves: the per-server warm-client cache, the
419
+ * definition-byte cache, single-flight coalescing, the pipeline call, and the
420
+ * outcome→HTTP mapping. Composes `createClientCache` + `createDefinitionByteCache`
421
+ * + `createSolveCacheSingleFlight` + `runSolvePipeline` — the same primitives a
422
+ * hand-assembled app route already wires, minus the wiring.
423
+ *
424
+ * Deliberately does NOT read env itself (no `resolveComputeLimits` call here):
425
+ * `@selvajs/server`, which owns that function, depends on `@selvajs/solve`, so
426
+ * the reverse import would be circular. A consumer resolves its own limits
427
+ * (`resolveComputeLimits` from `@selvajs/server/compute`, or any equivalent) and
428
+ * passes the handful of fields this engine actually needs.
429
+ *
430
+ * What stays OUT, on purpose (matches `server/index.ts`'s existing boundary):
431
+ * auth, DB reads, share tokens, rate limiting, metric sinks, and which compute
432
+ * server to use (`resolveServerForOrg`-equivalent) — all app policy, supplied
433
+ * per-call via `SolveEngineSolveArgs.server`.
434
+ */
435
+
436
+ /**
437
+ * The subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs,
438
+ * passed straight through to `createClientCache` — pass the resolved object
439
+ * through, its extra fields are ignored.
440
+ */
441
+ interface SolveEngineLimits {
442
+ maxSolveDurationMs: number;
443
+ computeResponseMaxBytes: number;
444
+ computeReuseDefinitionCache: boolean;
445
+ computeServerCachesolve: boolean;
446
+ computeCacheErroredSolves: boolean;
447
+ computeMaxQueueDepth: number;
448
+ computeQueueWaitMs: number;
449
+ computeDefinitionCacheBytes: number;
450
+ computeSolveCacheBytes: number;
451
+ }
452
+ interface SolveEngineOptions {
453
+ /** Required — the engine does not read env itself (see module doc). */
454
+ limits: SolveEngineLimits;
455
+ logger?: ILogger;
456
+ /** Max distinct warm compute servers before the LRU evicts the oldest. Default 16 (see `createClientCache`). */
457
+ maxWarmComputeServers?: number;
458
+ /** Concise cache/timing logs when truthy; `'verbose'` also enables full lib-level request/response dumps. Default off. */
459
+ debug?: ClientCacheDebug;
460
+ onDebugLog?: (message: string) => void;
461
+ /** Fired when a caller joins an already-in-flight solve instead of running its own (`createSolveCacheSingleFlight`'s `onJoin`). */
462
+ onSolveCoalesced?: (key: string) => void;
463
+ }
464
+ /**
465
+ * `definitionSource` accepts every form `runSolvePipeline` does, plus the
466
+ * `{versionId, load}` sugar that builds (and caches) a `ByteCacheRef` internally.
467
+ * A `ByteCacheRef` obtained from `engine.definitionRef()` ahead of time (e.g. to
468
+ * read its bytes for schema extraction before solving) is accepted directly and
469
+ * NOT re-wrapped — recognized by its `outcome` field, which a plain external
470
+ * `DefinitionRef` never has.
471
+ */
472
+ type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef | {
473
+ versionId: string;
474
+ load: () => Promise<Uint8Array>;
475
+ };
476
+ interface SolveEngineSolveArgs {
477
+ server: ResolvedServer;
478
+ definitionSource: SolveEngineDefinitionSource;
479
+ /**
480
+ * Coalesce-key identity for a raw `Uint8Array`/`string` `definitionSource`,
481
+ * which has no natural identity the way a `DefinitionRef`'s `.key` does.
482
+ * Required in that case — `solve()` throws without it, rather than silently
483
+ * hashing bytes (a different identity convention than the rest of the
484
+ * package, which keys on immutable ids). Ignored for the other source forms.
485
+ */
486
+ definitionKey?: string;
487
+ inputs: PipelineInput[];
488
+ values: Record<string, unknown>;
489
+ signal: AbortSignal;
490
+ acceptEncoding?: string;
491
+ /** Stamped as `X-Selva-Definition` on this solve's client — see `ClientCache.getClient`. */
492
+ definitionGuid?: string;
493
+ loadStartMs?: number;
494
+ defLoadMs?: number;
495
+ prepMarks?: [string, number][];
496
+ }
497
+ interface FrameworkAgnosticResponse {
498
+ status: number;
499
+ headers: Record<string, string>;
500
+ body: string | Uint8Array;
501
+ }
502
+ interface SolveEngineStats {
503
+ client: SolveCacheStats;
504
+ definitionBytes: ByteCacheStats;
505
+ coalescing: {
506
+ inFlight: number;
507
+ };
508
+ }
509
+ declare class SolveEngine {
510
+ private readonly limits;
511
+ private readonly clientCache;
512
+ private readonly byteCache;
513
+ private readonly singleFlight;
514
+ constructor(options: SolveEngineOptions);
515
+ getClient(server: ResolvedServer, opts?: {
516
+ definitionGuid?: string;
517
+ }): Promise<CachedClient>;
518
+ definitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;
519
+ evictServer(id: string): void;
520
+ clearSolveCaches(): void;
521
+ stats(): SolveEngineStats;
522
+ solve(args: SolveEngineSolveArgs): Promise<SolveOutcome>;
523
+ toResponse(outcome: SolveOutcome, opts?: {
524
+ onError?: (status: number, body: {
525
+ message: string;
526
+ retryAfter?: number;
527
+ }) => never;
528
+ }): FrameworkAgnosticResponse;
529
+ toWebResponse(outcome: SolveOutcome): Response;
530
+ private resolveDefinitionSource;
531
+ }
532
+
533
+ export { type ByteCacheRef, type ByteCacheStats, type ByteRefOutcome, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type CachedClient, type ClientCache, type ClientCacheConfig, type DefinitionByteCache, type FrameworkAgnosticResponse, type PipelineInput, type ResolvedServer, type ServerIdentity, type SolveCacheSingleFlight, type SolveCacheSingleFlightOptions, type SolveCacheStats, SolveEngine, type SolveEngineDefinitionSource, type SolveEngineLimits, type SolveEngineOptions, type SolveEngineSolveArgs, type SolveEngineStats, type SolveEnvelope, type SolveOutcome, type SolvePhaseMetrics, type SolvePipelineArgs, adaptEnvelopeToEncoding, buildSolveInputTree, createClientCache, createDefinitionByteCache, createSolveCacheSingleFlight, runSolvePipeline, serverIdentity, transformInputParameter };
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- import{ErrorCodes as R,RhinoComputeError as I,TreeBuilder as O}from"@selvajs/compute";import{gzip as A,gunzipSync as _}from"zlib";import{promisify as L}from"util";import{processInput as z}from"@selvajs/compute";function T(e,r){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:e.minimum??null,maximum:e.maximum??null,atLeast:1,atMost:1,stepSize:e.stepSize,default:r??e.default},s=z(t),o=r??e.default;return s.default==null&&o!=null&&(Array.isArray(o)||s.paramType==="ValueList"&&typeof o!="object")&&(s.default=o),s}var $=L(A),P=1,E="X-Selva-Compute-Version";function B(e,r){return O.fromInputParams(e.filter(t=>t.paramType).map(t=>T(t,r[t.id])))}async function D(e){let{client:r,signal:t}=e,s=performance.now(),o=e.inputTree??B(e.inputs,e.values),u=performance.now()-s,i=r.solveMeta.seq,l=r.rhinoTiming.seq,n,a=performance.now();try{n=await r.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let b=performance.now()-a;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:b}:{kind:"timeout",durationMs:b,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof I){if(v.code===R.QUEUE_FULL)return{kind:"shed",durationMs:b,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===R.QUEUE_TIMEOUT)return{kind:"shed",durationMs:b,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:b,error:v}}let c=performance.now()-a,h=performance.now(),d;try{d=JSON.stringify(n)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,y=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,f=null,g=0;if(y){let v=performance.now();f=await $(Buffer.from(d)),g=performance.now()-v}let S=performance.now()-e.loadStartMs,M=r.solveMeta.seq===i+1?r.solveMeta.last:null,k=M&&!M.fromCache&&r.rhinoTiming.seq===l+1?r.rhinoTiming.last:null,C={"Content-Type":"application/json","Server-Timing":U({defLoadMs:e.defLoadMs,treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,rhino:k,settle:M,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[E]:String(P),Vary:"Accept-Encoding"},w={treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,serializedBytes:d.length,compressedBytes:f?.byteLength??null},x;return f?(C["Content-Encoding"]="gzip",C["Content-Length"]=String(f.byteLength),x={body:new Uint8Array(f),encoding:"gzip",headers:C,result:n,metrics:w}):(C["Content-Length"]=String(Buffer.byteLength(d)),x={body:d,headers:C,result:n,metrics:w}),{kind:"ok",envelope:x,solveMs:c,errorCount:n.errors?.length??0,warningCount:n.warnings?.length??0}}function F(e,r){let t=/\bgzip\b/i.test(r);if(e.encoding==="gzip"&&!t){let s=_(e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(s)),{body:s,headers:o}}return{body:e.body,headers:e.headers}}function U(e){let r=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let u=t.decode+t.solve+t.encode,i=Math.max(0,e.solveMs-u);r+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[u,i]of e.prepMarks)r+=`, p_${u};dur=${i.toFixed(1)}`;let s=e.settle;s&&(r+=`, selva_cache;dur=${s.fromCache?1:0}`,s.definitionReuploaded!==void 0&&(r+=`, def_reupload;dur=${s.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let u=o.loaded?o.fromCache?"hit":"miss":"skipped";r+=`, def_bytes;desc=${u}`}return r}import{GrasshopperClient as q,enableDebugLogging as G}from"@selvajs/compute";function V(e){return e.id}var N=16;function j(e){let r=e.maxCachedClients??N,t=new Map,s=new Map;e.debugVerbose&&G();let o=i=>{e.debug&&e.onDebugLog?.(i)};async function u(i,l){let n={last:null,seq:0},a={last:null,seq:0},c={serverUrl:i.serverUrl,apiKey:i.apiKey,debug:e.debugVerbose,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:p=>{let m=p.decode??0,y=p.solve??0,f=p.encode??0;if(n.seq+=1,n.last={decode:m,solve:y,encode:f},!e.debug)return;let g=m<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",S=e.cachesolve&&y<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${m}ms solve=${y}ms encode=${f}ms | definition-cache: ${g} | solve-cache (cachesolve): ${S}`)}};l&&(c.headers={"X-Selva-Definition":l});let h=await q.create(c),d=h.createScheduler({mode:"queue",maxConcurrent:e.maxConcurrentSolves,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(p,m)=>{if(a.seq+=1,m.status==="success"&&(a.last={fromCache:m.fromCache===!0,definitionReuploaded:m.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${m.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(m.durationMs)}ms)`),m.fromCache||o(m.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":m.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!m.fromCache))){let y=m.response?.errors,f=Array.isArray(y)?y.length:0;if(f>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${f} GH error(s); ${g}. First: ${JSON.stringify(y[0])}`)}}}});return{client:h,scheduler:d,rhinoTiming:n,solveMeta:a}}return{async getClient(i,l){let n=i.id,a=t.get(n);if(a)return t.delete(n),t.set(n,a),a;let c=s.get(n);if(c)return c;o(`[Compute/client-cache] miss \u2014 building warm client for server ${n}`);let h=(async()=>{let d=await u(i,l?.definitionGuid);if(t.size>=r){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${r})`),t.get(p)?.scheduler.dispose(),t.delete(p))}return t.set(n,d),d})().finally(()=>s.delete(n));return s.set(n,h),h},evict(i){let l=i,n=t.get(l);n&&(o(`[Compute/client-cache] evicted warm client for server ${l} (config change)`),n.scheduler.dispose(),t.delete(l))},solveCacheStats(){let i={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let l of t.values()){let n=l.scheduler.cacheStats();i.entries+=n.entries,i.bytes+=n.bytes,i.hits+=n.hits,i.misses+=n.misses,i.evictions+=n.evictions}return i},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.dispose();t.clear()}}}function H(e){let r=Math.max(0,Math.floor(e)),t=new Map,s=0,o={hits:0,misses:0,evictions:0};function u(n,a){t.delete(n),t.set(n,a)}function i(){for(;s>r&&t.size>0;){let n=t.keys().next().value,a=t.get(n);t.delete(n),s-=a.bytes.byteLength,o.evictions+=1}}function l(n,a){if(r===0||a.byteLength>r)return;let c=t.get(n);c&&(s-=c.bytes.byteLength),t.set(n,{bytes:a}),s+=a.byteLength,i()}return{getOrLoad(n,a){let c={loaded:!1,fromCache:!1};return{key:n,outcome:c,load:async()=>{let h=t.get(n);if(h)return u(n,h),o.hits+=1,c.loaded=!0,c.fromCache=!0,h.bytes;o.misses+=1,c.loaded=!0,c.fromCache=!1;let d=await a();return l(n,d),d}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:s}},clear(){t.clear(),s=0}}}function W(e={}){let r=new Map;return{run(t,s,o){let u=r.get(t);if(u)return e.onJoin?.(t),u.notifyOwner?.(),u.promise;let i={notifyOwner:o,promise:void 0},l=(async()=>s())().finally(()=>{i.notifyOwner=void 0,r.get(t)===i&&r.delete(t)});return i.promise=l,r.set(t,i),l},inFlight(){return r.size}}}export{P as COMPUTE_CONTRACT_VERSION,E as COMPUTE_VERSION_HEADER,F as adaptEnvelopeToEncoding,B as buildSolveInputTree,j as createClientCache,H as createDefinitionByteCache,W as createSolveCacheSingleFlight,D as runSolvePipeline,V as serverIdentity,T as transformInputParameter};
1
+ import{ErrorCodes as I,RhinoComputeError as U,TreeBuilder as q}from"@selvajs/compute";import{gzip as W,gunzipSync as G}from"zlib";import{promisify as N}from"util";import{processInput as $}from"@selvajs/compute";function A(e,n){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:e.minimum??null,maximum:e.maximum??null,atLeast:1,atMost:1,stepSize:e.stepSize,default:n??e.default},r=$(t),o=n??e.default;return r.default==null&&o!=null&&(Array.isArray(o)||r.paramType==="ValueList"&&typeof o!="object")&&(r.default=o),r}var j=N(W),F=1,_="X-Selva-Compute-Version";function E(e,n){return q.fromInputParams(e.filter(t=>t.paramType).map(t=>A(t,n[t.id])))}async function B(e){let{client:n,signal:t}=e,r=performance.now(),o=e.inputTree??E(e.inputs,e.values),a=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,c=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let x=performance.now()-c;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:x}:{kind:"timeout",durationMs:x,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof U){if(v.code===I.QUEUE_FULL)return{kind:"shed",durationMs:x,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===I.QUEUE_TIMEOUT)return{kind:"shed",durationMs:x,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:x,error:v}}let u=performance.now()-c,p=performance.now(),d;try{d=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let f=performance.now()-p,M=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(d)),S=performance.now()-v}let g=performance.now()-e.loadStartMs,l=n.solveMeta.seq===s+1?n.solveMeta.last:null,h=l&&!l.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,y={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:a,solveMs:u,serializeMs:f,gzipMs:S,serverTotalMs:g,rhino:h,settle:l,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[_]:String(F),Vary:"Accept-Encoding"},R={treeBuildMs:a,solveMs:u,serializeMs:f,gzipMs:S,serverTotalMs:g,serializedBytes:d.length,compressedBytes:C?.byteLength??null},D;return C?(y["Content-Encoding"]="gzip",y["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:y,result:i,metrics:R}):(y["Content-Length"]=String(Buffer.byteLength(d)),D={body:d,headers:y,result:i,metrics:R}),{kind:"ok",envelope:D,solveMs:u,errorCount:i.errors?.length??0,warningCount:i.warnings?.length??0}}function T(e,n){let t=/\bgzip\b/i.test(n);if(e.encoding==="gzip"&&!t){let r=G(e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(r)),{body:r,headers:o}}return{body:e.body,headers:e.headers}}function V(e){let n=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let a=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-a);n+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${s.toFixed(1)}`}for(let[a,s]of e.prepMarks)n+=`, p_${a};dur=${s.toFixed(1)}`;let r=e.settle;r&&(n+=`, selva_cache;dur=${r.fromCache?1:0}`,r.definitionReuploaded!==void 0&&(n+=`, def_reupload;dur=${r.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let a=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${a}`}return n}import{GrasshopperClient as K,enableDebugLogging as Q}from"@selvajs/compute";function H(e){return e.id}var J=16,z=1,X=300*1e3;function k(e){let n=e.maxWarmComputeServers??J,t=new Map,r=new Map;e.debug==="verbose"&&Q();let o=s=>{e.debug&&e.onDebugLog?.(s)};async function a(s,m){let i={last:null,seq:0},c={last:null,seq:0},u=e.debug==="verbose",p={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:u,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:g=>{let l=g.decode??0,h=g.solve??0,b=g.encode??0;if(i.seq+=1,i.last={decode:l,solve:h,encode:b},!e.debug)return;let y=l<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",R=e.cachesolve&&h<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${l}ms solve=${h}ms encode=${b}ms | definition-cache: ${y} | solve-cache (cachesolve): ${R}`)}};m&&(p.headers={"X-Selva-Definition":m});let d=await K.create(p),f=z,w=0,M=!1;async function C(g){if(!M){M=!0;try{let l=await Promise.resolve(d.serverStats?.getActiveChildren({initialize:!1})).catch(()=>null);w=Date.now();let h=!l||l<=0?z:l;if(h===f)return;o(`[Compute/client-cache] concurrency ${f} \u2192 ${h} (server child count)`),f=h,g?.setMaxConcurrent(h)}finally{M=!1}}}await C();let S=d.createScheduler({mode:"queue",maxConcurrent:f,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(g,l)=>{if(c.seq+=1,l.status==="success"&&(l.fromCache!==!0&&Date.now()-w>=X&&C(S),c.last={fromCache:l.fromCache===!0,definitionReuploaded:l.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${l.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(l.durationMs)}ms)`),l.fromCache||o(l.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":l.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!l.fromCache))){let h=l.response?.errors,b=Array.isArray(h)?h.length:0;if(b>0){let y=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${y}. First: ${JSON.stringify(h[0])}`)}}}});return{client:d,scheduler:S,rhinoTiming:i,solveMeta:c}}return{async getClient(s,m){let i=s.id,c=t.get(i);if(c)return t.delete(i),t.set(i,c),c;let u=r.get(i);if(u)return u;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let p=(async()=>{let d=await a(s,m?.definitionGuid);if(t.size>=n){let f=t.keys().next().value;f!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${f} (cap ${n})`),t.get(f)?.scheduler.dispose(),t.delete(f))}return t.set(i,d),d})().finally(()=>r.delete(i));return r.set(i,p),p},evict(s){let m=s,i=t.get(m);i&&(o(`[Compute/client-cache] evicted warm client for server ${m} (config change)`),i.scheduler.dispose(),t.delete(m))},solveCacheStats(){let s={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let m of t.values()){let i=m.scheduler.cacheStats();s.entries+=i.entries,s.bytes+=i.bytes,s.hits+=i.hits,s.misses+=i.misses,s.evictions+=i.evictions}return s},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.dispose();t.clear()}}}function P(e){let n=Math.max(0,Math.floor(e)),t=new Map,r=0,o={hits:0,misses:0,evictions:0};function a(i,c){t.delete(i),t.set(i,c)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,c=t.get(i);t.delete(i),r-=c.bytes.byteLength,o.evictions+=1}}function m(i,c){if(n===0||c.byteLength>n)return;let u=t.get(i);u&&(r-=u.bytes.byteLength),t.set(i,{bytes:c}),r+=c.byteLength,s()}return{getOrLoad(i,c){let u={loaded:!1,fromCache:!1};return{key:i,outcome:u,load:async()=>{let p=t.get(i);if(p)return a(i,p),o.hits+=1,u.loaded=!0,u.fromCache=!0,p.bytes;o.misses+=1,u.loaded=!0,u.fromCache=!1;let d=await c();return m(i,d),d}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:r}},clear(){t.clear(),r=0}}}function O(e={}){let n=new Map;return{run(t,r,o){let a=n.get(t);if(a)return e.onJoin?.(t),a.notifyOwner?.(),a.promise;let s={notifyOwner:o,promise:void 0},m=(async()=>r())().finally(()=>{s.notifyOwner=void 0,n.get(t)===s&&n.delete(t)});return s.promise=m,n.set(t,s),m},inFlight(){return n.size}}}import{isDefinitionRef as Y,stableStringify as Z}from"@selvajs/compute";import{NoopLogger as ee}from"@selvajs/platform";function te(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&"outcome"in e}function ne(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&!("outcome"in e)&&!("key"in e)&&"versionId"in e}var L=class{limits;clientCache;byteCache;singleFlight;constructor(n){this.limits=n.limits;let t=n.logger??new ee;this.clientCache=k({maxSolveDurationMs:this.limits.maxSolveDurationMs,cachesolve:this.limits.computeServerCachesolve,cacheerroredsolves:this.limits.computeCacheErroredSolves,reuseServerDefinitionCache:this.limits.computeReuseDefinitionCache,maxQueueDepth:this.limits.computeMaxQueueDepth,queueWaitMs:this.limits.computeQueueWaitMs,responseCacheMaxBytes:this.limits.computeSolveCacheBytes,debug:n.debug??!1,maxWarmComputeServers:n.maxWarmComputeServers,onDebugLog:n.onDebugLog??(r=>t.debug(r,{component:"Compute/client-cache"}))}),this.byteCache=P(this.limits.computeDefinitionCacheBytes),this.singleFlight=O({onJoin:n.onSolveCoalesced})}getClient(n,t){return this.clientCache.getClient(n,t)}definitionRef(n,t){return this.byteCache.getOrLoad(n,t)}evictServer(n){this.clientCache.evict(n)}clearSolveCaches(){this.clientCache.clearSolveCaches()}stats(){return{client:this.clientCache.solveCacheStats(),definitionBytes:this.byteCache.stats(),coalescing:{inFlight:this.singleFlight.inFlight()}}}async solve(n){let{definitionSource:t,byteRefOutcome:r,identity:o}=this.resolveDefinitionSource(n),a=await this.clientCache.getClient(n.server,{definitionGuid:n.definitionGuid}),s=E(n.inputs,n.values),m=`${o}:${n.server.id}:${Z(s)}`,i=new AbortController,c=!1;n.signal.addEventListener("abort",()=>{c||i.abort()},{once:!0});let u=n.acceptEncoding??"",p=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:a,responseMaxBytes:this.limits.computeResponseMaxBytes,maxSolveDurationMs:this.limits.maxSolveDurationMs,acceptEncoding:u,signal:i.signal,loadStartMs:n.loadStartMs??performance.now(),defLoadMs:n.defLoadMs??0,prepMarks:n.prepMarks}),()=>{c=!0});if(p.kind!=="ok")return p;let d=T(p.envelope,u);return{...p,envelope:{...p.envelope,body:d.body,headers:d.headers}}}toResponse(n,t){let r=(o,a)=>{t?.onError&&t.onError(o,a);let s={"Content-Type":"application/json"};return a.retryAfter!==void 0&&(s["Retry-After"]=String(a.retryAfter)),{status:o,headers:s,body:JSON.stringify(a)}};switch(n.kind){case"ok":return{status:200,headers:n.envelope.headers,body:n.envelope.body};case"timeout":return r(504,{message:n.message});case"client_abort":return r(499,{message:"Client closed request"});case"too_large":return r(413,{message:"Solve result is too large to return. This usually means a file output exceeds the supported size."});case"shed":return r(503,{message:n.message,retryAfter:n.retryAfterSeconds});case"compute_error":throw n.error}}toWebResponse(n){let t=this.toResponse(n);return new Response(typeof t.body=="string"?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(n){let t=n.definitionSource;if(ne(t)){let r=this.byteCache.getOrLoad(t.versionId,t.load);return{definitionSource:r,byteRefOutcome:r.outcome,identity:r.key}}if(te(t))return{definitionSource:t,byteRefOutcome:t.outcome,identity:t.key};if(Y(t))return{definitionSource:t,identity:t.key};if(!n.definitionKey)throw new Error("SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.");return{definitionSource:t,identity:n.definitionKey}}};import{isDefinitionRef as Te}from"@selvajs/compute";export{F as COMPUTE_CONTRACT_VERSION,_ as COMPUTE_VERSION_HEADER,L as SolveEngine,T as adaptEnvelopeToEncoding,E as buildSolveInputTree,k as createClientCache,P as createDefinitionByteCache,O as createSolveCacheSingleFlight,Te as isDefinitionRef,B as runSolvePipeline,H as serverIdentity,A as transformInputParameter};
2
2
  //# sourceMappingURL=server.js.map