@selvajs/solve 1.0.0-beta.5 → 1.0.0-beta.7
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.cjs +1 -1
- package/dist/client.d.cts +4 -10
- package/dist/client.d.ts +4 -10
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
package/dist/client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _schemas = require('@selvajs/schemas');var _="external";function E(e,r){return`${_}:${e}:${r}`}function X(e){let{scopeKey:r,inputId:t,value:n}=e;!r||!t||typeof sessionStorage>"u"||sessionStorage.setItem(E(r,t),JSON.stringify(n))}function x(e){let{scopeKey:r,inputId:t}=e;if(!r||!t||typeof sessionStorage>"u")return;let n=sessionStorage.getItem(E(r,t));if(n!==null)try{return JSON.parse(n)}catch (e2){return}}function Y(e){let{scopeKey:r,inputId:t}=e;!r||!t||typeof sessionStorage>"u"||sessionStorage.removeItem(E(r,t))}function I(e){return _schemas.getInputItems.call(void 0, e).filter(r=>_optionalChain([r, 'access', _2 => _2.source, 'optionalAccess', _3 => _3.kind])==="client").map(r=>({paramId:r.paramId,displayName:_nullishCoalesce(r.displayName, () => (r.paramId))}))}function b(e,r,t){let n=new Set(I(e).map(s=>s.paramId)),l={};for(let s of e.inputs){if(n.has(s.id)){let i=t({scopeKey:r,inputId:s.id});i!==void 0&&(l[s.id]=i);continue}l[s.id]=_nullishCoalesce(s.default, () => (_schemas.getDefaultValue.call(void 0, s.paramType)))}for(let s of e.outputs)l[s.id]=null;return l}function w(e){let r=e===!1;return{hasPendingChanges:r,hasNeverSolved:r}}function C(e,r,t,n){return e.values[r]=t,n===!1?(e.pendingValues[r]=t,e.hasPendingChanges=!0,{state:e,shouldSolve:!1}):{state:e,shouldSolve:!0}}function P(e,r){if(!_optionalChain([e, 'optionalAccess', _4 => _4.inputs]))return r;let t={};for(let n of e.inputs)n.id in r&&(t[n.id]=r[n.id]);return t}function V(e,r){e.error="",e.computeErrors=_nullishCoalesce(r.errors, () => ([])),e.computeWarnings=_nullishCoalesce(r.warnings, () => ([])),e.meshes=_nullishCoalesce(r.meshes, () => ([]));let{meshes:t,...n}=r;return e.lastResult=n,Object.assign(e.values,r.outputs),e.pendingValues={},e.hasPendingChanges=!1,e.hasNeverSolved=!1,e}function Z(e){let r=e.schema,t=w(_optionalChain([r, 'optionalAccess', _5 => _5.instanceSolve])),n={values:b(r,e.scopeKey,x),error:"",computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:t.hasPendingChanges,hasNeverSolved:t.hasNeverSolved,lastResult:null},l=new Set,s=()=>l.forEach(o=>o());function i(){e.driver.solve({...P(r,n.values)}),s()}return{get values(){return n.values},get error(){return n.error},get computeErrors(){return n.computeErrors},get computeWarnings(){return n.computeWarnings},get meshes(){return n.meshes},get lastResult(){return n.lastResult},get hasPendingChanges(){return n.hasPendingChanges},get hasNeverSolved(){return n.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(o,a,u=!1){let{shouldSolve:p}=C(n,o,a,_optionalChain([r, 'optionalAccess', _6 => _6.instanceSolve]));p||u?(u&&!p&&(n.pendingValues={},n.hasPendingChanges=!1),i()):s()},solve(){i()},loadValues(o){Object.assign(n.values,o),_optionalChain([r, 'optionalAccess', _7 => _7.instanceSolve])!==!1?i():(n.hasPendingChanges=!0,s())},rebuild(o,a){r=o,_optionalChain([e, 'access', _8 => _8.driver, 'access', _9 => _9.clearCache, 'optionalCall', _10 => _10()]),n.meshes=[],n.lastResult=null,n.error="",n.computeErrors=[],n.computeWarnings=[],n.pendingValues={},n.values=b(o,a,x);let u=w(_optionalChain([o, 'optionalAccess', _11 => _11.instanceSolve]));n.hasPendingChanges=u.hasPendingChanges,n.hasNeverSolved=u.hasNeverSolved,o&&Object.keys(n.values).length>0&&o.instanceSolve!==!1?i():s()},report(o){V(n,o),s()},reportError(o){n.error=o,s()},subscribe(o){return l.add(o),()=>l.delete(o)},notify:s}}function F(e,r){let{runDeadlineMs:t,onChange:n}=r,l=!1,s=null,i=null;function o(m){l!==m&&(l=m,_optionalChain([n, 'optionalCall', _12 => _12()]))}function a(){_optionalChain([i, 'optionalAccess', _13 => _13.abort, 'call', _14 => _14()]),i=null}async function u(m){a(),i=new AbortController;let{signal:S}=i,y=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${t}ms \u2014 aborting`),_optionalChain([i, 'optionalAccess', _15 => _15.abort, 'call', _16 => _16()])},t);o(!0);try{await e(m,S)}catch(h){(!(h instanceof Error)||h.name!=="AbortError"&&h.name!=="TimeoutError")&&console.error("[Solve/throttle] unhandled error in run:",h)}finally{if(clearTimeout(y),o(!1),i=null,s!==null){let h=s;s=null,u(h)}}}function p(m){l?(s!==null&&console.debug("[Solve/throttle] superseded pending run (latest-wins)"),s=m):u(m)}function f(){s=null,a()}return{trigger:p,get isRunning(){return l},get hasPending(){return s!==null},cancel:f}}function O(e){return $(e)}function $(e){if(e===null||typeof e!="object")return _nullishCoalesce(JSON.stringify(e), () => ("null"));if(Array.isArray(e))return`[${e.map($).join(",")}]`;let r=e;return`{${Object.keys(r).sort().map(n=>`${JSON.stringify(n)}:${$(r[n])}`).join(",")}}`}function j(e={}){let{max:r=16,meshPolicy:t}=e,n=new Map;function l(o){_optionalChain([o, 'access', _17 => _17.meshes, 'optionalAccess', _18 => _18.length])&&_optionalChain([t, 'optionalAccess', _19 => _19.release, 'call', _20 => _20(o.meshes)])}function s(o){return!t||!_optionalChain([o, 'access', _21 => _21.meshes, 'optionalAccess', _22 => _22.length])?o:{...o,meshes:t.clone(o.meshes)}}function i(o){let a=n.get(o);a!==void 0&&(n.delete(o),l(a))}return{get(o){let a=O(o),u=n.get(a);if(u!==void 0)return n.delete(a),n.set(a,u),console.info(`[Solve/memo] HIT \u2014 served from client memo (${n.size}/${r})`),s(u)},set(o,a){let u=O(o);for(i(u),n.set(u,s(a));n.size>r;){let p=n.keys().next().value;if(p===void 0)break;i(p),console.info(`[Solve/memo] evicted LRU entry (cap ${r})`)}},clear(){n.size>0&&console.info(`[Solve/memo] cleared ${n.size} entries (definition changed)`),n.forEach(l),n.clear()}}}function ee(e,r,t){let{meshPolicy:n,solveDeadlineMs:l,onChange:s}=t,i=j({meshPolicy:n}),o=F(async(a,u)=>{let p=i.get(a);if(p!==void 0){if(u.aborted)return;r().report(p);return}try{let f=await e(a,u);if(u.aborted){console.debug("[Solve/driver] solve completed after abort \u2014 result discarded");return}let m={...f,values:a};i.set(a,m),r().report(m)}catch(f){if(u.aborted){console.debug("[Solve/driver] solve aborted (superseded, cancelled, or timed out)");return}console.warn("[Solve/driver] solve failed:",f),r().reportError(f instanceof Error?f.message:String(f))}},{runDeadlineMs:l,onChange:s});return{solve(a){o.trigger(a)},cancel(){o.cancel()},get isSolving(){return o.isRunning},clearCache(){i.clear()}}}var _compute = require('@selvajs/compute');function re(e){let r={};if(!e)return r;for(let t of e.split(",")){let[n,...l]=t.split(";").map(o=>o.trim()),s=l.find(o=>o.startsWith("dur="));if(!n||!s)continue;let i=Number(s.slice(4));Number.isFinite(i)&&(r[n]=i)}return r}var oe=e=>{throw new Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},te=()=>{throw new Error("Your session has expired. Sign in again in a new tab, then re-run \u2014 your inputs are preserved.")};function se(e){let r=_nullishCoalesce(e.debug, () => (!1)),t=(...i)=>{r&&console.info(...i)},n=null;function l(){if(!e.meshes)throw new Error("createComputeFetchSolveFn: meshes option not configured.");return n||(n=e.meshes.loadRhino()),n}let s=0;return async(i,o)=>{let a=s-Date.now();if(a>0)throw new Error(`Rate limit reached. Try again in ${Math.ceil(a/1e3)}s.`);let u=performance.now(),p=e.inputs(),f=e.outputs(),m=_optionalChain([e, 'access', _23 => _23.channel, 'optionalCall', _24 => _24()]),S=_optionalChain([e, 'access', _25 => _25.versionId, 'optionalCall', _26 => _26()]),y=JSON.stringify({inputs:p,values:i,definitionUrl:e.definitionUrl(),...S?{versionId:S}:m==="draft"?{channel:"draft"}:{}}),h=JSON.stringify(i).length,d;try{d=await fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:y,signal:o})}catch(c){if(o.aborted)return t("[Compute] solve aborted during fetch \u2014 empty result discarded"),{outputs:{}};throw new Error("Request blocked \u2014 your session may have expired. Reload the page to sign in again.",{cause:c})}if(o.aborted)return t("[Compute] solve aborted after headers \u2014 empty result discarded"),{outputs:{}};let J=d.redirected&&new URL(d.url).pathname==="/login";if((d.status===401||J)&&(_nullishCoalesce(e.onSessionExpired, () => (te)))(),!d.ok){if(d.status===503)throw new Error("Compute server is offline or unreachable. Please try again later.");let c=await d.text().catch(()=>""),v={};try{v=JSON.parse(c)}catch (e3){c&&console.warn(`[Compute] non-JSON error body (HTTP ${d.status}):`,c.slice(0,300))}if(d.status===429){let R=Number(d.headers.get("Retry-After"))||Number(v.retryAfter)||5;s=Date.now()+R*1e3,(_nullishCoalesce(e.onRateLimited, () => (oe)))(R)}throw new Error(v.message||`Compute error (HTTP ${d.status})`)}let N=performance.now()-u,B=performance.now(),A=await d.text();if(o.aborted)return t("[Compute] solve aborted mid-download \u2014 empty result discarded"),{outputs:{}};let T=performance.now()-B,D=A.length,g;try{g=JSON.parse(A)}catch(c){throw new Error("Received an invalid response from the server. Reload the page and try again.",{cause:c})}let M=new (0, _compute.GrasshopperResponseProcessor)(g,!1),z=e.meshes?await e.meshes.extract(M.response,{debug:r,rhino:await l()}):[],U={};for(let c of f){let v=M.getValue({byId:c.id},{parseValues:!0});U[c.id]=_nullishCoalesce(v, () => ((c.nickname?M.getValue({byName:c.nickname},{parseValues:!0}):void 0)))}if(r){let c=re(d.headers.get("Server-Timing")),v=_nullishCoalesce(c.total, () => (0)),R=Math.max(0,N-v),k=D/(1024*1024),q=k>=1?`${k.toFixed(2)} MB`:`${(D/1024).toFixed(0)} KB`,W=T>0?(k/(T/1e3)).toFixed(1):"\u221E",K=_optionalChain([performance, 'access', _27 => _27.memory, 'optionalAccess', _28 => _28.usedJSHeapSize]),G=K!==void 0?` | heap=${(K/(1024*1024)).toFixed(0)} MB`:"";if(t(`[Compute] req=${(y.length/1024).toFixed(0)}KB (values ${(h/1024).toFixed(0)}KB) | ttfb=${N.toFixed(0)}ms (network\u2248${R.toFixed(0)} + server ${v.toFixed(0)}) | download=${T.toFixed(0)}ms (${q} @ ${W} MB/s)${G}`),c.selva_cache!==void 0){let H=c.selva_cache?"HIT \u2014 served without calling compute":"miss \u2014 solved on Rhino.Compute";t(`[Compute] \u2514\u2500 cache: ${H}`)}}return{outputs:U,meshes:z,errors:_nullishCoalesce(g.errors, () => ([])),warnings:_nullishCoalesce(g.warnings, () => ([])),source:g}}}exports.applySolveResult = V; exports.applyValueChange = C; exports.buildInitialValues = b; exports.clearExternalValue = Y; exports.createAsyncThrottle = F; exports.createComputeFetchSolveFn = se; exports.createRequestResponseDriver = ee; exports.createSolveMemo = j; exports.createSolveSession = Z; exports.getExternalInputs = I; exports.makeInitialFlags = w; exports.pickInputValues = P; exports.readExternalValue = x; exports.stableInputKey = O; exports.writeExternalValue = X;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _schemas = require('@selvajs/schemas');var H="external";function k(e,r){return`${H}:${e}:${r}`}function L(e){let{scopeKey:r,inputId:s,value:n}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.setItem(k(r,s),JSON.stringify(n))}function y(e){let{scopeKey:r,inputId:s}=e;if(!r||!s||typeof sessionStorage>"u")return;let n=sessionStorage.getItem(k(r,s));if(n!==null)try{return JSON.parse(n)}catch (e2){return}}function _(e){let{scopeKey:r,inputId:s}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.removeItem(k(r,s))}function E(e){return _schemas.getInputItems.call(void 0, e).filter(r=>_optionalChain([r, 'access', _2 => _2.source, 'optionalAccess', _3 => _3.kind])==="client").map(r=>({paramId:r.paramId,displayName:_nullishCoalesce(r.displayName, () => (r.paramId))}))}function R(e,r,s){let n=new Set(E(e).map(t=>t.paramId)),a={};for(let t of e.inputs){if(n.has(t.id)){let i=s({scopeKey:r,inputId:t.id});i!==void 0&&(a[t.id]=i);continue}a[t.id]=_nullishCoalesce(t.default, () => (_schemas.getDefaultValue.call(void 0, t.paramType)))}for(let t of e.outputs)a[t.id]=null;return a}function x(e){let r=e===!1;return{hasPendingChanges:r,hasNeverSolved:r}}function I(e,r,s,n){return e.values[r]=s,n===!1?(e.pendingValues[r]=s,e.hasPendingChanges=!0,{state:e,shouldSolve:!1}):{state:e,shouldSolve:!0}}function C(e,r){if(!_optionalChain([e, 'optionalAccess', _4 => _4.inputs]))return r;let s={};for(let n of e.inputs)n.id in r&&(s[n.id]=r[n.id]);return s}function V(e,r){e.error="",e.computeErrors=_nullishCoalesce(r.errors, () => ([])),e.computeWarnings=_nullishCoalesce(r.warnings, () => ([])),e.meshes=_nullishCoalesce(r.meshes, () => ([]));let{meshes:s,...n}=r;return e.lastResult=n,Object.assign(e.values,r.outputs),e.pendingValues={},e.hasPendingChanges=!1,e.hasNeverSolved=!1,e}function Y(e){let r=e.schema,s=x(_optionalChain([r, 'optionalAccess', _5 => _5.instanceSolve])),n={values:R(r,e.scopeKey,y),error:"",computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:s.hasPendingChanges,hasNeverSolved:s.hasNeverSolved,lastResult:null},a=new Set,t=()=>a.forEach(o=>o());function i(){e.driver.solve({...C(r,n.values)}),t()}return{get values(){return n.values},get error(){return n.error},get computeErrors(){return n.computeErrors},get computeWarnings(){return n.computeWarnings},get meshes(){return n.meshes},get lastResult(){return n.lastResult},get hasPendingChanges(){return n.hasPendingChanges},get hasNeverSolved(){return n.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(o,l,u=!1){let{shouldSolve:f}=I(n,o,l,_optionalChain([r, 'optionalAccess', _6 => _6.instanceSolve]));f||u?(u&&!f&&(n.pendingValues={},n.hasPendingChanges=!1),i()):t()},solve(){i()},loadValues(o){Object.assign(n.values,o),_optionalChain([r, 'optionalAccess', _7 => _7.instanceSolve])!==!1?i():(n.hasPendingChanges=!0,t())},rebuild(o,l){r=o,_optionalChain([e, 'access', _8 => _8.driver, 'access', _9 => _9.clearCache, 'optionalCall', _10 => _10()]),n.meshes=[],n.lastResult=null,n.error="",n.computeErrors=[],n.computeWarnings=[],n.pendingValues={},n.values=R(o,l,y);let u=x(_optionalChain([o, 'optionalAccess', _11 => _11.instanceSolve]));n.hasPendingChanges=u.hasPendingChanges,n.hasNeverSolved=u.hasNeverSolved,o&&Object.keys(n.values).length>0&&o.instanceSolve!==!1?i():t()},report(o){V(n,o),t()},reportError(o){n.error=o,t()},subscribe(o){return a.add(o),()=>a.delete(o)},notify:t}}function P(e,r){let{runDeadlineMs:s,onChange:n}=r,a=!1,t=null,i=null;function o(p){a!==p&&(a=p,_optionalChain([n, 'optionalCall', _12 => _12()]))}function l(){_optionalChain([i, 'optionalAccess', _13 => _13.abort, 'call', _14 => _14()]),i=null}async function u(p){l(),i=new AbortController;let{signal:b}=i,d=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${s}ms \u2014 aborting`),_optionalChain([i, 'optionalAccess', _15 => _15.abort, 'call', _16 => _16()])},s);o(!0);try{await e(p,b)}catch(v){(!(v instanceof Error)||v.name!=="AbortError"&&v.name!=="TimeoutError")&&console.error("[Solve/throttle] unhandled error in run:",v)}finally{if(clearTimeout(d),o(!1),i=null,t!==null){let v=t;t=null,u(v)}}}function f(p){a?(t!==null&&console.debug("[Solve/throttle] superseded pending run (latest-wins)"),t=p):u(p)}function m(){t=null,l()}return{trigger:f,get isRunning(){return a},get hasPending(){return t!==null},cancel:m}}function F(e){return O(e)}function O(e){if(e===null||typeof e!="object")return _nullishCoalesce(JSON.stringify(e), () => ("null"));if(Array.isArray(e))return`[${e.map(O).join(",")}]`;let r=e;return`{${Object.keys(r).sort().map(n=>`${JSON.stringify(n)}:${O(r[n])}`).join(",")}}`}function $(e={}){let{max:r=16,meshPolicy:s}=e,n=new Map;function a(o){_optionalChain([o, 'access', _17 => _17.meshes, 'optionalAccess', _18 => _18.length])&&_optionalChain([s, 'optionalAccess', _19 => _19.release, 'call', _20 => _20(o.meshes)])}function t(o){return!s||!_optionalChain([o, 'access', _21 => _21.meshes, 'optionalAccess', _22 => _22.length])?o:{...o,meshes:s.clone(o.meshes)}}function i(o){let l=n.get(o);l!==void 0&&(n.delete(o),a(l))}return{get(o){let l=F(o),u=n.get(l);if(u!==void 0)return n.delete(l),n.set(l,u),console.info(`[Solve/memo] HIT \u2014 served from client memo (${n.size}/${r})`),t(u)},set(o,l){let u=F(o);for(i(u),n.set(u,t(l));n.size>r;){let f=n.keys().next().value;if(f===void 0)break;i(f),console.info(`[Solve/memo] evicted LRU entry (cap ${r})`)}},clear(){n.size>0&&console.info(`[Solve/memo] cleared ${n.size} entries (definition changed)`),n.forEach(a),n.clear()}}}function Q(e,r,s){let{meshPolicy:n,solveDeadlineMs:a,onChange:t}=s,i=$({meshPolicy:n}),o=P(async(l,u)=>{let f=i.get(l);if(f!==void 0){if(u.aborted)return;r().report(f);return}try{let m=await e(l,u);if(u.aborted){console.debug("[Solve/driver] solve completed after abort \u2014 result discarded");return}let p={...m,values:l};i.set(l,p),r().report(p)}catch(m){if(u.aborted){console.debug("[Solve/driver] solve aborted (superseded, cancelled, or timed out)");return}console.warn("[Solve/driver] solve failed:",m),r().reportError(m instanceof Error?m.message:String(m))}},{runDeadlineMs:a,onChange:t});return{solve(l){o.trigger(l)},cancel(){o.cancel()},get isSolving(){return o.isRunning},clearCache(){i.clear()}}}var _grasshopper = require('@selvajs/compute/grasshopper');function ee(e){let r={};if(!e)return r;for(let s of e.split(",")){let[n,...a]=s.split(";").map(o=>o.trim()),t=a.find(o=>o.startsWith("dur="));if(!n||!t)continue;let i=Number(t.slice(4));Number.isFinite(i)&&(r[n]=i)}return r}var ne=e=>{throw new Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},re=()=>{throw new Error("Your session has expired. Sign in again in a new tab, then re-run \u2014 your inputs are preserved.")};function oe(e){let r=_nullishCoalesce(e.debug, () => (!1)),s=(...a)=>{r&&console.info(...a)},n=0;return async(a,t)=>{let i=n-Date.now();if(i>0)throw new Error(`Rate limit reached. Try again in ${Math.ceil(i/1e3)}s.`);let o=performance.now(),l=e.inputs(),u=e.outputs(),f=_optionalChain([e, 'access', _23 => _23.channel, 'optionalCall', _24 => _24()]),m=_optionalChain([e, 'access', _25 => _25.versionId, 'optionalCall', _26 => _26()]),p=JSON.stringify({inputs:l,values:a,definitionUrl:e.definitionUrl(),...m?{versionId:m}:f==="draft"?{channel:"draft"}:{}}),b=JSON.stringify(a).length,d;try{d=await fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:p,signal:t})}catch(c){if(t.aborted)return s("[Compute] solve aborted during fetch \u2014 empty result discarded"),{outputs:{}};throw new Error("Request blocked \u2014 your session may have expired. Reload the page to sign in again.",{cause:c})}if(t.aborted)return s("[Compute] solve aborted after headers \u2014 empty result discarded"),{outputs:{}};let v=d.redirected&&new URL(d.url).pathname==="/login";if((d.status===401||v)&&(_nullishCoalesce(e.onSessionExpired, () => (re)))(),!d.ok){if(d.status===503)throw new Error("Compute server is offline or unreachable. Please try again later.");let c=await d.text().catch(()=>""),h={};try{h=JSON.parse(c)}catch (e3){c&&console.warn(`[Compute] non-JSON error body (HTTP ${d.status}):`,c.slice(0,300))}if(d.status===429){let S=Number(d.headers.get("Retry-After"))||Number(h.retryAfter)||5;n=Date.now()+S*1e3,(_nullishCoalesce(e.onRateLimited, () => (ne)))(S)}throw new Error(h.message||`Compute error (HTTP ${d.status})`)}let j=performance.now()-o,K=performance.now(),N=await d.text();if(t.aborted)return s("[Compute] solve aborted mid-download \u2014 empty result discarded"),{outputs:{}};let w=performance.now()-K,A=N.length,g;try{g=JSON.parse(N)}catch(c){throw new Error("Received an invalid response from the server. Reload the page and try again.",{cause:c})}let T=new (0, _grasshopper.GrasshopperResponseProcessor)(g,!1),J=e.meshes?await e.meshes.extract(T.response,{debug:r}):[],D={};for(let c of u){let h=T.getValue({byId:c.id},{parseValues:!0});D[c.id]=_nullishCoalesce(h, () => ((c.nickname?T.getValue({byName:c.nickname},{parseValues:!0}):void 0)))}if(r){let c=ee(d.headers.get("Server-Timing")),h=_nullishCoalesce(c.total, () => (0)),S=Math.max(0,j-h),M=A/(1024*1024),B=M>=1?`${M.toFixed(2)} MB`:`${(A/1024).toFixed(0)} KB`,z=w>0?(M/(w/1e3)).toFixed(1):"\u221E",U=_optionalChain([performance, 'access', _27 => _27.memory, 'optionalAccess', _28 => _28.usedJSHeapSize]),q=U!==void 0?` | heap=${(U/(1024*1024)).toFixed(0)} MB`:"";if(s(`[Compute] req=${(p.length/1024).toFixed(0)}KB (values ${(b/1024).toFixed(0)}KB) | ttfb=${j.toFixed(0)}ms (network\u2248${S.toFixed(0)} + server ${h.toFixed(0)}) | download=${w.toFixed(0)}ms (${B} @ ${z} MB/s)${q}`),c.selva_cache!==void 0){let W=c.selva_cache?"HIT \u2014 served without calling compute":"miss \u2014 solved on Rhino.Compute";s(`[Compute] \u2514\u2500 cache: ${W}`)}}return{outputs:D,meshes:J,errors:_nullishCoalesce(g.errors, () => ([])),warnings:_nullishCoalesce(g.warnings, () => ([])),source:g}}}exports.applySolveResult = V; exports.applyValueChange = I; exports.buildInitialValues = R; exports.clearExternalValue = _; exports.createAsyncThrottle = P; exports.createComputeFetchSolveFn = oe; exports.createRequestResponseDriver = Q; exports.createSolveMemo = $; exports.createSolveSession = Y; exports.getExternalInputs = E; exports.makeInitialFlags = x; exports.pickInputValues = C; exports.readExternalValue = y; exports.stableInputKey = F; exports.writeExternalValue = L;
|
|
2
2
|
//# sourceMappingURL=client.cjs.map
|
package/dist/client.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UISchema } from '@selvajs/schemas';
|
|
2
2
|
import { a as SolveResult, S as SolveFn } from './solve-fn-DrRgaPZ5.cjs';
|
|
3
|
-
import { GrasshopperComputeResponse } from '@selvajs/compute';
|
|
3
|
+
import { GrasshopperComputeResponse } from '@selvajs/compute/grasshopper';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Transport behind a solve session. `solve()` never returns outputs directly — results
|
|
@@ -225,7 +225,7 @@ declare function createAsyncThrottle<T>(run: (values: T, signal: AbortSignal) =>
|
|
|
225
225
|
* `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).
|
|
226
226
|
*/
|
|
227
227
|
|
|
228
|
-
interface ComputeFetchSolveFnOptions<TMesh = unknown
|
|
228
|
+
interface ComputeFetchSolveFnOptions<TMesh = unknown> {
|
|
229
229
|
/** The compute endpoint to POST to, e.g. `/api/compute`. */
|
|
230
230
|
endpoint: string;
|
|
231
231
|
/** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */
|
|
@@ -241,16 +241,10 @@ interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {
|
|
|
241
241
|
}>;
|
|
242
242
|
channel?: () => 'live' | 'draft' | undefined;
|
|
243
243
|
versionId?: () => string | null | undefined;
|
|
244
|
-
/**
|
|
245
|
-
* Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`
|
|
246
|
-
* and no rhino3dm module is ever loaded. `loadRhino` is called lazily on the
|
|
247
|
-
* first solve that needs meshes and cached for the lifetime of this `SolveFn`.
|
|
248
|
-
*/
|
|
244
|
+
/** Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`. */
|
|
249
245
|
meshes?: {
|
|
250
|
-
loadRhino: () => Promise<TRhino>;
|
|
251
246
|
extract: (response: GrasshopperComputeResponse, opts: {
|
|
252
247
|
debug: boolean;
|
|
253
|
-
rhino: TRhino;
|
|
254
248
|
}) => TMesh[] | Promise<TMesh[]>;
|
|
255
249
|
};
|
|
256
250
|
/** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */
|
|
@@ -260,6 +254,6 @@ interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {
|
|
|
260
254
|
/** Called on a 401 or a login-page redirect. Default: throws an actionable "reload to sign in again" Error. */
|
|
261
255
|
onSessionExpired?: () => void;
|
|
262
256
|
}
|
|
263
|
-
declare function createComputeFetchSolveFn<TMesh = unknown
|
|
257
|
+
declare function createComputeFetchSolveFn<TMesh = unknown>(opts: ComputeFetchSolveFnOptions<TMesh>): SolveFn<TMesh, GrasshopperComputeResponse>;
|
|
264
258
|
|
|
265
259
|
export { type AsyncThrottle, type ComputeFetchSolveFnOptions, type ExternalInput, type ExternalReader, type ExternalValueRef, type MeshPolicy, type RequestResponseDriverOptions, type RetainedSolveResult, type SolveDriver, SolveFn, type SolveMemo, type SolveMemoOptions, type SolveReporter, SolveResult, type SolveSession, type SolveSessionArgs, type SolveSessionState, applySolveResult, applyValueChange, buildInitialValues, clearExternalValue, createAsyncThrottle, createComputeFetchSolveFn, createRequestResponseDriver, createSolveMemo, createSolveSession, getExternalInputs, makeInitialFlags, pickInputValues, readExternalValue, stableInputKey, writeExternalValue };
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UISchema } from '@selvajs/schemas';
|
|
2
2
|
import { a as SolveResult, S as SolveFn } from './solve-fn-DrRgaPZ5.js';
|
|
3
|
-
import { GrasshopperComputeResponse } from '@selvajs/compute';
|
|
3
|
+
import { GrasshopperComputeResponse } from '@selvajs/compute/grasshopper';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Transport behind a solve session. `solve()` never returns outputs directly — results
|
|
@@ -225,7 +225,7 @@ declare function createAsyncThrottle<T>(run: (values: T, signal: AbortSignal) =>
|
|
|
225
225
|
* `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).
|
|
226
226
|
*/
|
|
227
227
|
|
|
228
|
-
interface ComputeFetchSolveFnOptions<TMesh = unknown
|
|
228
|
+
interface ComputeFetchSolveFnOptions<TMesh = unknown> {
|
|
229
229
|
/** The compute endpoint to POST to, e.g. `/api/compute`. */
|
|
230
230
|
endpoint: string;
|
|
231
231
|
/** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */
|
|
@@ -241,16 +241,10 @@ interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {
|
|
|
241
241
|
}>;
|
|
242
242
|
channel?: () => 'live' | 'draft' | undefined;
|
|
243
243
|
versionId?: () => string | null | undefined;
|
|
244
|
-
/**
|
|
245
|
-
* Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`
|
|
246
|
-
* and no rhino3dm module is ever loaded. `loadRhino` is called lazily on the
|
|
247
|
-
* first solve that needs meshes and cached for the lifetime of this `SolveFn`.
|
|
248
|
-
*/
|
|
244
|
+
/** Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`. */
|
|
249
245
|
meshes?: {
|
|
250
|
-
loadRhino: () => Promise<TRhino>;
|
|
251
246
|
extract: (response: GrasshopperComputeResponse, opts: {
|
|
252
247
|
debug: boolean;
|
|
253
|
-
rhino: TRhino;
|
|
254
248
|
}) => TMesh[] | Promise<TMesh[]>;
|
|
255
249
|
};
|
|
256
250
|
/** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */
|
|
@@ -260,6 +254,6 @@ interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {
|
|
|
260
254
|
/** Called on a 401 or a login-page redirect. Default: throws an actionable "reload to sign in again" Error. */
|
|
261
255
|
onSessionExpired?: () => void;
|
|
262
256
|
}
|
|
263
|
-
declare function createComputeFetchSolveFn<TMesh = unknown
|
|
257
|
+
declare function createComputeFetchSolveFn<TMesh = unknown>(opts: ComputeFetchSolveFnOptions<TMesh>): SolveFn<TMesh, GrasshopperComputeResponse>;
|
|
264
258
|
|
|
265
259
|
export { type AsyncThrottle, type ComputeFetchSolveFnOptions, type ExternalInput, type ExternalReader, type ExternalValueRef, type MeshPolicy, type RequestResponseDriverOptions, type RetainedSolveResult, type SolveDriver, SolveFn, type SolveMemo, type SolveMemoOptions, type SolveReporter, SolveResult, type SolveSession, type SolveSessionArgs, type SolveSessionState, applySolveResult, applyValueChange, buildInitialValues, clearExternalValue, createAsyncThrottle, createComputeFetchSolveFn, createRequestResponseDriver, createSolveMemo, createSolveSession, getExternalInputs, makeInitialFlags, pickInputValues, readExternalValue, stableInputKey, writeExternalValue };
|
package/dist/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getInputItems as
|
|
1
|
+
import{getInputItems as G}from"@selvajs/schemas";var H="external";function k(e,r){return`${H}:${e}:${r}`}function L(e){let{scopeKey:r,inputId:s,value:n}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.setItem(k(r,s),JSON.stringify(n))}function y(e){let{scopeKey:r,inputId:s}=e;if(!r||!s||typeof sessionStorage>"u")return;let n=sessionStorage.getItem(k(r,s));if(n!==null)try{return JSON.parse(n)}catch{return}}function _(e){let{scopeKey:r,inputId:s}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.removeItem(k(r,s))}function E(e){return G(e).filter(r=>r.source?.kind==="client").map(r=>({paramId:r.paramId,displayName:r.displayName??r.paramId}))}import{getDefaultValue as X}from"@selvajs/schemas";function R(e,r,s){let n=new Set(E(e).map(t=>t.paramId)),a={};for(let t of e.inputs){if(n.has(t.id)){let i=s({scopeKey:r,inputId:t.id});i!==void 0&&(a[t.id]=i);continue}a[t.id]=t.default??X(t.paramType)}for(let t of e.outputs)a[t.id]=null;return a}function x(e){let r=e===!1;return{hasPendingChanges:r,hasNeverSolved:r}}function I(e,r,s,n){return e.values[r]=s,n===!1?(e.pendingValues[r]=s,e.hasPendingChanges=!0,{state:e,shouldSolve:!1}):{state:e,shouldSolve:!0}}function C(e,r){if(!e?.inputs)return r;let s={};for(let n of e.inputs)n.id in r&&(s[n.id]=r[n.id]);return s}function V(e,r){e.error="",e.computeErrors=r.errors??[],e.computeWarnings=r.warnings??[],e.meshes=r.meshes??[];let{meshes:s,...n}=r;return e.lastResult=n,Object.assign(e.values,r.outputs),e.pendingValues={},e.hasPendingChanges=!1,e.hasNeverSolved=!1,e}function Y(e){let r=e.schema,s=x(r?.instanceSolve),n={values:R(r,e.scopeKey,y),error:"",computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:s.hasPendingChanges,hasNeverSolved:s.hasNeverSolved,lastResult:null},a=new Set,t=()=>a.forEach(o=>o());function i(){e.driver.solve({...C(r,n.values)}),t()}return{get values(){return n.values},get error(){return n.error},get computeErrors(){return n.computeErrors},get computeWarnings(){return n.computeWarnings},get meshes(){return n.meshes},get lastResult(){return n.lastResult},get hasPendingChanges(){return n.hasPendingChanges},get hasNeverSolved(){return n.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(o,l,u=!1){let{shouldSolve:f}=I(n,o,l,r?.instanceSolve);f||u?(u&&!f&&(n.pendingValues={},n.hasPendingChanges=!1),i()):t()},solve(){i()},loadValues(o){Object.assign(n.values,o),r?.instanceSolve!==!1?i():(n.hasPendingChanges=!0,t())},rebuild(o,l){r=o,e.driver.clearCache?.(),n.meshes=[],n.lastResult=null,n.error="",n.computeErrors=[],n.computeWarnings=[],n.pendingValues={},n.values=R(o,l,y);let u=x(o?.instanceSolve);n.hasPendingChanges=u.hasPendingChanges,n.hasNeverSolved=u.hasNeverSolved,o&&Object.keys(n.values).length>0&&o.instanceSolve!==!1?i():t()},report(o){V(n,o),t()},reportError(o){n.error=o,t()},subscribe(o){return a.add(o),()=>a.delete(o)},notify:t}}function P(e,r){let{runDeadlineMs:s,onChange:n}=r,a=!1,t=null,i=null;function o(p){a!==p&&(a=p,n?.())}function l(){i?.abort(),i=null}async function u(p){l(),i=new AbortController;let{signal:b}=i,d=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${s}ms \u2014 aborting`),i?.abort()},s);o(!0);try{await e(p,b)}catch(v){(!(v instanceof Error)||v.name!=="AbortError"&&v.name!=="TimeoutError")&&console.error("[Solve/throttle] unhandled error in run:",v)}finally{if(clearTimeout(d),o(!1),i=null,t!==null){let v=t;t=null,u(v)}}}function f(p){a?(t!==null&&console.debug("[Solve/throttle] superseded pending run (latest-wins)"),t=p):u(p)}function m(){t=null,l()}return{trigger:f,get isRunning(){return a},get hasPending(){return t!==null},cancel:m}}function F(e){return O(e)}function O(e){if(e===null||typeof e!="object")return JSON.stringify(e)??"null";if(Array.isArray(e))return`[${e.map(O).join(",")}]`;let r=e;return`{${Object.keys(r).sort().map(n=>`${JSON.stringify(n)}:${O(r[n])}`).join(",")}}`}function $(e={}){let{max:r=16,meshPolicy:s}=e,n=new Map;function a(o){o.meshes?.length&&s?.release(o.meshes)}function t(o){return!s||!o.meshes?.length?o:{...o,meshes:s.clone(o.meshes)}}function i(o){let l=n.get(o);l!==void 0&&(n.delete(o),a(l))}return{get(o){let l=F(o),u=n.get(l);if(u!==void 0)return n.delete(l),n.set(l,u),console.info(`[Solve/memo] HIT \u2014 served from client memo (${n.size}/${r})`),t(u)},set(o,l){let u=F(o);for(i(u),n.set(u,t(l));n.size>r;){let f=n.keys().next().value;if(f===void 0)break;i(f),console.info(`[Solve/memo] evicted LRU entry (cap ${r})`)}},clear(){n.size>0&&console.info(`[Solve/memo] cleared ${n.size} entries (definition changed)`),n.forEach(a),n.clear()}}}function Q(e,r,s){let{meshPolicy:n,solveDeadlineMs:a,onChange:t}=s,i=$({meshPolicy:n}),o=P(async(l,u)=>{let f=i.get(l);if(f!==void 0){if(u.aborted)return;r().report(f);return}try{let m=await e(l,u);if(u.aborted){console.debug("[Solve/driver] solve completed after abort \u2014 result discarded");return}let p={...m,values:l};i.set(l,p),r().report(p)}catch(m){if(u.aborted){console.debug("[Solve/driver] solve aborted (superseded, cancelled, or timed out)");return}console.warn("[Solve/driver] solve failed:",m),r().reportError(m instanceof Error?m.message:String(m))}},{runDeadlineMs:a,onChange:t});return{solve(l){o.trigger(l)},cancel(){o.cancel()},get isSolving(){return o.isRunning},clearCache(){i.clear()}}}import{GrasshopperResponseProcessor as Z}from"@selvajs/compute/grasshopper";function ee(e){let r={};if(!e)return r;for(let s of e.split(",")){let[n,...a]=s.split(";").map(o=>o.trim()),t=a.find(o=>o.startsWith("dur="));if(!n||!t)continue;let i=Number(t.slice(4));Number.isFinite(i)&&(r[n]=i)}return r}var ne=e=>{throw new Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},re=()=>{throw new Error("Your session has expired. Sign in again in a new tab, then re-run \u2014 your inputs are preserved.")};function oe(e){let r=e.debug??!1,s=(...a)=>{r&&console.info(...a)},n=0;return async(a,t)=>{let i=n-Date.now();if(i>0)throw new Error(`Rate limit reached. Try again in ${Math.ceil(i/1e3)}s.`);let o=performance.now(),l=e.inputs(),u=e.outputs(),f=e.channel?.(),m=e.versionId?.(),p=JSON.stringify({inputs:l,values:a,definitionUrl:e.definitionUrl(),...m?{versionId:m}:f==="draft"?{channel:"draft"}:{}}),b=JSON.stringify(a).length,d;try{d=await fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:p,signal:t})}catch(c){if(t.aborted)return s("[Compute] solve aborted during fetch \u2014 empty result discarded"),{outputs:{}};throw new Error("Request blocked \u2014 your session may have expired. Reload the page to sign in again.",{cause:c})}if(t.aborted)return s("[Compute] solve aborted after headers \u2014 empty result discarded"),{outputs:{}};let v=d.redirected&&new URL(d.url).pathname==="/login";if((d.status===401||v)&&(e.onSessionExpired??re)(),!d.ok){if(d.status===503)throw new Error("Compute server is offline or unreachable. Please try again later.");let c=await d.text().catch(()=>""),h={};try{h=JSON.parse(c)}catch{c&&console.warn(`[Compute] non-JSON error body (HTTP ${d.status}):`,c.slice(0,300))}if(d.status===429){let S=Number(d.headers.get("Retry-After"))||Number(h.retryAfter)||5;n=Date.now()+S*1e3,(e.onRateLimited??ne)(S)}throw new Error(h.message||`Compute error (HTTP ${d.status})`)}let j=performance.now()-o,K=performance.now(),N=await d.text();if(t.aborted)return s("[Compute] solve aborted mid-download \u2014 empty result discarded"),{outputs:{}};let w=performance.now()-K,A=N.length,g;try{g=JSON.parse(N)}catch(c){throw new Error("Received an invalid response from the server. Reload the page and try again.",{cause:c})}let T=new Z(g,!1),J=e.meshes?await e.meshes.extract(T.response,{debug:r}):[],D={};for(let c of u){let h=T.getValue({byId:c.id},{parseValues:!0});D[c.id]=h??(c.nickname?T.getValue({byName:c.nickname},{parseValues:!0}):void 0)}if(r){let c=ee(d.headers.get("Server-Timing")),h=c.total??0,S=Math.max(0,j-h),M=A/(1024*1024),B=M>=1?`${M.toFixed(2)} MB`:`${(A/1024).toFixed(0)} KB`,z=w>0?(M/(w/1e3)).toFixed(1):"\u221E",U=performance.memory?.usedJSHeapSize,q=U!==void 0?` | heap=${(U/(1024*1024)).toFixed(0)} MB`:"";if(s(`[Compute] req=${(p.length/1024).toFixed(0)}KB (values ${(b/1024).toFixed(0)}KB) | ttfb=${j.toFixed(0)}ms (network\u2248${S.toFixed(0)} + server ${h.toFixed(0)}) | download=${w.toFixed(0)}ms (${B} @ ${z} MB/s)${q}`),c.selva_cache!==void 0){let W=c.selva_cache?"HIT \u2014 served without calling compute":"miss \u2014 solved on Rhino.Compute";s(`[Compute] \u2514\u2500 cache: ${W}`)}}return{outputs:D,meshes:J,errors:g.errors??[],warnings:g.warnings??[],source:g}}}export{V as applySolveResult,I as applyValueChange,R as buildInitialValues,_ as clearExternalValue,P as createAsyncThrottle,oe as createComputeFetchSolveFn,Q as createRequestResponseDriver,$ as createSolveMemo,Y as createSolveSession,E as getExternalInputs,x as makeInitialFlags,C as pickInputValues,y as readExternalValue,F as stableInputKey,L as writeExternalValue};
|
|
2
2
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/external-storage.ts","../src/client/solve-session-core.ts","../src/client/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts","../src/client/compute-fetch-solve-fn.ts"],"sourcesContent":["// Transit storage for inputs with source.kind === 'client': a producer route writes the\n// value here, the solver route reads it back. Keyed by (scopeKey, inputId) so values\n// don't bleed across solver contexts — scopeKey is whatever uniquely identifies one\n// (sessionId in plugin-ui/preview, definition guid in selva/library).\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nfunction makeKey(scopeKey: string, inputId: string): string {\n\treturn `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;\n}\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst { scopeKey, inputId, value } = args;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return undefined;\n\tif (typeof sessionStorage === 'undefined') return undefined;\n\tconst raw = sessionStorage.getItem(makeKey(scopeKey, inputId));\n\tif (raw === null) return undefined;\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function clearExternalValue(ref: ExternalValueRef): void {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.removeItem(makeKey(scopeKey, inputId));\n}\n\nexport interface ExternalInput {\n\tparamId: string;\n\tdisplayName: string;\n}\n\nexport function getExternalInputs(schema: UISchema): ExternalInput[] {\n\treturn getInputItems(schema)\n\t\t.filter((item) => item.source?.kind === 'client')\n\t\t.map((item) => ({\n\t\t\tparamId: item.paramId,\n\t\t\tdisplayName: item.displayName ?? item.paramId\n\t\t}));\n}\n","// Pure transition logic for a Solve Session.\n//\n// This module holds the value/lifecycle state machine with no transport and no state\n// ownership: how values are seeded (including client-sourced hydration), when a value\n// change should dispatch a solve vs. defer it, and what a reported solve result does to\n// the flags. `solve-session.ts` is a thin shell that owns the state object and the\n// subscriber set; every decision lives here, which is what makes it directly testable.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getDefaultValue } from '@selvajs/schemas';\nimport { getExternalInputs, type ExternalValueRef } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\n\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\tmeshes: unknown[];\n\t/** Values changed since the last solve, in manual (instanceSolve === false) mode. */\n\tpendingValues: Record<string, unknown>;\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n\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}\n\nexport type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;\n\n/**\n * Client-sourced inputs are hydrated from `read` and left absent when no stored value\n * exists, so the missing-inputs panel can detect them. Other inputs fall back to their\n * declared default, then the paramType default.\n */\nexport function buildInitialValues(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): Record<string, unknown> {\n\tconst clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));\n\tconst values: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (clientSet.has(input.id)) {\n\t\t\tconst stored = read({ scopeKey, inputId: input.id });\n\t\t\tif (stored !== undefined) values[input.id] = stored;\n\t\t\tcontinue;\n\t\t}\n\t\tvalues[input.id] = input.default ?? getDefaultValue(input.paramType);\n\t}\n\tfor (const output of schema.outputs) {\n\t\tvalues[output.id] = null;\n\t}\n\treturn values;\n}\n\n/** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */\nexport function makeInitialFlags(instanceSolve: boolean | undefined): {\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n} {\n\tconst manual = instanceSolve === false;\n\treturn { hasPendingChanges: manual, hasNeverSolved: manual };\n}\n\n/** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined\n): { state: SolveSessionState; shouldSolve: boolean } {\n\tstate.values[id] = value;\n\tif (instanceSolve === false) {\n\t\tstate.pendingValues[id] = value;\n\t\tstate.hasPendingChanges = true;\n\t\treturn { state, shouldSolve: false };\n\t}\n\treturn { state, shouldSolve: true };\n}\n\n/**\n * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value\n * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB\n * options-list output showed this can re-upload a multi-MB payload no backend reads.\n */\nexport function pickInputValues(\n\tschema: UISchema | undefined,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tif (!schema?.inputs) return values;\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (input.id in values) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\nexport function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.meshes = result.meshes ?? [];\n\tconst { meshes: _meshes, ...retained } = result;\n\tstate.lastResult = retained;\n\tObject.assign(state.values, result.outputs);\n\tstate.pendingValues = {};\n\tstate.hasPendingChanges = false;\n\tstate.hasNeverSolved = false;\n\treturn state;\n}\n","// A framework-free shell over the pure transition logic in solve-session-core.ts: owns the\n// live values/flags, delegates every transition to the core, and drives solves through a\n// transport-agnostic SolveDriver.\n//\n// State is exposed as plain getters; it carries no framework reactivity of its own, so a\n// Svelte host must wrap it — see `useSolveSession.svelte.ts` in `@selvajs/ui`, which\n// republishes these getters as $state. Reading a getter without subscribing gives a\n// correct value but will not re-render.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { readExternalValue } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\nimport type { SolveDriver } from './drivers/driver.js';\nimport {\n\tbuildInitialValues,\n\tmakeInitialFlags,\n\tapplyValueChange,\n\tapplySolveResult,\n\tpickInputValues,\n\ttype SolveSessionState,\n\ttype RetainedSolveResult\n} from './solve-session-core.js';\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\treadonly meshes: unknown[];\n\t/**\n\t * The last reported result without its meshes — what the viewer is currently showing,\n\t * including the `source`/`values` pair a commit path needs. Null before the first solve,\n\t * and again after `rebuild()`. Survives `reportError`: a failed solve leaves the last good\n\t * geometry on screen, so a host committing what it sees 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\tsolve(): void;\n\t/** Merges incoming values, then solves (auto) or marks dirty (manual). */\n\tloadValues(incoming: Record<string, unknown>): void;\n\trebuild(schema: UISchema, scopeKey: string): void;\n\treport(result: SolveResult): void;\n\treportError(message: string): void;\n\t/** Fires after any mutation of the getters above, including driver-reported `isSolving` transitions. */\n\tsubscribe(listener: () => void): () => void;\n\t/** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */\n\tnotify(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\n\tconst flags = makeInitialFlags(currentSchema?.instanceSolve);\n\tconst state: SolveSessionState = {\n\t\tvalues: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tmeshes: [],\n\t\tpendingValues: {},\n\t\thasPendingChanges: flags.hasPendingChanges,\n\t\thasNeverSolved: flags.hasNeverSolved,\n\t\tlastResult: null\n\t};\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => listeners.forEach((l) => l());\n\n\tfunction dispatch() {\n\t\t// Copy: state.values is the live map, and a driver may hold the payload across an\n\t\t// async solve — a later setValue must not mutate what's already in flight.\n\t\targs.driver.solve({ ...pickInputValues(currentSchema, state.values) });\n\t\temit();\n\t}\n\n\treturn {\n\t\tget values() {\n\t\t\treturn state.values;\n\t\t},\n\t\tget error() {\n\t\t\treturn state.error;\n\t\t},\n\t\tget computeErrors() {\n\t\t\treturn state.computeErrors;\n\t\t},\n\t\tget computeWarnings() {\n\t\t\treturn state.computeWarnings;\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget 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\tconst { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);\n\t\t\tif (shouldSolve || forceSolve) {\n\t\t\t\tif (forceSolve && !shouldSolve) {\n\t\t\t\t\tstate.pendingValues = {};\n\t\t\t\t\tstate.hasPendingChanges = false;\n\t\t\t\t}\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\tsolve() {\n\t\t\tdispatch();\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tObject.assign(state.values, incoming);\n\t\t\tif (currentSchema?.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\tstate.hasPendingChanges = true;\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\trebuild(schema, scopeKey) {\n\t\t\tcurrentSchema = schema;\n\t\t\t// The new definition has its own input space; a matching input key from the prior\n\t\t\t// one must not serve that definition's cached result.\n\t\t\targs.driver.clearCache?.();\n\t\t\tstate.meshes = [];\n\t\t\t// Same reason as clearCache: a retained result belongs to the previous definition.\n\t\t\tstate.lastResult = null;\n\t\t\tstate.error = '';\n\t\t\tstate.computeErrors = [];\n\t\t\tstate.computeWarnings = [];\n\t\t\tstate.pendingValues = {};\n\t\t\tstate.values = buildInitialValues(schema, scopeKey, readExternalValue);\n\t\t\tconst f = makeInitialFlags(schema?.instanceSolve);\n\t\t\tstate.hasPendingChanges = f.hasPendingChanges;\n\t\t\tstate.hasNeverSolved = f.hasNeverSolved;\n\t\t\tif (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\treport(result) {\n\t\t\tapplySolveResult(state, result);\n\t\t\temit();\n\t\t},\n\n\t\treportError(message) {\n\t\t\tstate.error = message;\n\t\t\temit();\n\t\t},\n\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => listeners.delete(listener);\n\t\t},\n\n\t\tnotify: emit\n\t};\n}\n","/**\n * Single-in-flight, latest-wins throttle for any async operation: only one run at a\n * time, a newly triggered value overwrites the single pending slot (no queue), and a\n * stale run can be aborted via `AbortController` or timeout.\n *\n * Framework-free: `isRunning` is a plain getter, not a store. A reactive host must\n * subscribe via `onChange` (see `@selvajs/ui`'s rune adapter) — polling the getter\n * directly reads correct values but won't trigger a re-render.\n */\ninterface AsyncThrottleOptions {\n\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 (`COMPUTE_SOLVE_DEADLINE_MS`) — 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\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\tonChange?.();\n\t}\n\n\tfunction abortCurrent() {\n\t\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\n\t}\n\n\tasync function execute(values: T) {\n\t\tabortCurrent();\n\n\t\tcurrentAbortController = new AbortController();\n\t\tconst { signal } = currentAbortController;\n\t\tconst deadlineId = setTimeout(() => {\n\t\t\t// Cleared in `finally` on every other path, so firing means a genuine\n\t\t\t// timeout — the only signal for it (the abort itself is swallowed below).\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${runDeadlineMs}ms — aborting`);\n\t\t\tcurrentAbortController?.abort();\n\t\t}, runDeadlineMs);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, signal);\n\t\t} catch (err) {\n\t\t\t// AbortError is expected (timeout or cancel). Non-abort errors must be\n\t\t\t// handled inside `run` — re-throwing here would produce an unhandled\n\t\t\t// rejection because `execute` is always called fire-and-forget.\n\t\t\tif (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {\n\t\t\t\tconsole.error('[Solve/throttle] unhandled error in run:', err);\n\t\t\t}\n\t\t} finally {\n\t\t\tclearTimeout(deadlineId);\n\t\t\tsetRunning(false);\n\t\t\tcurrentAbortController = null;\n\n\t\t\tif (pendingValues !== null) {\n\t\t\t\tconst next = pendingValues;\n\t\t\t\tpendingValues = null;\n\t\t\t\texecute(next);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction trigger(values: T) {\n\t\tif (isRunning) {\n\t\t\tif (pendingValues !== null) {\n\t\t\t\t// Latest-wins: the previously-queued values are dropped, not run.\n\t\t\t\t// `debug`, not `info`: a slider scrub fires this per frame, and it is a trace of\n\t\t\t\t// normal operation. Promoting it would spam a host's console on every drag.\n\t\t\t\t// eslint-disable-next-line no-console -- see above\n\t\t\t\tconsole.debug('[Solve/throttle] superseded pending run (latest-wins)');\n\t\t\t}\n\t\t\tpendingValues = values;\n\t\t} else {\n\t\t\texecute(values);\n\t\t}\n\t}\n\n\tfunction cancel() {\n\t\tpendingValues = null;\n\t\tabortCurrent();\n\t}\n\n\treturn {\n\t\ttrigger,\n\t\tget isRunning() {\n\t\t\treturn isRunning;\n\t\t},\n\t\tget hasPending() {\n\t\t\treturn pendingValues !== null;\n\t\t},\n\t\tcancel\n\t};\n}\n","// LRU cache of solve results keyed on a stable serialization of the inputs, so re-solving\n// something already seen this session (e.g. dragging a slider back) returns instantly\n// instead of round-tripping.\n//\n// A viewer typically takes ownership of every mesh it renders, disposing the previous\n// content on the next update. So the memo can't hand out its own stored instances (they'd\n// get disposed under it) or drop entries silently (their GPU buffers would leak): it keeps\n// private copies via an injected clone/release policy, which also keeps `three` out of this\n// package — `TMesh` stays opaque here.\n\nimport type { SolveResult } from '../shared/solve-fn.js';\n\n/**\n * Deterministic string key for a set of input values. Sorts object keys at every level so\n * two logically-equal inputs built in different key order collide.\n */\nexport function stableInputKey(values: Record<string, unknown>): string {\n\treturn serialize(values);\n}\n\nfunction serialize(value: unknown): string {\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(serialize).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(',')}}`;\n}\n\n/**\n * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then\n * stores and serves them by reference, which is safe only because nothing frees them behind\n * its back.\n */\nexport interface MeshPolicy<TMesh> {\n\t/** Called on the way in and on every hit, so the stored entry and a served copy are never the same instance. */\n\tclone(meshes: TMesh[]): TMesh[];\n\t/** Must not free anything shared with live content (e.g. materials another object still uses). */\n\trelease(meshes: TMesh[]): void;\n}\n\nexport interface SolveMemoOptions<TMesh> {\n\t/** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */\n\tmax?: number;\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\nexport interface SolveMemo<TMesh = unknown> {\n\tget(values: Record<string, unknown>): SolveResult<TMesh> | undefined;\n\tset(values: Record<string, unknown>, result: SolveResult<TMesh>): void;\n\tclear(): void;\n}\n\nexport function createSolveMemo<TMesh = unknown>(\n\toptions: SolveMemoOptions<TMesh> = {}\n): SolveMemo<TMesh> {\n\tconst { max = 16, meshPolicy } = options;\n\tconst entries = new Map<string, SolveResult<TMesh>>();\n\n\tfunction release(result: SolveResult<TMesh>): void {\n\t\tif (result.meshes?.length) meshPolicy?.release(result.meshes);\n\t}\n\n\tfunction copy(result: SolveResult<TMesh>): SolveResult<TMesh> {\n\t\tif (!meshPolicy || !result.meshes?.length) return result;\n\t\treturn { ...result, meshes: meshPolicy.clone(result.meshes) };\n\t}\n\n\tfunction evict(key: string): void {\n\t\tconst entry = entries.get(key);\n\t\tif (entry === undefined) return;\n\t\tentries.delete(key);\n\t\trelease(entry);\n\t}\n\n\treturn {\n\t\tget(values) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tconst hit = entries.get(key);\n\t\t\tif (hit === undefined) return undefined;\n\t\t\tentries.delete(key);\n\t\t\tentries.set(key, hit);\n\t\t\tconsole.info(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);\n\t\t\treturn copy(hit);\n\t\t},\n\t\tset(values, result) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tevict(key);\n\t\t\tentries.set(key, copy(result));\n\t\t\twhile (entries.size > max) {\n\t\t\t\tconst oldest = entries.keys().next().value;\n\t\t\t\tif (oldest === undefined) break;\n\t\t\t\tevict(oldest);\n\t\t\t\tconsole.info(`[Solve/memo] evicted LRU entry (cap ${max})`);\n\t\t\t}\n\t\t},\n\t\tclear() {\n\t\t\tif (entries.size > 0) {\n\t\t\t\tconsole.info(`[Solve/memo] cleared ${entries.size} entries (definition changed)`);\n\t\t\t}\n\t\t\tentries.forEach(release);\n\t\t\tentries.clear();\n\t\t}\n\t};\n}\n","import { createAsyncThrottle } from '../async-throttle.js';\nimport { createSolveMemo, type MeshPolicy } from '../solve-memo.js';\nimport type { SolveFn } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\t/** How long one solve may take before the client aborts it (ms). Pass the server's `COMPUTE_SOLVE_DEADLINE_MS` 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}\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 } = options;\n\n\t// Checked inside the throttled run, not before triggering it, so a hit only serves\n\t// after the throttle's latest-wins ordering has picked these values to run.\n\tconst memo = createSolveMemo<TMesh>({ meshPolicy });\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(\n\t\tasync (values, signal) => {\n\t\t\tconst cached = memo.get(values);\n\t\t\tif (cached !== undefined) {\n\t\t\t\tif (signal.aborted) return;\n\t\t\t\tgetReporter().report(cached);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await onSolve(values, signal);\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve completed after abort — result discarded');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Stamped here rather than in the SolveFn: a memo hit never calls it, so values\n\t\t\t\t// captured inside would pair the served result with whatever solved last.\n\t\t\t\tconst stamped = { ...result, values };\n\t\t\t\tmemo.set(values, stamped);\n\t\t\t\tgetReporter().report(stamped);\n\t\t\t} catch (err) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve aborted (superseded, cancelled, or timed out)');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\t\tgetReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t\t}\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\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel() {\n\t\t\tthrottle.cancel();\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tclearCache() {\n\t\t\tmemo.clear();\n\t\t}\n\t};\n}\n","/**\n * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a\n * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +\n * `SolveEngine` produce server-side) and unpacks the result.\n *\n * Ports the resilience behavior an interactive solve endpoint needs regardless\n * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response\n * guarding, and abort handling at every await point — so a new consumer gets it\n * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:\n * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a\n * viewer-enabled consumer supplies its own `meshes.extract` hook (typically\n * `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).\n */\n\nimport { GrasshopperResponseProcessor, type GrasshopperComputeResponse } from '@selvajs/compute';\nimport type { SolveFn, SolveResult } from '../shared/solve-fn.js';\n\nexport interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {\n\t/** The compute endpoint to POST to, e.g. `/api/compute`. */\n\tendpoint: string;\n\t/** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */\n\tdefinitionUrl: () => string;\n\t/** Schema input params, sent as the request body's `inputs`. */\n\tinputs: () => Array<{ id: string }>;\n\t/** Schema output params — drives byId/byName output extraction after a solve. */\n\toutputs: () => Array<{ id: string; nickname?: string }>;\n\tchannel?: () => 'live' | 'draft' | undefined;\n\tversionId?: () => string | null | undefined;\n\t/**\n\t * Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`\n\t * and no rhino3dm module is ever loaded. `loadRhino` is called lazily on the\n\t * first solve that needs meshes and cached for the lifetime of this `SolveFn`.\n\t */\n\tmeshes?: {\n\t\tloadRhino: () => Promise<TRhino>;\n\t\textract: (\n\t\t\tresponse: GrasshopperComputeResponse,\n\t\t\topts: { debug: boolean; rhino: TRhino }\n\t\t) => TMesh[] | Promise<TMesh[]>;\n\t};\n\t/** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */\n\tdebug?: boolean;\n\t/** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */\n\tonRateLimited?: (retryAfterSeconds: number) => void;\n\t/** Called on a 401 or a login-page redirect. Default: throws an actionable \"reload to sign in again\" Error. */\n\tonSessionExpired?: () => void;\n}\n\nfunction parseServerTiming(header: string | null): Record<string, number> {\n\tconst out: Record<string, number> = {};\n\tif (!header) return out;\n\tfor (const part of header.split(',')) {\n\t\tconst [name, ...params] = part.split(';').map((s) => s.trim());\n\t\tconst dur = params.find((p) => p.startsWith('dur='));\n\t\tif (!name || !dur) continue;\n\t\tconst ms = Number(dur.slice(4));\n\t\tif (Number.isFinite(ms)) out[name] = ms;\n\t}\n\treturn out;\n}\n\nconst defaultOnRateLimited = (retryAfterSeconds: number): never => {\n\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);\n};\n\nconst defaultOnSessionExpired = (): never => {\n\tthrow new Error(\n\t\t'Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.'\n\t);\n};\n\nexport function createComputeFetchSolveFn<TMesh = unknown, TRhino = unknown>(\n\topts: ComputeFetchSolveFnOptions<TMesh, TRhino>\n): SolveFn<TMesh, GrasshopperComputeResponse> {\n\tconst debug = opts.debug ?? false;\n\tconst log = (...args: unknown[]) => {\n\t\tif (debug) console.info(...args);\n\t};\n\n\tlet rhinoPromise: Promise<TRhino> | null = null;\n\tfunction getRhino(): Promise<TRhino> {\n\t\tif (!opts.meshes) throw new Error('createComputeFetchSolveFn: meshes option not configured.');\n\t\tif (!rhinoPromise) rhinoPromise = opts.meshes.loadRhino();\n\t\treturn rhinoPromise;\n\t}\n\n\tlet cooldownUntil = 0;\n\n\treturn async (values, signal): Promise<SolveResult<TMesh, GrasshopperComputeResponse>> => {\n\t\tconst remainingMs = cooldownUntil - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(remainingMs / 1000)}s.`);\n\t\t}\n\n\t\tconst solveStart = performance.now();\n\t\tconst inputs = opts.inputs();\n\t\tconst outputs = opts.outputs();\n\t\tconst channel = opts.channel?.();\n\t\tconst versionId = opts.versionId?.();\n\n\t\tconst payload = JSON.stringify({\n\t\t\tinputs,\n\t\t\tvalues,\n\t\t\tdefinitionUrl: opts.definitionUrl(),\n\t\t\t...(versionId ? { versionId } : channel === 'draft' ? { channel: 'draft' } : {})\n\t\t});\n\t\tconst valuesBytes = JSON.stringify(values).length;\n\n\t\tlet res: Response;\n\t\ttry {\n\t\t\tres = await fetch(opts.endpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tbody: payload,\n\t\t\t\tsignal\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\tlog('[Compute] solve aborted during fetch — empty result discarded');\n\t\t\t\treturn { outputs: {} };\n\t\t\t}\n\t\t\tthrow new Error(\n\t\t\t\t'Request blocked — your session may have expired. Reload the page to sign in again.',\n\t\t\t\t{ cause: err }\n\t\t\t);\n\t\t}\n\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted after headers — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\n\t\tconst redirectedToLogin = res.redirected && new URL(res.url).pathname === '/login';\n\t\tif (res.status === 401 || redirectedToLogin) {\n\t\t\t(opts.onSessionExpired ?? defaultOnSessionExpired)();\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tif (res.status === 503) {\n\t\t\t\tthrow new Error('Compute server is offline or unreachable. Please try again later.');\n\t\t\t}\n\t\t\tconst errorBody = await res.text().catch(() => '');\n\t\t\tlet d: { message?: string; retryAfter?: number } = {};\n\t\t\ttry {\n\t\t\t\td = JSON.parse(errorBody);\n\t\t\t} catch {\n\t\t\t\tif (errorBody) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[Compute] non-JSON error body (HTTP ${res.status}):`,\n\t\t\t\t\t\terrorBody.slice(0, 300)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (res.status === 429) {\n\t\t\t\tconst retryAfter = Number(res.headers.get('Retry-After')) || Number(d.retryAfter) || 5;\n\t\t\t\tcooldownUntil = Date.now() + retryAfter * 1000;\n\t\t\t\t(opts.onRateLimited ?? defaultOnRateLimited)(retryAfter);\n\t\t\t}\n\t\t\tthrow new Error(d.message || `Compute error (HTTP ${res.status})`);\n\t\t}\n\n\t\tconst ttfbMs = performance.now() - solveStart;\n\n\t\tconst downloadStart = performance.now();\n\t\tconst bodyText = await res.text();\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted mid-download — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\t\tconst downloadMs = performance.now() - downloadStart;\n\t\tconst bytes = bodyText.length;\n\n\t\tlet solved: GrasshopperComputeResponse;\n\t\ttry {\n\t\t\tsolved = JSON.parse(bodyText);\n\t\t} catch (err) {\n\t\t\tthrow new Error(\n\t\t\t\t'Received an invalid response from the server. Reload the page and try again.',\n\t\t\t\t{\n\t\t\t\t\tcause: err\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\tconst processor = new GrasshopperResponseProcessor(solved, false);\n\n\t\tconst meshes = opts.meshes\n\t\t\t? await opts.meshes.extract(processor.response, { debug, rhino: await getRhino() })\n\t\t\t: [];\n\n\t\tconst resultOutputs: Record<string, unknown> = {};\n\t\tfor (const o of outputs) {\n\t\t\tconst byId = processor.getValue({ byId: o.id }, { parseValues: true });\n\t\t\tresultOutputs[o.id] =\n\t\t\t\tbyId ??\n\t\t\t\t(o.nickname\n\t\t\t\t\t? processor.getValue({ byName: o.nickname }, { parseValues: true })\n\t\t\t\t\t: undefined);\n\t\t}\n\n\t\tif (debug) {\n\t\t\tconst serverTiming = parseServerTiming(res.headers.get('Server-Timing'));\n\t\t\tconst serverTotal = serverTiming.total ?? 0;\n\t\t\tconst networkMs = Math.max(0, ttfbMs - serverTotal);\n\t\t\tconst sizeMB = bytes / (1024 * 1024);\n\t\t\tconst size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bytes / 1024).toFixed(0)} KB`;\n\t\t\tconst mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1000)).toFixed(1) : '∞';\n\t\t\tconst heapMB = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory\n\t\t\t\t?.usedJSHeapSize;\n\t\t\tconst heap = heapMB !== undefined ? ` | heap=${(heapMB / (1024 * 1024)).toFixed(0)} MB` : '';\n\t\t\tlog(\n\t\t\t\t`[Compute] req=${(payload.length / 1024).toFixed(0)}KB (values ${(valuesBytes / 1024).toFixed(0)}KB) | ` +\n\t\t\t\t\t`ttfb=${ttfbMs.toFixed(0)}ms (network≈${networkMs.toFixed(0)} + server ${serverTotal.toFixed(0)}) | ` +\n\t\t\t\t\t`download=${downloadMs.toFixed(0)}ms (${size} @ ${mbps} MB/s)${heap}`\n\t\t\t);\n\t\t\tif (serverTiming.selva_cache !== undefined) {\n\t\t\t\tconst cacheMsg = serverTiming.selva_cache\n\t\t\t\t\t? 'HIT — served without calling compute'\n\t\t\t\t\t: 'miss — solved on Rhino.Compute';\n\t\t\t\tlog(`[Compute] └─ cache: ${cacheMsg}`);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\toutputs: resultOutputs,\n\t\t\tmeshes,\n\t\t\terrors: solved.errors ?? [],\n\t\t\twarnings: solved.warnings ?? [],\n\t\t\tsource: solved\n\t\t};\n\t};\n}\n"],"mappings":"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","names":["getInputItems","STORAGE_PREFIX","makeKey","scopeKey","inputId","writeExternalValue","args","value","readExternalValue","ref","raw","clearExternalValue","getExternalInputs","schema","item","getDefaultValue","buildInitialValues","schema","scopeKey","read","clientSet","getExternalInputs","e","values","input","stored","getDefaultValue","output","makeInitialFlags","instanceSolve","manual","applyValueChange","state","id","value","pickInputValues","picked","applySolveResult","result","_meshes","retained","createSolveSession","args","currentSchema","flags","makeInitialFlags","state","buildInitialValues","readExternalValue","listeners","emit","l","dispatch","pickInputValues","id","value","forceSolve","shouldSolve","applyValueChange","incoming","schema","scopeKey","f","result","applySolveResult","message","listener","createAsyncThrottle","run","options","runDeadlineMs","onChange","isRunning","pendingValues","currentAbortController","setRunning","next","abortCurrent","execute","values","signal","deadlineId","err","trigger","cancel","stableInputKey","values","serialize","value","obj","k","createSolveMemo","options","max","meshPolicy","entries","release","result","copy","evict","key","entry","hit","oldest","createRequestResponseDriver","onSolve","getReporter","options","meshPolicy","solveDeadlineMs","onChange","memo","createSolveMemo","throttle","createAsyncThrottle","values","signal","cached","result","stamped","err","GrasshopperResponseProcessor","parseServerTiming","header","out","part","name","params","s","dur","p","ms","defaultOnRateLimited","retryAfterSeconds","defaultOnSessionExpired","createComputeFetchSolveFn","opts","debug","log","args","rhinoPromise","getRhino","cooldownUntil","values","signal","remainingMs","solveStart","inputs","outputs","channel","versionId","payload","valuesBytes","res","err","redirectedToLogin","errorBody","d","retryAfter","ttfbMs","downloadStart","bodyText","downloadMs","bytes","solved","processor","meshes","resultOutputs","o","byId","serverTiming","serverTotal","networkMs","sizeMB","size","mbps","heapMB","heap","cacheMsg"]}
|
|
1
|
+
{"version":3,"sources":["../src/client/external-storage.ts","../src/client/solve-session-core.ts","../src/client/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts","../src/client/compute-fetch-solve-fn.ts"],"sourcesContent":["// Transit storage for inputs with source.kind === 'client': a producer route writes the\n// value here, the solver route reads it back. Keyed by (scopeKey, inputId) so values\n// don't bleed across solver contexts — scopeKey is whatever uniquely identifies one\n// (sessionId in plugin-ui/preview, definition guid in selva/library).\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nfunction makeKey(scopeKey: string, inputId: string): string {\n\treturn `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;\n}\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst { scopeKey, inputId, value } = args;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return undefined;\n\tif (typeof sessionStorage === 'undefined') return undefined;\n\tconst raw = sessionStorage.getItem(makeKey(scopeKey, inputId));\n\tif (raw === null) return undefined;\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function clearExternalValue(ref: ExternalValueRef): void {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.removeItem(makeKey(scopeKey, inputId));\n}\n\nexport interface ExternalInput {\n\tparamId: string;\n\tdisplayName: string;\n}\n\nexport function getExternalInputs(schema: UISchema): ExternalInput[] {\n\treturn getInputItems(schema)\n\t\t.filter((item) => item.source?.kind === 'client')\n\t\t.map((item) => ({\n\t\t\tparamId: item.paramId,\n\t\t\tdisplayName: item.displayName ?? item.paramId\n\t\t}));\n}\n","// Pure transition logic for a Solve Session.\n//\n// This module holds the value/lifecycle state machine with no transport and no state\n// ownership: how values are seeded (including client-sourced hydration), when a value\n// change should dispatch a solve vs. defer it, and what a reported solve result does to\n// the flags. `solve-session.ts` is a thin shell that owns the state object and the\n// subscriber set; every decision lives here, which is what makes it directly testable.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getDefaultValue } from '@selvajs/schemas';\nimport { getExternalInputs, type ExternalValueRef } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\n\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\tmeshes: unknown[];\n\t/** Values changed since the last solve, in manual (instanceSolve === false) mode. */\n\tpendingValues: Record<string, unknown>;\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n\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}\n\nexport type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;\n\n/**\n * Client-sourced inputs are hydrated from `read` and left absent when no stored value\n * exists, so the missing-inputs panel can detect them. Other inputs fall back to their\n * declared default, then the paramType default.\n */\nexport function buildInitialValues(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): Record<string, unknown> {\n\tconst clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));\n\tconst values: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (clientSet.has(input.id)) {\n\t\t\tconst stored = read({ scopeKey, inputId: input.id });\n\t\t\tif (stored !== undefined) values[input.id] = stored;\n\t\t\tcontinue;\n\t\t}\n\t\tvalues[input.id] = input.default ?? getDefaultValue(input.paramType);\n\t}\n\tfor (const output of schema.outputs) {\n\t\tvalues[output.id] = null;\n\t}\n\treturn values;\n}\n\n/** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */\nexport function makeInitialFlags(instanceSolve: boolean | undefined): {\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n} {\n\tconst manual = instanceSolve === false;\n\treturn { hasPendingChanges: manual, hasNeverSolved: manual };\n}\n\n/** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined\n): { state: SolveSessionState; shouldSolve: boolean } {\n\tstate.values[id] = value;\n\tif (instanceSolve === false) {\n\t\tstate.pendingValues[id] = value;\n\t\tstate.hasPendingChanges = true;\n\t\treturn { state, shouldSolve: false };\n\t}\n\treturn { state, shouldSolve: true };\n}\n\n/**\n * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value\n * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB\n * options-list output showed this can re-upload a multi-MB payload no backend reads.\n */\nexport function pickInputValues(\n\tschema: UISchema | undefined,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tif (!schema?.inputs) return values;\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (input.id in values) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\nexport function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.meshes = result.meshes ?? [];\n\tconst { meshes: _meshes, ...retained } = result;\n\tstate.lastResult = retained;\n\tObject.assign(state.values, result.outputs);\n\tstate.pendingValues = {};\n\tstate.hasPendingChanges = false;\n\tstate.hasNeverSolved = false;\n\treturn state;\n}\n","// A framework-free shell over the pure transition logic in solve-session-core.ts: owns the\n// live values/flags, delegates every transition to the core, and drives solves through a\n// transport-agnostic SolveDriver.\n//\n// State is exposed as plain getters; it carries no framework reactivity of its own, so a\n// Svelte host must wrap it — see `useSolveSession.svelte.ts` in `@selvajs/ui`, which\n// republishes these getters as $state. Reading a getter without subscribing gives a\n// correct value but will not re-render.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { readExternalValue } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\nimport type { SolveDriver } from './drivers/driver.js';\nimport {\n\tbuildInitialValues,\n\tmakeInitialFlags,\n\tapplyValueChange,\n\tapplySolveResult,\n\tpickInputValues,\n\ttype SolveSessionState,\n\ttype RetainedSolveResult\n} from './solve-session-core.js';\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\treadonly meshes: unknown[];\n\t/**\n\t * The last reported result without its meshes — what the viewer is currently showing,\n\t * including the `source`/`values` pair a commit path needs. Null before the first solve,\n\t * and again after `rebuild()`. Survives `reportError`: a failed solve leaves the last good\n\t * geometry on screen, so a host committing what it sees 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\tsolve(): void;\n\t/** Merges incoming values, then solves (auto) or marks dirty (manual). */\n\tloadValues(incoming: Record<string, unknown>): void;\n\trebuild(schema: UISchema, scopeKey: string): void;\n\treport(result: SolveResult): void;\n\treportError(message: string): void;\n\t/** Fires after any mutation of the getters above, including driver-reported `isSolving` transitions. */\n\tsubscribe(listener: () => void): () => void;\n\t/** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */\n\tnotify(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\n\tconst flags = makeInitialFlags(currentSchema?.instanceSolve);\n\tconst state: SolveSessionState = {\n\t\tvalues: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tmeshes: [],\n\t\tpendingValues: {},\n\t\thasPendingChanges: flags.hasPendingChanges,\n\t\thasNeverSolved: flags.hasNeverSolved,\n\t\tlastResult: null\n\t};\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => listeners.forEach((l) => l());\n\n\tfunction dispatch() {\n\t\t// Copy: state.values is the live map, and a driver may hold the payload across an\n\t\t// async solve — a later setValue must not mutate what's already in flight.\n\t\targs.driver.solve({ ...pickInputValues(currentSchema, state.values) });\n\t\temit();\n\t}\n\n\treturn {\n\t\tget values() {\n\t\t\treturn state.values;\n\t\t},\n\t\tget error() {\n\t\t\treturn state.error;\n\t\t},\n\t\tget computeErrors() {\n\t\t\treturn state.computeErrors;\n\t\t},\n\t\tget computeWarnings() {\n\t\t\treturn state.computeWarnings;\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget 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\tconst { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);\n\t\t\tif (shouldSolve || forceSolve) {\n\t\t\t\tif (forceSolve && !shouldSolve) {\n\t\t\t\t\tstate.pendingValues = {};\n\t\t\t\t\tstate.hasPendingChanges = false;\n\t\t\t\t}\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\tsolve() {\n\t\t\tdispatch();\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tObject.assign(state.values, incoming);\n\t\t\tif (currentSchema?.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\tstate.hasPendingChanges = true;\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\trebuild(schema, scopeKey) {\n\t\t\tcurrentSchema = schema;\n\t\t\t// The new definition has its own input space; a matching input key from the prior\n\t\t\t// one must not serve that definition's cached result.\n\t\t\targs.driver.clearCache?.();\n\t\t\tstate.meshes = [];\n\t\t\t// Same reason as clearCache: a retained result belongs to the previous definition.\n\t\t\tstate.lastResult = null;\n\t\t\tstate.error = '';\n\t\t\tstate.computeErrors = [];\n\t\t\tstate.computeWarnings = [];\n\t\t\tstate.pendingValues = {};\n\t\t\tstate.values = buildInitialValues(schema, scopeKey, readExternalValue);\n\t\t\tconst f = makeInitialFlags(schema?.instanceSolve);\n\t\t\tstate.hasPendingChanges = f.hasPendingChanges;\n\t\t\tstate.hasNeverSolved = f.hasNeverSolved;\n\t\t\tif (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\treport(result) {\n\t\t\tapplySolveResult(state, result);\n\t\t\temit();\n\t\t},\n\n\t\treportError(message) {\n\t\t\tstate.error = message;\n\t\t\temit();\n\t\t},\n\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => listeners.delete(listener);\n\t\t},\n\n\t\tnotify: emit\n\t};\n}\n","/**\n * Single-in-flight, latest-wins throttle for any async operation: only one run at a\n * time, a newly triggered value overwrites the single pending slot (no queue), and a\n * stale run can be aborted via `AbortController` or timeout.\n *\n * Framework-free: `isRunning` is a plain getter, not a store. A reactive host must\n * subscribe via `onChange` (see `@selvajs/ui`'s rune adapter) — polling the getter\n * directly reads correct values but won't trigger a re-render.\n */\ninterface AsyncThrottleOptions {\n\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 (`COMPUTE_SOLVE_DEADLINE_MS`) — 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\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\tonChange?.();\n\t}\n\n\tfunction abortCurrent() {\n\t\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\n\t}\n\n\tasync function execute(values: T) {\n\t\tabortCurrent();\n\n\t\tcurrentAbortController = new AbortController();\n\t\tconst { signal } = currentAbortController;\n\t\tconst deadlineId = setTimeout(() => {\n\t\t\t// Cleared in `finally` on every other path, so firing means a genuine\n\t\t\t// timeout — the only signal for it (the abort itself is swallowed below).\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${runDeadlineMs}ms — aborting`);\n\t\t\tcurrentAbortController?.abort();\n\t\t}, runDeadlineMs);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, signal);\n\t\t} catch (err) {\n\t\t\t// AbortError is expected (timeout or cancel). Non-abort errors must be\n\t\t\t// handled inside `run` — re-throwing here would produce an unhandled\n\t\t\t// rejection because `execute` is always called fire-and-forget.\n\t\t\tif (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {\n\t\t\t\tconsole.error('[Solve/throttle] unhandled error in run:', err);\n\t\t\t}\n\t\t} finally {\n\t\t\tclearTimeout(deadlineId);\n\t\t\tsetRunning(false);\n\t\t\tcurrentAbortController = null;\n\n\t\t\tif (pendingValues !== null) {\n\t\t\t\tconst next = pendingValues;\n\t\t\t\tpendingValues = null;\n\t\t\t\texecute(next);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction trigger(values: T) {\n\t\tif (isRunning) {\n\t\t\tif (pendingValues !== null) {\n\t\t\t\t// Latest-wins: the previously-queued values are dropped, not run.\n\t\t\t\t// `debug`, not `info`: a slider scrub fires this per frame, and it is a trace of\n\t\t\t\t// normal operation. Promoting it would spam a host's console on every drag.\n\t\t\t\t// eslint-disable-next-line no-console -- see above\n\t\t\t\tconsole.debug('[Solve/throttle] superseded pending run (latest-wins)');\n\t\t\t}\n\t\t\tpendingValues = values;\n\t\t} else {\n\t\t\texecute(values);\n\t\t}\n\t}\n\n\tfunction cancel() {\n\t\tpendingValues = null;\n\t\tabortCurrent();\n\t}\n\n\treturn {\n\t\ttrigger,\n\t\tget isRunning() {\n\t\t\treturn isRunning;\n\t\t},\n\t\tget hasPending() {\n\t\t\treturn pendingValues !== null;\n\t\t},\n\t\tcancel\n\t};\n}\n","// LRU cache of solve results keyed on a stable serialization of the inputs, so re-solving\n// something already seen this session (e.g. dragging a slider back) returns instantly\n// instead of round-tripping.\n//\n// A viewer typically takes ownership of every mesh it renders, disposing the previous\n// content on the next update. So the memo can't hand out its own stored instances (they'd\n// get disposed under it) or drop entries silently (their GPU buffers would leak): it keeps\n// private copies via an injected clone/release policy, which also keeps `three` out of this\n// package — `TMesh` stays opaque here.\n\nimport type { SolveResult } from '../shared/solve-fn.js';\n\n/**\n * Deterministic string key for a set of input values. Sorts object keys at every level so\n * two logically-equal inputs built in different key order collide.\n */\nexport function stableInputKey(values: Record<string, unknown>): string {\n\treturn serialize(values);\n}\n\nfunction serialize(value: unknown): string {\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(serialize).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(',')}}`;\n}\n\n/**\n * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then\n * stores and serves them by reference, which is safe only because nothing frees them behind\n * its back.\n */\nexport interface MeshPolicy<TMesh> {\n\t/** Called on the way in and on every hit, so the stored entry and a served copy are never the same instance. */\n\tclone(meshes: TMesh[]): TMesh[];\n\t/** Must not free anything shared with live content (e.g. materials another object still uses). */\n\trelease(meshes: TMesh[]): void;\n}\n\nexport interface SolveMemoOptions<TMesh> {\n\t/** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */\n\tmax?: number;\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\nexport interface SolveMemo<TMesh = unknown> {\n\tget(values: Record<string, unknown>): SolveResult<TMesh> | undefined;\n\tset(values: Record<string, unknown>, result: SolveResult<TMesh>): void;\n\tclear(): void;\n}\n\nexport function createSolveMemo<TMesh = unknown>(\n\toptions: SolveMemoOptions<TMesh> = {}\n): SolveMemo<TMesh> {\n\tconst { max = 16, meshPolicy } = options;\n\tconst entries = new Map<string, SolveResult<TMesh>>();\n\n\tfunction release(result: SolveResult<TMesh>): void {\n\t\tif (result.meshes?.length) meshPolicy?.release(result.meshes);\n\t}\n\n\tfunction copy(result: SolveResult<TMesh>): SolveResult<TMesh> {\n\t\tif (!meshPolicy || !result.meshes?.length) return result;\n\t\treturn { ...result, meshes: meshPolicy.clone(result.meshes) };\n\t}\n\n\tfunction evict(key: string): void {\n\t\tconst entry = entries.get(key);\n\t\tif (entry === undefined) return;\n\t\tentries.delete(key);\n\t\trelease(entry);\n\t}\n\n\treturn {\n\t\tget(values) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tconst hit = entries.get(key);\n\t\t\tif (hit === undefined) return undefined;\n\t\t\tentries.delete(key);\n\t\t\tentries.set(key, hit);\n\t\t\tconsole.info(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);\n\t\t\treturn copy(hit);\n\t\t},\n\t\tset(values, result) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tevict(key);\n\t\t\tentries.set(key, copy(result));\n\t\t\twhile (entries.size > max) {\n\t\t\t\tconst oldest = entries.keys().next().value;\n\t\t\t\tif (oldest === undefined) break;\n\t\t\t\tevict(oldest);\n\t\t\t\tconsole.info(`[Solve/memo] evicted LRU entry (cap ${max})`);\n\t\t\t}\n\t\t},\n\t\tclear() {\n\t\t\tif (entries.size > 0) {\n\t\t\t\tconsole.info(`[Solve/memo] cleared ${entries.size} entries (definition changed)`);\n\t\t\t}\n\t\t\tentries.forEach(release);\n\t\t\tentries.clear();\n\t\t}\n\t};\n}\n","import { createAsyncThrottle } from '../async-throttle.js';\nimport { createSolveMemo, type MeshPolicy } from '../solve-memo.js';\nimport type { SolveFn } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\t/** How long one solve may take before the client aborts it (ms). Pass the server's `COMPUTE_SOLVE_DEADLINE_MS` 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}\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 } = options;\n\n\t// Checked inside the throttled run, not before triggering it, so a hit only serves\n\t// after the throttle's latest-wins ordering has picked these values to run.\n\tconst memo = createSolveMemo<TMesh>({ meshPolicy });\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(\n\t\tasync (values, signal) => {\n\t\t\tconst cached = memo.get(values);\n\t\t\tif (cached !== undefined) {\n\t\t\t\tif (signal.aborted) return;\n\t\t\t\tgetReporter().report(cached);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await onSolve(values, signal);\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve completed after abort — result discarded');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Stamped here rather than in the SolveFn: a memo hit never calls it, so values\n\t\t\t\t// captured inside would pair the served result with whatever solved last.\n\t\t\t\tconst stamped = { ...result, values };\n\t\t\t\tmemo.set(values, stamped);\n\t\t\t\tgetReporter().report(stamped);\n\t\t\t} catch (err) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve aborted (superseded, cancelled, or timed out)');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\t\tgetReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t\t}\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\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel() {\n\t\t\tthrottle.cancel();\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tclearCache() {\n\t\t\tmemo.clear();\n\t\t}\n\t};\n}\n","/**\n * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a\n * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +\n * `SolveEngine` produce server-side) and unpacks the result.\n *\n * Ports the resilience behavior an interactive solve endpoint needs regardless\n * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response\n * guarding, and abort handling at every await point — so a new consumer gets it\n * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:\n * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a\n * viewer-enabled consumer supplies its own `meshes.extract` hook (typically\n * `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).\n */\n\nimport {\n\tGrasshopperResponseProcessor,\n\ttype GrasshopperComputeResponse\n} from '@selvajs/compute/grasshopper';\nimport type { SolveFn, SolveResult } from '../shared/solve-fn.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`. */\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/** 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 }\n\t\t) => TMesh[] | Promise<TMesh[]>;\n\t};\n\t/** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */\n\tdebug?: boolean;\n\t/** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */\n\tonRateLimited?: (retryAfterSeconds: number) => void;\n\t/** Called on a 401 or a login-page redirect. Default: throws an actionable \"reload to sign in again\" Error. */\n\tonSessionExpired?: () => void;\n}\n\nfunction parseServerTiming(header: string | null): Record<string, number> {\n\tconst out: Record<string, number> = {};\n\tif (!header) return out;\n\tfor (const part of header.split(',')) {\n\t\tconst [name, ...params] = part.split(';').map((s) => s.trim());\n\t\tconst dur = params.find((p) => p.startsWith('dur='));\n\t\tif (!name || !dur) continue;\n\t\tconst ms = Number(dur.slice(4));\n\t\tif (Number.isFinite(ms)) out[name] = ms;\n\t}\n\treturn out;\n}\n\nconst defaultOnRateLimited = (retryAfterSeconds: number): never => {\n\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);\n};\n\nconst defaultOnSessionExpired = (): never => {\n\tthrow new Error(\n\t\t'Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.'\n\t);\n};\n\nexport function createComputeFetchSolveFn<TMesh = unknown>(\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\treturn async (values, signal): Promise<SolveResult<TMesh, GrasshopperComputeResponse>> => {\n\t\tconst remainingMs = cooldownUntil - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(remainingMs / 1000)}s.`);\n\t\t}\n\n\t\tconst solveStart = performance.now();\n\t\tconst inputs = opts.inputs();\n\t\tconst outputs = opts.outputs();\n\t\tconst channel = opts.channel?.();\n\t\tconst versionId = opts.versionId?.();\n\n\t\tconst payload = JSON.stringify({\n\t\t\tinputs,\n\t\t\tvalues,\n\t\t\tdefinitionUrl: opts.definitionUrl(),\n\t\t\t...(versionId ? { versionId } : channel === 'draft' ? { channel: 'draft' } : {})\n\t\t});\n\t\tconst valuesBytes = JSON.stringify(values).length;\n\n\t\tlet res: Response;\n\t\ttry {\n\t\t\tres = await fetch(opts.endpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tbody: payload,\n\t\t\t\tsignal\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\tlog('[Compute] solve aborted during fetch — empty result discarded');\n\t\t\t\treturn { outputs: {} };\n\t\t\t}\n\t\t\tthrow new Error(\n\t\t\t\t'Request blocked — your session may have expired. Reload the page to sign in again.',\n\t\t\t\t{ cause: err }\n\t\t\t);\n\t\t}\n\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted after headers — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\n\t\tconst redirectedToLogin = res.redirected && new URL(res.url).pathname === '/login';\n\t\tif (res.status === 401 || redirectedToLogin) {\n\t\t\t(opts.onSessionExpired ?? defaultOnSessionExpired)();\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tif (res.status === 503) {\n\t\t\t\tthrow new Error('Compute server is offline or unreachable. Please try again later.');\n\t\t\t}\n\t\t\tconst errorBody = await res.text().catch(() => '');\n\t\t\tlet d: { message?: string; retryAfter?: number } = {};\n\t\t\ttry {\n\t\t\t\td = JSON.parse(errorBody);\n\t\t\t} catch {\n\t\t\t\tif (errorBody) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[Compute] non-JSON error body (HTTP ${res.status}):`,\n\t\t\t\t\t\terrorBody.slice(0, 300)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (res.status === 429) {\n\t\t\t\tconst retryAfter = Number(res.headers.get('Retry-After')) || Number(d.retryAfter) || 5;\n\t\t\t\tcooldownUntil = Date.now() + retryAfter * 1000;\n\t\t\t\t(opts.onRateLimited ?? defaultOnRateLimited)(retryAfter);\n\t\t\t}\n\t\t\tthrow new Error(d.message || `Compute error (HTTP ${res.status})`);\n\t\t}\n\n\t\tconst ttfbMs = performance.now() - solveStart;\n\n\t\tconst downloadStart = performance.now();\n\t\tconst bodyText = await res.text();\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted mid-download — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\t\tconst downloadMs = performance.now() - downloadStart;\n\t\tconst bytes = bodyText.length;\n\n\t\tlet solved: GrasshopperComputeResponse;\n\t\ttry {\n\t\t\tsolved = JSON.parse(bodyText);\n\t\t} catch (err) {\n\t\t\tthrow new Error(\n\t\t\t\t'Received an invalid response from the server. Reload the page and try again.',\n\t\t\t\t{\n\t\t\t\t\tcause: err\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\tconst processor = new GrasshopperResponseProcessor(solved, false);\n\n\t\tconst meshes = opts.meshes ? await opts.meshes.extract(processor.response, { debug }) : [];\n\n\t\tconst resultOutputs: Record<string, unknown> = {};\n\t\tfor (const o of outputs) {\n\t\t\tconst byId = processor.getValue({ byId: o.id }, { parseValues: true });\n\t\t\tresultOutputs[o.id] =\n\t\t\t\tbyId ??\n\t\t\t\t(o.nickname\n\t\t\t\t\t? processor.getValue({ byName: o.nickname }, { parseValues: true })\n\t\t\t\t\t: undefined);\n\t\t}\n\n\t\tif (debug) {\n\t\t\tconst serverTiming = parseServerTiming(res.headers.get('Server-Timing'));\n\t\t\tconst serverTotal = serverTiming.total ?? 0;\n\t\t\tconst networkMs = Math.max(0, ttfbMs - serverTotal);\n\t\t\tconst sizeMB = bytes / (1024 * 1024);\n\t\t\tconst size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bytes / 1024).toFixed(0)} KB`;\n\t\t\tconst mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1000)).toFixed(1) : '∞';\n\t\t\tconst heapMB = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory\n\t\t\t\t?.usedJSHeapSize;\n\t\t\tconst heap = heapMB !== undefined ? ` | heap=${(heapMB / (1024 * 1024)).toFixed(0)} MB` : '';\n\t\t\tlog(\n\t\t\t\t`[Compute] req=${(payload.length / 1024).toFixed(0)}KB (values ${(valuesBytes / 1024).toFixed(0)}KB) | ` +\n\t\t\t\t\t`ttfb=${ttfbMs.toFixed(0)}ms (network≈${networkMs.toFixed(0)} + server ${serverTotal.toFixed(0)}) | ` +\n\t\t\t\t\t`download=${downloadMs.toFixed(0)}ms (${size} @ ${mbps} MB/s)${heap}`\n\t\t\t);\n\t\t\tif (serverTiming.selva_cache !== undefined) {\n\t\t\t\tconst cacheMsg = serverTiming.selva_cache\n\t\t\t\t\t? 'HIT — served without calling compute'\n\t\t\t\t\t: 'miss — solved on Rhino.Compute';\n\t\t\t\tlog(`[Compute] └─ cache: ${cacheMsg}`);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\toutputs: resultOutputs,\n\t\t\tmeshes,\n\t\t\terrors: solved.errors ?? [],\n\t\t\twarnings: solved.warnings ?? [],\n\t\t\tsource: solved\n\t\t};\n\t};\n}\n"],"mappings":"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","names":["getInputItems","STORAGE_PREFIX","makeKey","scopeKey","inputId","writeExternalValue","args","value","readExternalValue","ref","raw","clearExternalValue","getExternalInputs","schema","item","getDefaultValue","buildInitialValues","schema","scopeKey","read","clientSet","getExternalInputs","e","values","input","stored","getDefaultValue","output","makeInitialFlags","instanceSolve","manual","applyValueChange","state","id","value","pickInputValues","picked","applySolveResult","result","_meshes","retained","createSolveSession","args","currentSchema","flags","makeInitialFlags","state","buildInitialValues","readExternalValue","listeners","emit","l","dispatch","pickInputValues","id","value","forceSolve","shouldSolve","applyValueChange","incoming","schema","scopeKey","f","result","applySolveResult","message","listener","createAsyncThrottle","run","options","runDeadlineMs","onChange","isRunning","pendingValues","currentAbortController","setRunning","next","abortCurrent","execute","values","signal","deadlineId","err","trigger","cancel","stableInputKey","values","serialize","value","obj","k","createSolveMemo","options","max","meshPolicy","entries","release","result","copy","evict","key","entry","hit","oldest","createRequestResponseDriver","onSolve","getReporter","options","meshPolicy","solveDeadlineMs","onChange","memo","createSolveMemo","throttle","createAsyncThrottle","values","signal","cached","result","stamped","err","GrasshopperResponseProcessor","parseServerTiming","header","out","part","name","params","s","dur","p","ms","defaultOnRateLimited","retryAfterSeconds","defaultOnSessionExpired","createComputeFetchSolveFn","opts","debug","log","args","cooldownUntil","values","signal","remainingMs","solveStart","inputs","outputs","channel","versionId","payload","valuesBytes","res","err","redirectedToLogin","errorBody","d","retryAfter","ttfbMs","downloadStart","bodyText","downloadMs","bytes","solved","processor","meshes","resultOutputs","o","byId","serverTiming","serverTotal","networkMs","sizeMB","size","mbps","heapMB","heap","cacheMsg"]}
|
package/dist/server.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _compute = require('@selvajs/compute');var _zlib = require('zlib');var _util = require('util');function A(e,n){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:_nullishCoalesce(e.minimum, () => (null)),maximum:_nullishCoalesce(e.maximum, () => (null)),atLeast:1,atMost:1,stepSize:e.stepSize,default:_nullishCoalesce(n, () => (e.default))},r=_compute.processInput.call(void 0, t),o=_nullishCoalesce(n, () => (e.default));return r.default==null&&o!=null&&(Array.isArray(o)||r.paramType==="ValueList"&&typeof o!="object")&&(r.default=o),r}var j=_util.promisify.call(void 0, _zlib.gzip),F= exports.COMPUTE_CONTRACT_VERSION =1,_= exports.COMPUTE_VERSION_HEADER ="X-Selva-Compute-Version";function E(e,n){return _compute.TreeBuilder.fromInputParams(e.filter(t=>t.paramType).map(t=>A(t,n[t.id])))}async function B(e){let{client:n,signal:t}=e,r=performance.now(),o=_nullishCoalesce(e.inputTree, () => (E(e.inputs,e.values))),c=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,l=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let R=performance.now()-l;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:R}:{kind:"timeout",durationMs:R,message:`Solve exceeded the ${Math.round(e.solveDeadlineMs/1e3)}s deadline.`};if(v instanceof _compute.RhinoComputeError){if(v.code===_compute.ErrorCodes.QUEUE_FULL)return{kind:"shed",durationMs:R,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===_compute.ErrorCodes.QUEUE_TIMEOUT)return{kind:"shed",durationMs:R,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:R,error:v}}let d=performance.now()-l,h=performance.now(),a;try{a=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(a.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,M=/\bgzip\b/i.test(e.acceptEncoding)&&a.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(a)),S=performance.now()-v}let y=performance.now()-e.loadStartMs,u=n.solveMeta.seq===s+1?n.solveMeta.last:null,f=u&&!u.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,g={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,rhino:f,settle:u,byteRefOutcome:e.byteRefOutcome,prepMarks:_nullishCoalesce(e.prepMarks, () => ([]))}),[_]:String(F),Vary:"Accept-Encoding"},x={treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,serializedBytes:a.length,compressedBytes:_nullishCoalesce(_optionalChain([C, 'optionalAccess', _2 => _2.byteLength]), () => (null))},D;return C?(g["Content-Encoding"]="gzip",g["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:g,result:i,metrics:x}):(g["Content-Length"]=String(Buffer.byteLength(a)),D={body:a,headers:g,result:i,metrics:x}),{kind:"ok",envelope:D,solveMs:d,errorCount:_nullishCoalesce(_optionalChain([i, 'access', _3 => _3.errors, 'optionalAccess', _4 => _4.length]), () => (0)),warningCount:_nullishCoalesce(_optionalChain([i, 'access', _5 => _5.warnings, 'optionalAccess', _6 => _6.length]), () => (0))}}function T(e,n){let t=/\bgzip\b/i.test(n);if(e.encoding==="gzip"&&!t){let r=_zlib.gunzipSync.call(void 0, e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(r)),{body:r,headers:o}}return{body:e.body,headers:e.headers}}function V(e){let n=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let c=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-c);n+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${s.toFixed(1)}`}for(let[c,s]of e.prepMarks)n+=`, p_${c};dur=${s.toFixed(1)}`;let r=e.settle;r&&(n+=`, selva_cache;dur=${r.fromCache?1:0}`,r.definitionReuploaded!==void 0&&(n+=`, def_reupload;dur=${r.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let c=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${c}`}return n}function H(e){return e.id}var J=16,z=1,X=300*1e3;function k(e){let n=_nullishCoalesce(e.maxWarmComputeServers, () => (J)),t=new Map,r=new Map;e.debug==="verbose"&&_compute.enableDebugLogging.call(void 0, );let o=s=>{e.debug&&_optionalChain([e, 'access', _7 => _7.onDebugLog, 'optionalCall', _8 => _8(s)])};async function c(s,m){let i={last:null,seq:0},l={last:null,seq:0},d=e.debug==="verbose",h={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:d,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:y=>{let u=_nullishCoalesce(y.decode, () => (0)),f=_nullishCoalesce(y.solve, () => (0)),b=_nullishCoalesce(y.encode, () => (0));if(i.seq+=1,i.last={decode:u,solve:f,encode:b},!e.debug)return;let g=u<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",x=e.cachesolve&&f<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${u}ms solve=${f}ms encode=${b}ms | definition-cache: ${g} | solve-cache (cachesolve): ${x}`)}};m&&(h.headers={"X-Selva-Definition":m});let a=await _compute.GrasshopperClient.create(h),p=z,w=0,M=!1;async function C(y){if(!M){M=!0;try{let u=await Promise.resolve(_optionalChain([a, 'access', _9 => _9.serverStats, 'optionalAccess', _10 => _10.getActiveChildren, 'call', _11 => _11({initialize:!1})])).catch(()=>null);w=Date.now();let f=!u||u<=0?z:u;if(f===p)return;o(`[Compute/client-cache] concurrency ${p} \u2192 ${f} (server child count)`),p=f,_optionalChain([y, 'optionalAccess', _12 => _12.setMaxConcurrent, 'call', _13 => _13(f)])}finally{M=!1}}}await C();let S=a.createScheduler({mode:"queue",maxConcurrent:p,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.solveDeadlineMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(y,u)=>{if(l.seq+=1,u.status==="success"&&(u.fromCache!==!0&&Date.now()-w>=X&&C(S),l.last={fromCache:u.fromCache===!0,definitionReuploaded:u.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${u.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(u.durationMs)}ms)`),u.fromCache||o(u.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":u.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!u.fromCache))){let f=_optionalChain([u, 'access', _14 => _14.response, 'optionalAccess', _15 => _15.errors]),b=Array.isArray(f)?f.length:0;if(b>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${g}. First: ${JSON.stringify(f[0])}`)}}}});return{client:a,scheduler:S,rhinoTiming:i,solveMeta:l}}return{async getClient(s,m){let i=s.id,l=t.get(i);if(l)return t.delete(i),t.set(i,l),l;let d=r.get(i);if(d)return d;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let h=(async()=>{let a=await c(s,_optionalChain([m, 'optionalAccess', _16 => _16.definitionGuid]));if(t.size>=n){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${n})`),_optionalChain([t, 'access', _17 => _17.get, 'call', _18 => _18(p), 'optionalAccess', _19 => _19.scheduler, 'access', _20 => _20.dispose, 'call', _21 => _21()]),t.delete(p))}return t.set(i,a),a})().finally(()=>r.delete(i));return r.set(i,h),h},evict(s){let m=s,i=t.get(m);i&&(o(`[Compute/client-cache] evicted warm client for server ${m} (config change)`),i.scheduler.dispose(),t.delete(m))},solveCacheStats(){let s={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let m of t.values()){let i=m.scheduler.cacheStats();s.entries+=i.entries,s.bytes+=i.bytes,s.hits+=i.hits,s.misses+=i.misses,s.evictions+=i.evictions}return s},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.dispose();t.clear()}}}function P(e){let n=Math.max(0,Math.floor(e)),t=new Map,r=0,o={hits:0,misses:0,evictions:0};function c(i,l){t.delete(i),t.set(i,l)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,l=t.get(i);t.delete(i),r-=l.bytes.byteLength,o.evictions+=1}}function m(i,l){if(n===0||l.byteLength>n)return;let d=t.get(i);d&&(r-=d.bytes.byteLength),t.set(i,{bytes:l}),r+=l.byteLength,s()}return{getOrLoad(i,l){let d={loaded:!1,fromCache:!1};return{key:i,outcome:d,load:async()=>{let h=t.get(i);if(h)return c(i,h),o.hits+=1,d.loaded=!0,d.fromCache=!0,h.bytes;o.misses+=1,d.loaded=!0,d.fromCache=!1;let a=await l();return m(i,a),a}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:r}},clear(){t.clear(),r=0}}}function O(e={}){let n=new Map;return{run(t,r,o){let c=n.get(t);if(c)return _optionalChain([e, 'access', _22 => _22.onJoin, 'optionalCall', _23 => _23(t)]),_optionalChain([c, 'access', _24 => _24.notifyOwner, 'optionalCall', _25 => _25()]),c.promise;let s={notifyOwner:o,promise:void 0},m=(async()=>r())().finally(()=>{s.notifyOwner=void 0,n.get(t)===s&&n.delete(t)});return s.promise=m,n.set(t,s),m},inFlight(){return n.size}}}var _platform = require('@selvajs/platform');function te(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&"outcome"in e}function ne(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&!("outcome"in e)&&!("key"in e)&&"versionId"in e}var L=class{constructor(n){this.limits=n.limits;let t=_nullishCoalesce(n.logger, () => (new _platform.NoopLogger));this.clientCache=k({solveDeadlineMs:this.limits.solveDeadlineMs,cachesolve:this.limits.computeServerCachesolve,cacheerroredsolves:this.limits.computeCacheErroredSolves,reuseServerDefinitionCache:this.limits.computeReuseDefinitionCache,maxQueueDepth:this.limits.computeMaxQueueDepth,queueWaitMs:this.limits.computeQueueWaitMs,responseCacheMaxBytes:this.limits.computeSolveCacheBytes,debug:_nullishCoalesce(n.debug, () => (!1)),maxWarmComputeServers:n.maxWarmComputeServers,onDebugLog:_nullishCoalesce(n.onDebugLog, () => ((r=>t.debug(r,{component:"Compute/client-cache"}))))}),this.byteCache=P(this.limits.computeDefinitionCacheBytes),this.singleFlight=O({onJoin:n.onSolveCoalesced})}getClient(n,t){return this.clientCache.getClient(n,t)}definitionRef(n,t){return this.byteCache.getOrLoad(n,t)}evictServer(n){this.clientCache.evict(n)}clearSolveCaches(){this.clientCache.clearSolveCaches()}stats(){return{client:this.clientCache.solveCacheStats(),definitionBytes:this.byteCache.stats(),coalescing:{inFlight:this.singleFlight.inFlight()}}}async solve(n){let{definitionSource:t,byteRefOutcome:r,identity:o}=this.resolveDefinitionSource(n),c=await this.clientCache.getClient(n.server,{definitionGuid:n.definitionGuid}),s=E(n.inputs,n.values),m=`${o}:${n.server.id}:${_compute.stableStringify.call(void 0, s)}`,i=new AbortController,l=!1,d=()=>{l||i.abort()};n.signal.addEventListener("abort",d,{once:!0});let h=_nullishCoalesce(n.acceptEncoding, () => ("")),a;try{a=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:c,responseMaxBytes:this.limits.computeResponseMaxBytes,solveDeadlineMs:this.limits.solveDeadlineMs,acceptEncoding:h,signal:i.signal,loadStartMs:_nullishCoalesce(n.loadStartMs, () => (performance.now())),defLoadMs:_nullishCoalesce(n.defLoadMs, () => (0)),prepMarks:n.prepMarks}),()=>{l=!0})}finally{n.signal.removeEventListener("abort",d)}if(a.kind!=="ok")return a;let p=T(a.envelope,h);return{...a,envelope:{...a.envelope,body:p.body,headers:p.headers}}}toResponse(n,t){let r=(o,c)=>{_optionalChain([t, 'optionalAccess', _26 => _26.onError])&&t.onError(o,c);let s={"Content-Type":"application/json"};return c.retryAfter!==void 0&&(s["Retry-After"]=String(c.retryAfter)),{status:o,headers:s,body:JSON.stringify(c)}};switch(n.kind){case"ok":return{status:200,headers:n.envelope.headers,body:n.envelope.body};case"timeout":return r(504,{message:n.message});case"client_abort":return r(499,{message:"Client closed request"});case"too_large":return r(413,{message:"Solve result is too large to return. This usually means a file output exceeds the supported size."});case"shed":return r(503,{message:n.message,retryAfter:n.retryAfterSeconds});case"compute_error":throw n.error}}toWebResponse(n){let t=this.toResponse(n);return new Response(typeof t.body=="string"?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(n){let t=n.definitionSource;if(ne(t)){let r=this.byteCache.getOrLoad(t.versionId,t.load);return{definitionSource:r,byteRefOutcome:r.outcome,identity:r.key}}if(te(t))return{definitionSource:t,byteRefOutcome:t.outcome,identity:t.key};if(_compute.isDefinitionRef.call(void 0, t))return{definitionSource:t,identity:t.key};if(!n.definitionKey)throw new Error("SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.");return{definitionSource:t,identity:n.definitionKey}}};exports.COMPUTE_CONTRACT_VERSION = F; exports.COMPUTE_VERSION_HEADER = _; exports.SolveEngine = L; exports.adaptEnvelopeToEncoding = T; exports.buildSolveInputTree = E; exports.createClientCache = k; exports.createDefinitionByteCache = P; exports.createSolveCacheSingleFlight = O; exports.isDefinitionRef = _compute.isDefinitionRef; exports.runSolvePipeline = B; exports.serverIdentity = H; exports.transformInputParameter = A;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _core = require('@selvajs/compute/core');var _grasshopper = require('@selvajs/compute/grasshopper');var _zlib = require('zlib');var _util = require('util');function A(e,n){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:_nullishCoalesce(e.minimum, () => (null)),maximum:_nullishCoalesce(e.maximum, () => (null)),atLeast:1,atMost:1,stepSize:e.stepSize,default:_nullishCoalesce(n, () => (e.default))},r=_grasshopper.processInput.call(void 0, t),o=_nullishCoalesce(n, () => (e.default));return r.default==null&&o!=null&&(Array.isArray(o)||r.paramType==="ValueList"&&typeof o!="object")&&(r.default=o),r}var j=_util.promisify.call(void 0, _zlib.gzip),F= exports.COMPUTE_CONTRACT_VERSION =1,_= exports.COMPUTE_VERSION_HEADER ="X-Selva-Compute-Version";function E(e,n){return _grasshopper.TreeBuilder.fromInputParams(e.filter(t=>t.paramType).map(t=>A(t,n[t.id])))}async function B(e){let{client:n,signal:t}=e,r=performance.now(),o=_nullishCoalesce(e.inputTree, () => (E(e.inputs,e.values))),c=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,l=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let x=performance.now()-l;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:x}:{kind:"timeout",durationMs:x,message:`Solve exceeded the ${Math.round(e.solveDeadlineMs/1e3)}s deadline.`};if(v instanceof _core.ComputeError){if(v.code===_core.ErrorCodes.QUEUE_FULL)return{kind:"shed",durationMs:x,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===_core.ErrorCodes.QUEUE_TIMEOUT)return{kind:"shed",durationMs:x,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:x,error:v}}let d=performance.now()-l,f=performance.now(),a;try{a=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(a.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-f,M=/\bgzip\b/i.test(e.acceptEncoding)&&a.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(a)),S=performance.now()-v}let y=performance.now()-e.loadStartMs,u=n.solveMeta.seq===s+1?n.solveMeta.last:null,h=u&&!u.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,g={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,rhino:h,settle:u,byteRefOutcome:e.byteRefOutcome,prepMarks:_nullishCoalesce(e.prepMarks, () => ([]))}),[_]:String(F),Vary:"Accept-Encoding"},R={treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,serializedBytes:a.length,compressedBytes:_nullishCoalesce(_optionalChain([C, 'optionalAccess', _2 => _2.byteLength]), () => (null))},D;return C?(g["Content-Encoding"]="gzip",g["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:g,result:i,metrics:R}):(g["Content-Length"]=String(Buffer.byteLength(a)),D={body:a,headers:g,result:i,metrics:R}),{kind:"ok",envelope:D,solveMs:d,errorCount:_nullishCoalesce(_optionalChain([i, 'access', _3 => _3.errors, 'optionalAccess', _4 => _4.length]), () => (0)),warningCount:_nullishCoalesce(_optionalChain([i, 'access', _5 => _5.warnings, 'optionalAccess', _6 => _6.length]), () => (0))}}function T(e,n){let t=/\bgzip\b/i.test(n);if(e.encoding==="gzip"&&!t){let r=_zlib.gunzipSync.call(void 0, e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(r)),{body:r,headers:o}}return{body:e.body,headers:e.headers}}function V(e){let n=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let c=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-c);n+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${s.toFixed(1)}`}for(let[c,s]of e.prepMarks)n+=`, p_${c};dur=${s.toFixed(1)}`;let r=e.settle;r&&(n+=`, selva_cache;dur=${r.fromCache?1:0}`,r.definitionReuploaded!==void 0&&(n+=`, def_reupload;dur=${r.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let c=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${c}`}return n}function H(e){return e.id}var J=16,z=1,X=300*1e3;function k(e){let n=_nullishCoalesce(e.maxWarmComputeServers, () => (J)),t=new Map,r=new Map;e.debug==="verbose"&&_core.enableDebugLogging.call(void 0, );let o=s=>{e.debug&&_optionalChain([e, 'access', _7 => _7.onDebugLog, 'optionalCall', _8 => _8(s)])};async function c(s,m){let i={last:null,seq:0},l={last:null,seq:0},d=e.debug==="verbose",f={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:d,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:y=>{let u=_nullishCoalesce(y.decode, () => (0)),h=_nullishCoalesce(y.solve, () => (0)),b=_nullishCoalesce(y.encode, () => (0));if(i.seq+=1,i.last={decode:u,solve:h,encode:b},!e.debug)return;let g=u<=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=${u}ms solve=${h}ms encode=${b}ms | definition-cache: ${g} | solve-cache (cachesolve): ${R}`)}};m&&(f.headers={"X-Selva-Definition":m});let a=await _grasshopper.GrasshopperClient.create(f),p=z,w=0,M=!1;async function C(y){if(!M){M=!0;try{let u=await Promise.resolve(_optionalChain([a, 'access', _9 => _9.serverStats, 'optionalAccess', _10 => _10.getActiveChildren, 'call', _11 => _11({initialize:!1})])).catch(()=>null);w=Date.now();let h=!u||u<=0?z:u;if(h===p)return;o(`[Compute/client-cache] concurrency ${p} \u2192 ${h} (server child count)`),p=h,_optionalChain([y, 'optionalAccess', _12 => _12.setMaxConcurrent, 'call', _13 => _13(h)])}finally{M=!1}}}await C();let S=a.createScheduler({mode:"queue",maxConcurrent:p,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.solveDeadlineMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(y,u)=>{if(l.seq+=1,u.status==="success"&&(u.fromCache!==!0&&Date.now()-w>=X&&C(S),l.last={fromCache:u.fromCache===!0,definitionReuploaded:u.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${u.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(u.durationMs)}ms)`),u.fromCache||o(u.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":u.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!u.fromCache))){let h=_optionalChain([u, 'access', _14 => _14.response, 'optionalAccess', _15 => _15.errors]),b=Array.isArray(h)?h.length:0;if(b>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${g}. First: ${JSON.stringify(h[0])}`)}}}});return{client:a,scheduler:S,rhinoTiming:i,solveMeta:l}}return{async getClient(s,m){let i=s.id,l=t.get(i);if(l)return t.delete(i),t.set(i,l),l;let d=r.get(i);if(d)return d;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let f=(async()=>{let a=await c(s,_optionalChain([m, 'optionalAccess', _16 => _16.definitionGuid]));if(t.size>=n){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${n})`),_optionalChain([t, 'access', _17 => _17.get, 'call', _18 => _18(p), 'optionalAccess', _19 => _19.scheduler, 'access', _20 => _20.dispose, 'call', _21 => _21()]),t.delete(p))}return t.set(i,a),a})().finally(()=>r.delete(i));return r.set(i,f),f},evict(s){let m=s,i=t.get(m);i&&(o(`[Compute/client-cache] evicted warm client for server ${m} (config change)`),i.scheduler.dispose(),t.delete(m))},solveCacheStats(){let s={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let m of t.values()){let i=m.scheduler.cacheStats();s.entries+=i.entries,s.bytes+=i.bytes,s.hits+=i.hits,s.misses+=i.misses,s.evictions+=i.evictions}return s},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.dispose();t.clear()}}}function P(e){let n=Math.max(0,Math.floor(e)),t=new Map,r=0,o={hits:0,misses:0,evictions:0};function c(i,l){t.delete(i),t.set(i,l)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,l=t.get(i);t.delete(i),r-=l.bytes.byteLength,o.evictions+=1}}function m(i,l){if(n===0||l.byteLength>n)return;let d=t.get(i);d&&(r-=d.bytes.byteLength),t.set(i,{bytes:l}),r+=l.byteLength,s()}return{getOrLoad(i,l){let d={loaded:!1,fromCache:!1};return{key:i,outcome:d,load:async()=>{let f=t.get(i);if(f)return c(i,f),o.hits+=1,d.loaded=!0,d.fromCache=!0,f.bytes;o.misses+=1,d.loaded=!0,d.fromCache=!1;let a=await l();return m(i,a),a}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:r}},clear(){t.clear(),r=0}}}function O(e={}){let n=new Map;return{run(t,r,o){let c=n.get(t);if(c)return _optionalChain([e, 'access', _22 => _22.onJoin, 'optionalCall', _23 => _23(t)]),_optionalChain([c, 'access', _24 => _24.notifyOwner, 'optionalCall', _25 => _25()]),c.promise;let s={notifyOwner:o,promise:void 0},m=(async()=>r())().finally(()=>{s.notifyOwner=void 0,n.get(t)===s&&n.delete(t)});return s.promise=m,n.set(t,s),m},inFlight(){return n.size}}}var _platform = require('@selvajs/platform');function te(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&"outcome"in e}function ne(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&!("outcome"in e)&&!("key"in e)&&"versionId"in e}var L=class{constructor(n){this.limits=n.limits;let t=_nullishCoalesce(n.logger, () => (new _platform.NoopLogger));this.clientCache=k({solveDeadlineMs:this.limits.solveDeadlineMs,cachesolve:this.limits.computeServerCachesolve,cacheerroredsolves:this.limits.computeCacheErroredSolves,reuseServerDefinitionCache:this.limits.computeReuseDefinitionCache,maxQueueDepth:this.limits.computeMaxQueueDepth,queueWaitMs:this.limits.computeQueueWaitMs,responseCacheMaxBytes:this.limits.computeSolveCacheBytes,debug:_nullishCoalesce(n.debug, () => (!1)),maxWarmComputeServers:n.maxWarmComputeServers,onDebugLog:_nullishCoalesce(n.onDebugLog, () => ((r=>t.debug(r,{component:"Compute/client-cache"}))))}),this.byteCache=P(this.limits.computeDefinitionCacheBytes),this.singleFlight=O({onJoin:n.onSolveCoalesced})}getClient(n,t){return this.clientCache.getClient(n,t)}definitionRef(n,t){return this.byteCache.getOrLoad(n,t)}evictServer(n){this.clientCache.evict(n)}clearSolveCaches(){this.clientCache.clearSolveCaches()}stats(){return{client:this.clientCache.solveCacheStats(),definitionBytes:this.byteCache.stats(),coalescing:{inFlight:this.singleFlight.inFlight()}}}async solve(n){let{definitionSource:t,byteRefOutcome:r,identity:o}=this.resolveDefinitionSource(n),c=await this.clientCache.getClient(n.server,{definitionGuid:n.definitionGuid}),s=E(n.inputs,n.values),m=`${o}:${n.server.id}:${_grasshopper.stableStringify.call(void 0, s)}`,i=new AbortController,l=!1,d=()=>{l||i.abort()};n.signal.addEventListener("abort",d,{once:!0});let f=_nullishCoalesce(n.acceptEncoding, () => ("")),a;try{a=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:c,responseMaxBytes:this.limits.computeResponseMaxBytes,solveDeadlineMs:this.limits.solveDeadlineMs,acceptEncoding:f,signal:i.signal,loadStartMs:_nullishCoalesce(n.loadStartMs, () => (performance.now())),defLoadMs:_nullishCoalesce(n.defLoadMs, () => (0)),prepMarks:n.prepMarks}),()=>{l=!0})}finally{n.signal.removeEventListener("abort",d)}if(a.kind!=="ok")return a;let p=T(a.envelope,f);return{...a,envelope:{...a.envelope,body:p.body,headers:p.headers}}}toResponse(n,t){let r=(o,c)=>{_optionalChain([t, 'optionalAccess', _26 => _26.onError])&&t.onError(o,c);let s={"Content-Type":"application/json"};return c.retryAfter!==void 0&&(s["Retry-After"]=String(c.retryAfter)),{status:o,headers:s,body:JSON.stringify(c)}};switch(n.kind){case"ok":return{status:200,headers:n.envelope.headers,body:n.envelope.body};case"timeout":return r(504,{message:n.message});case"client_abort":return r(499,{message:"Client closed request"});case"too_large":return r(413,{message:"Solve result is too large to return. This usually means a file output exceeds the supported size."});case"shed":return r(503,{message:n.message,retryAfter:n.retryAfterSeconds});case"compute_error":throw n.error}}toWebResponse(n){let t=this.toResponse(n);return new Response(typeof t.body=="string"?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(n){let t=n.definitionSource;if(ne(t)){let r=this.byteCache.getOrLoad(t.versionId,t.load);return{definitionSource:r,byteRefOutcome:r.outcome,identity:r.key}}if(te(t))return{definitionSource:t,byteRefOutcome:t.outcome,identity:t.key};if(_grasshopper.isDefinitionRef.call(void 0, t))return{definitionSource:t,identity:t.key};if(!n.definitionKey)throw new Error("SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.");return{definitionSource:t,identity:n.definitionKey}}};exports.COMPUTE_CONTRACT_VERSION = F; exports.COMPUTE_VERSION_HEADER = _; exports.SolveEngine = L; exports.adaptEnvelopeToEncoding = T; exports.buildSolveInputTree = E; exports.createClientCache = k; exports.createDefinitionByteCache = P; exports.createSolveCacheSingleFlight = O; exports.isDefinitionRef = _core.isDefinitionRef; exports.runSolvePipeline = B; exports.serverIdentity = H; exports.transformInputParameter = A;
|
|
2
2
|
//# sourceMappingURL=server.cjs.map
|
package/dist/server.cjs.map
CHANGED
|
@@ -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 * solve and returns a discriminated {@link SolveOutcome} instead of throwing\n * for expected failures. Auth, the database, share tokens, rate limits, and\n * metric sinks are the calling route's job, not this file's.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/** Mutable load outcome for a `DefinitionRef` source, so the `def_bytes` Server-Timing verdict can be emitted. Omit for raw-bytes solves. */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}; skips the\n\t * pipeline's own build and solves this object directly.\n\t *\n\t * A caller coalescing concurrent solves needs this: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Only phrases the timeout message — the scheduler enforces the deadline itself. */\n\tsolveDeadlineMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also distinguishes a client disconnect\n\t * from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/** Wall-clock origin (`performance.now()` at the top of the request) so `load`/`total` phase timings line up with the caller's pre-solve prep. */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/** Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*` Server-Timing entries. */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure the\n * calling route maps to a status code (timeout→504, client_abort→499,\n * too_large→413, shed→503+Retry-After, compute_error→generic 500/503).\n * `durationMs` on error variants is the solve wall time up to the failure, for\n * the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** Scheduler backpressure rejected the solve before it ran: queue was full (`queue_full`) or the wait exceeded the deadline (`queue_timeout`). Retryable — `retryAfterSeconds` is a backoff hint. */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/** The transformed input tree exactly as handed to the scheduler; `runSolvePipeline` calls this itself unless the caller supplies {@link SolvePipelineArgs.inputTree}. */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// client.solveMeta/rhinoTiming live on the warm client and are written by\n\t// every request on this server; with scheduler maxConcurrent > 1, another\n\t// request can write them mid-flight. A slot is attributed to THIS request\n\t// only if exactly one write happened since this snapshot — for solveMeta\n\t// that single settle is necessarily ours, since onSettle fires before\n\t// scheduler.solve() resolves. Anything ambiguous is dropped from\n\t// Server-Timing rather than misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// AbortError with the request signal NOT aborted means the scheduler's own\n\t\t// deadline timer fired, not a client disconnect.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.solveDeadlineMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tif (err instanceof RhinoComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t// here. Treat it the same as the byte cap: too_large.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// Buffered, not streamed, so Content-Length is known upfront and a connection\n\t// cut mid-transfer fails hard instead of silently truncating the JSON. Below\n\t// 1 KB gzip isn't worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// rhino is only meaningful when settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope},\n * baked from the FIRST caller's `Accept-Encoding`. A later waiter with a\n * different `Accept-Encoding` would otherwise get a body labelled with the\n * wrong encoding — `Vary` can't help here, since it's one shared object, not a\n * cache lookup. Encoding is deliberately left out of the coalesce key so mixed\n * clients still coalesce; this adapts the shared result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// Only mismatch that corrupts the response: gzip body, non-gzip waiter.\n\t// Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own\n * decode/solve/encode (VektorNode fork), `rhino_*` is time on the compute\n * server and `compute_link` is everything between it (network + queue wait).\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// skipped = scheduler never materialized bytes (pointer-known solve, the\n\t// whole point of this path); hit = warm byte cache; miss = fell through to\n\t// storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * `SchemaInput` has no `values`/`acceptedFormats` (those live on the layout item's config), so\n * valueList/file inputs here carry just the selected value with no option list.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// processInput has no dynamicValueList handler — the plugin's GetDynamicValueListParameter\n\t\t// reuses TypeName \"ValueList\" on the wire, so map it here or it falls back to Geometry with\n\t\t// a null default and TreeBuilder drops the input (Grasshopper solves on a stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// processInput drops a default in two cases we need to override, or the input is omitted from\n\t// the solve and the user's selection never reaches Grasshopper: a plain array default (e.g. a\n\t// dynamic value list checklist) is rejected as malformed; and ValueList drops a scalar default\n\t// it can't validate against a `values` map — which SchemaInput never carries, so every app\n\t// value-list selection hits this path.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Restoring for other types would mean shipping a value the package rejected as\n\t\t\t// genuinely unparseable — scoped to ValueList only.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n"]}
|
|
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","ComputeError","ErrorCodes","solveMs","serializeStart","serialized","serializeMs","worthSendingGzip","compressed","gzipMs","gzipStart","serverTotalMs","settle","rhino","headers","buildServerTiming","metrics","envelope","adaptEnvelopeToEncoding","acceptEncoding","waiterWantsGzip","json","gunzipSync","parts","header"],"mappings":"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","file":"/home/runner/work/selva/selva/packages/solve/dist/server.cjs","sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * solve and returns a discriminated {@link SolveOutcome} instead of throwing\n * for expected failures. Auth, the database, share tokens, rate limits, and\n * metric sinks are the calling route's job, not this file's.\n */\n\nimport { ErrorCodes, ComputeError, type SolveDefinition } from '@selvajs/compute/core';\nimport {\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam\n} from '@selvajs/compute/grasshopper';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/** Mutable load outcome for a `DefinitionRef` source, so the `def_bytes` Server-Timing verdict can be emitted. Omit for raw-bytes solves. */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}; skips the\n\t * pipeline's own build and solves this object directly.\n\t *\n\t * A caller coalescing concurrent solves needs this: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Only phrases the timeout message — the scheduler enforces the deadline itself. */\n\tsolveDeadlineMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also distinguishes a client disconnect\n\t * from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/** Wall-clock origin (`performance.now()` at the top of the request) so `load`/`total` phase timings line up with the caller's pre-solve prep. */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/** Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*` Server-Timing entries. */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure the\n * calling route maps to a status code (timeout→504, client_abort→499,\n * too_large→413, shed→503+Retry-After, compute_error→generic 500/503).\n * `durationMs` on error variants is the solve wall time up to the failure, for\n * the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** Scheduler backpressure rejected the solve before it ran: queue was full (`queue_full`) or the wait exceeded the deadline (`queue_timeout`). Retryable — `retryAfterSeconds` is a backoff hint. */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/** The transformed input tree exactly as handed to the scheduler; `runSolvePipeline` calls this itself unless the caller supplies {@link SolvePipelineArgs.inputTree}. */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// client.solveMeta/rhinoTiming live on the warm client and are written by\n\t// every request on this server; with scheduler maxConcurrent > 1, another\n\t// request can write them mid-flight. A slot is attributed to THIS request\n\t// only if exactly one write happened since this snapshot — for solveMeta\n\t// that single settle is necessarily ours, since onSettle fires before\n\t// scheduler.solve() resolves. Anything ambiguous is dropped from\n\t// Server-Timing rather than misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// AbortError with the request signal NOT aborted means the scheduler's own\n\t\t// deadline timer fired, not a client disconnect.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.solveDeadlineMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tif (err instanceof ComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t// here. Treat it the same as the byte cap: too_large.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// Buffered, not streamed, so Content-Length is known upfront and a connection\n\t// cut mid-transfer fails hard instead of silently truncating the JSON. Below\n\t// 1 KB gzip isn't worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// rhino is only meaningful when settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope},\n * baked from the FIRST caller's `Accept-Encoding`. A later waiter with a\n * different `Accept-Encoding` would otherwise get a body labelled with the\n * wrong encoding — `Vary` can't help here, since it's one shared object, not a\n * cache lookup. Encoding is deliberately left out of the coalesce key so mixed\n * clients still coalesce; this adapts the shared result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// Only mismatch that corrupts the response: gzip body, non-gzip waiter.\n\t// Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own\n * decode/solve/encode (VektorNode fork), `rhino_*` is time on the compute\n * server and `compute_link` is everything between it (network + queue wait).\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// skipped = scheduler never materialized bytes (pointer-known solve, the\n\t// whole point of this path); hit = warm byte cache; miss = fell through to\n\t// storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute/grasshopper';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * `SchemaInput` has no `values`/`acceptedFormats` (those live on the layout item's config), so\n * valueList/file inputs here carry just the selected value with no option list.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// processInput has no dynamicValueList handler — the plugin's GetDynamicValueListParameter\n\t\t// reuses TypeName \"ValueList\" on the wire, so map it here or it falls back to Geometry with\n\t\t// a null default and TreeBuilder drops the input (Grasshopper solves on a stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// processInput drops a default in two cases we need to override, or the input is omitted from\n\t// the solve and the user's selection never reaches Grasshopper: a plain array default (e.g. a\n\t// dynamic value list checklist) is rejected as malformed; and ValueList drops a scalar default\n\t// it can't validate against a `values` map — which SchemaInput never carries, so every app\n\t// value-list selection hits this path.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Restoring for other types would mean shipping a value the package rejected as\n\t\t\t// genuinely unparseable — scoped to ValueList only.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n"]}
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { DefinitionRef, SolveDefinition, isDefinitionRef } from '@selvajs/compute';
|
|
1
|
+
import { SolveDefinition } from '@selvajs/compute/core';
|
|
2
|
+
export { DefinitionRef, SolveDefinition, isDefinitionRef } from '@selvajs/compute/core';
|
|
3
|
+
import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, DataTree, InputParam, DefinitionRef } from '@selvajs/compute/grasshopper';
|
|
3
4
|
import { SchemaInput } from '@selvajs/schemas';
|
|
4
5
|
import { ILogger } from '@selvajs/platform';
|
|
5
6
|
export { S as SolveInput } from './solve-input-CqYtetTA.cjs';
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { DefinitionRef, SolveDefinition, isDefinitionRef } from '@selvajs/compute';
|
|
1
|
+
import { SolveDefinition } from '@selvajs/compute/core';
|
|
2
|
+
export { DefinitionRef, SolveDefinition, isDefinitionRef } from '@selvajs/compute/core';
|
|
3
|
+
import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, DataTree, InputParam, DefinitionRef } from '@selvajs/compute/grasshopper';
|
|
3
4
|
import { SchemaInput } from '@selvajs/schemas';
|
|
4
5
|
import { ILogger } from '@selvajs/platform';
|
|
5
6
|
export { S as SolveInput } from './solve-input-CqYtetTA.js';
|
package/dist/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
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),c=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,l=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let R=performance.now()-l;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:R}:{kind:"timeout",durationMs:R,message:`Solve exceeded the ${Math.round(e.solveDeadlineMs/1e3)}s deadline.`};if(v instanceof U){if(v.code===I.QUEUE_FULL)return{kind:"shed",durationMs:R,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===I.QUEUE_TIMEOUT)return{kind:"shed",durationMs:R,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:R,error:v}}let d=performance.now()-l,h=performance.now(),a;try{a=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(a.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,M=/\bgzip\b/i.test(e.acceptEncoding)&&a.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(a)),S=performance.now()-v}let y=performance.now()-e.loadStartMs,u=n.solveMeta.seq===s+1?n.solveMeta.last:null,f=u&&!u.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,g={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,rhino:f,settle:u,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[_]:String(F),Vary:"Accept-Encoding"},x={treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,serializedBytes:a.length,compressedBytes:C?.byteLength??null},D;return C?(g["Content-Encoding"]="gzip",g["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:g,result:i,metrics:x}):(g["Content-Length"]=String(Buffer.byteLength(a)),D={body:a,headers:g,result:i,metrics:x}),{kind:"ok",envelope:D,solveMs:d,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 c=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-c);n+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${s.toFixed(1)}`}for(let[c,s]of e.prepMarks)n+=`, p_${c};dur=${s.toFixed(1)}`;let r=e.settle;r&&(n+=`, selva_cache;dur=${r.fromCache?1:0}`,r.definitionReuploaded!==void 0&&(n+=`, def_reupload;dur=${r.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let c=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${c}`}return n}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 c(s,m){let i={last:null,seq:0},l={last:null,seq:0},d=e.debug==="verbose",h={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:d,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:y=>{let u=y.decode??0,f=y.solve??0,b=y.encode??0;if(i.seq+=1,i.last={decode:u,solve:f,encode:b},!e.debug)return;let g=u<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",x=e.cachesolve&&f<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${u}ms solve=${f}ms encode=${b}ms | definition-cache: ${g} | solve-cache (cachesolve): ${x}`)}};m&&(h.headers={"X-Selva-Definition":m});let a=await K.create(h),p=z,w=0,M=!1;async function C(y){if(!M){M=!0;try{let u=await Promise.resolve(a.serverStats?.getActiveChildren({initialize:!1})).catch(()=>null);w=Date.now();let f=!u||u<=0?z:u;if(f===p)return;o(`[Compute/client-cache] concurrency ${p} \u2192 ${f} (server child count)`),p=f,y?.setMaxConcurrent(f)}finally{M=!1}}}await C();let S=a.createScheduler({mode:"queue",maxConcurrent:p,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.solveDeadlineMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(y,u)=>{if(l.seq+=1,u.status==="success"&&(u.fromCache!==!0&&Date.now()-w>=X&&C(S),l.last={fromCache:u.fromCache===!0,definitionReuploaded:u.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${u.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(u.durationMs)}ms)`),u.fromCache||o(u.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":u.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!u.fromCache))){let f=u.response?.errors,b=Array.isArray(f)?f.length:0;if(b>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${g}. First: ${JSON.stringify(f[0])}`)}}}});return{client:a,scheduler:S,rhinoTiming:i,solveMeta:l}}return{async getClient(s,m){let i=s.id,l=t.get(i);if(l)return t.delete(i),t.set(i,l),l;let d=r.get(i);if(d)return d;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let h=(async()=>{let a=await c(s,m?.definitionGuid);if(t.size>=n){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${n})`),t.get(p)?.scheduler.dispose(),t.delete(p))}return t.set(i,a),a})().finally(()=>r.delete(i));return r.set(i,h),h},evict(s){let m=s,i=t.get(m);i&&(o(`[Compute/client-cache] evicted warm client for server ${m} (config change)`),i.scheduler.dispose(),t.delete(m))},solveCacheStats(){let s={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let m of t.values()){let i=m.scheduler.cacheStats();s.entries+=i.entries,s.bytes+=i.bytes,s.hits+=i.hits,s.misses+=i.misses,s.evictions+=i.evictions}return s},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.dispose();t.clear()}}}function P(e){let n=Math.max(0,Math.floor(e)),t=new Map,r=0,o={hits:0,misses:0,evictions:0};function c(i,l){t.delete(i),t.set(i,l)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,l=t.get(i);t.delete(i),r-=l.bytes.byteLength,o.evictions+=1}}function m(i,l){if(n===0||l.byteLength>n)return;let d=t.get(i);d&&(r-=d.bytes.byteLength),t.set(i,{bytes:l}),r+=l.byteLength,s()}return{getOrLoad(i,l){let d={loaded:!1,fromCache:!1};return{key:i,outcome:d,load:async()=>{let h=t.get(i);if(h)return c(i,h),o.hits+=1,d.loaded=!0,d.fromCache=!0,h.bytes;o.misses+=1,d.loaded=!0,d.fromCache=!1;let a=await l();return m(i,a),a}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:r}},clear(){t.clear(),r=0}}}function O(e={}){let n=new Map;return{run(t,r,o){let c=n.get(t);if(c)return e.onJoin?.(t),c.notifyOwner?.(),c.promise;let s={notifyOwner:o,promise:void 0},m=(async()=>r())().finally(()=>{s.notifyOwner=void 0,n.get(t)===s&&n.delete(t)});return s.promise=m,n.set(t,s),m},inFlight(){return n.size}}}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({solveDeadlineMs:this.limits.solveDeadlineMs,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),c=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,l=!1,d=()=>{l||i.abort()};n.signal.addEventListener("abort",d,{once:!0});let h=n.acceptEncoding??"",a;try{a=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:c,responseMaxBytes:this.limits.computeResponseMaxBytes,solveDeadlineMs:this.limits.solveDeadlineMs,acceptEncoding:h,signal:i.signal,loadStartMs:n.loadStartMs??performance.now(),defLoadMs:n.defLoadMs??0,prepMarks:n.prepMarks}),()=>{l=!0})}finally{n.signal.removeEventListener("abort",d)}if(a.kind!=="ok")return a;let p=T(a.envelope,h);return{...a,envelope:{...a.envelope,body:p.body,headers:p.headers}}}toResponse(n,t){let r=(o,c)=>{t?.onError&&t.onError(o,c);let s={"Content-Type":"application/json"};return c.retryAfter!==void 0&&(s["Retry-After"]=String(c.retryAfter)),{status:o,headers:s,body:JSON.stringify(c)}};switch(n.kind){case"ok":return{status:200,headers:n.envelope.headers,body:n.envelope.body};case"timeout":return r(504,{message:n.message});case"client_abort":return r(499,{message:"Client closed request"});case"too_large":return r(413,{message:"Solve result is too large to return. This usually means a file output exceeds the supported size."});case"shed":return r(503,{message:n.message,retryAfter:n.retryAfterSeconds});case"compute_error":throw n.error}}toWebResponse(n){let t=this.toResponse(n);return new Response(typeof t.body=="string"?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(n){let t=n.definitionSource;if(ne(t)){let r=this.byteCache.getOrLoad(t.versionId,t.load);return{definitionSource:r,byteRefOutcome:r.outcome,identity:r.key}}if(te(t))return{definitionSource:t,byteRefOutcome:t.outcome,identity:t.key};if(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};
|
|
1
|
+
import{ErrorCodes as I,ComputeError as U}from"@selvajs/compute/core";import{TreeBuilder as q}from"@selvajs/compute/grasshopper";import{gzip as W,gunzipSync as G}from"zlib";import{promisify as N}from"util";import{processInput as $}from"@selvajs/compute/grasshopper";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),c=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,l=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let x=performance.now()-l;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:x}:{kind:"timeout",durationMs:x,message:`Solve exceeded the ${Math.round(e.solveDeadlineMs/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 d=performance.now()-l,f=performance.now(),a;try{a=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(a.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-f,M=/\bgzip\b/i.test(e.acceptEncoding)&&a.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(a)),S=performance.now()-v}let y=performance.now()-e.loadStartMs,u=n.solveMeta.seq===s+1?n.solveMeta.last:null,h=u&&!u.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,g={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,rhino:h,settle:u,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[_]:String(F),Vary:"Accept-Encoding"},R={treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,serializedBytes:a.length,compressedBytes:C?.byteLength??null},D;return C?(g["Content-Encoding"]="gzip",g["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:g,result:i,metrics:R}):(g["Content-Length"]=String(Buffer.byteLength(a)),D={body:a,headers:g,result:i,metrics:R}),{kind:"ok",envelope:D,solveMs:d,errorCount: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 c=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-c);n+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${s.toFixed(1)}`}for(let[c,s]of e.prepMarks)n+=`, p_${c};dur=${s.toFixed(1)}`;let r=e.settle;r&&(n+=`, selva_cache;dur=${r.fromCache?1:0}`,r.definitionReuploaded!==void 0&&(n+=`, def_reupload;dur=${r.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let c=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${c}`}return n}import{GrasshopperClient as K}from"@selvajs/compute/grasshopper";import{enableDebugLogging as Q}from"@selvajs/compute/core";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 c(s,m){let i={last:null,seq:0},l={last:null,seq:0},d=e.debug==="verbose",f={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:d,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:y=>{let u=y.decode??0,h=y.solve??0,b=y.encode??0;if(i.seq+=1,i.last={decode:u,solve:h,encode:b},!e.debug)return;let g=u<=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=${u}ms solve=${h}ms encode=${b}ms | definition-cache: ${g} | solve-cache (cachesolve): ${R}`)}};m&&(f.headers={"X-Selva-Definition":m});let a=await K.create(f),p=z,w=0,M=!1;async function C(y){if(!M){M=!0;try{let u=await Promise.resolve(a.serverStats?.getActiveChildren({initialize:!1})).catch(()=>null);w=Date.now();let h=!u||u<=0?z:u;if(h===p)return;o(`[Compute/client-cache] concurrency ${p} \u2192 ${h} (server child count)`),p=h,y?.setMaxConcurrent(h)}finally{M=!1}}}await C();let S=a.createScheduler({mode:"queue",maxConcurrent:p,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.solveDeadlineMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(y,u)=>{if(l.seq+=1,u.status==="success"&&(u.fromCache!==!0&&Date.now()-w>=X&&C(S),l.last={fromCache:u.fromCache===!0,definitionReuploaded:u.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${u.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(u.durationMs)}ms)`),u.fromCache||o(u.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":u.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!u.fromCache))){let h=u.response?.errors,b=Array.isArray(h)?h.length:0;if(b>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${g}. First: ${JSON.stringify(h[0])}`)}}}});return{client:a,scheduler:S,rhinoTiming:i,solveMeta:l}}return{async getClient(s,m){let i=s.id,l=t.get(i);if(l)return t.delete(i),t.set(i,l),l;let d=r.get(i);if(d)return d;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let f=(async()=>{let a=await c(s,m?.definitionGuid);if(t.size>=n){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${n})`),t.get(p)?.scheduler.dispose(),t.delete(p))}return t.set(i,a),a})().finally(()=>r.delete(i));return r.set(i,f),f},evict(s){let m=s,i=t.get(m);i&&(o(`[Compute/client-cache] evicted warm client for server ${m} (config change)`),i.scheduler.dispose(),t.delete(m))},solveCacheStats(){let s={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let m of t.values()){let i=m.scheduler.cacheStats();s.entries+=i.entries,s.bytes+=i.bytes,s.hits+=i.hits,s.misses+=i.misses,s.evictions+=i.evictions}return s},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.dispose();t.clear()}}}function P(e){let n=Math.max(0,Math.floor(e)),t=new Map,r=0,o={hits:0,misses:0,evictions:0};function c(i,l){t.delete(i),t.set(i,l)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,l=t.get(i);t.delete(i),r-=l.bytes.byteLength,o.evictions+=1}}function m(i,l){if(n===0||l.byteLength>n)return;let d=t.get(i);d&&(r-=d.bytes.byteLength),t.set(i,{bytes:l}),r+=l.byteLength,s()}return{getOrLoad(i,l){let d={loaded:!1,fromCache:!1};return{key:i,outcome:d,load:async()=>{let f=t.get(i);if(f)return c(i,f),o.hits+=1,d.loaded=!0,d.fromCache=!0,f.bytes;o.misses+=1,d.loaded=!0,d.fromCache=!1;let a=await l();return m(i,a),a}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:r}},clear(){t.clear(),r=0}}}function O(e={}){let n=new Map;return{run(t,r,o){let c=n.get(t);if(c)return e.onJoin?.(t),c.notifyOwner?.(),c.promise;let s={notifyOwner:o,promise:void 0},m=(async()=>r())().finally(()=>{s.notifyOwner=void 0,n.get(t)===s&&n.delete(t)});return s.promise=m,n.set(t,s),m},inFlight(){return n.size}}}import{isDefinitionRef as Y,stableStringify as Z}from"@selvajs/compute/grasshopper";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({solveDeadlineMs:this.limits.solveDeadlineMs,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),c=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,l=!1,d=()=>{l||i.abort()};n.signal.addEventListener("abort",d,{once:!0});let f=n.acceptEncoding??"",a;try{a=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:c,responseMaxBytes:this.limits.computeResponseMaxBytes,solveDeadlineMs:this.limits.solveDeadlineMs,acceptEncoding:f,signal:i.signal,loadStartMs:n.loadStartMs??performance.now(),defLoadMs:n.defLoadMs??0,prepMarks:n.prepMarks}),()=>{l=!0})}finally{n.signal.removeEventListener("abort",d)}if(a.kind!=="ok")return a;let p=T(a.envelope,f);return{...a,envelope:{...a.envelope,body:p.body,headers:p.headers}}}toResponse(n,t){let r=(o,c)=>{t?.onError&&t.onError(o,c);let s={"Content-Type":"application/json"};return c.retryAfter!==void 0&&(s["Retry-After"]=String(c.retryAfter)),{status:o,headers:s,body:JSON.stringify(c)}};switch(n.kind){case"ok":return{status:200,headers:n.envelope.headers,body:n.envelope.body};case"timeout":return r(504,{message:n.message});case"client_abort":return r(499,{message:"Client closed request"});case"too_large":return r(413,{message:"Solve result is too large to return. This usually means a file output exceeds the supported size."});case"shed":return r(503,{message:n.message,retryAfter:n.retryAfterSeconds});case"compute_error":throw n.error}}toWebResponse(n){let t=this.toResponse(n);return new Response(typeof t.body=="string"?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(n){let t=n.definitionSource;if(ne(t)){let r=this.byteCache.getOrLoad(t.versionId,t.load);return{definitionSource:r,byteRefOutcome:r.outcome,identity:r.key}}if(te(t))return{definitionSource:t,byteRefOutcome:t.outcome,identity:t.key};if(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 Pe}from"@selvajs/compute/core";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,Pe as isDefinitionRef,B as runSolvePipeline,H as serverIdentity,A as transformInputParameter};
|
|
2
2
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/solve-pipeline.ts","../src/server/transform-input.ts","../src/server/client-cache.ts","../src/server/definition-byte-cache.ts","../src/server/solve-cache-single-flight.ts","../src/server/solve-engine.ts","../src/server/index.ts"],"sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * solve and returns a discriminated {@link SolveOutcome} instead of throwing\n * for expected failures. Auth, the database, share tokens, rate limits, and\n * metric sinks are the calling route's job, not this file's.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/** Mutable load outcome for a `DefinitionRef` source, so the `def_bytes` Server-Timing verdict can be emitted. Omit for raw-bytes solves. */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}; skips the\n\t * pipeline's own build and solves this object directly.\n\t *\n\t * A caller coalescing concurrent solves needs this: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Only phrases the timeout message — the scheduler enforces the deadline itself. */\n\tsolveDeadlineMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also distinguishes a client disconnect\n\t * from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/** Wall-clock origin (`performance.now()` at the top of the request) so `load`/`total` phase timings line up with the caller's pre-solve prep. */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/** Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*` Server-Timing entries. */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure the\n * calling route maps to a status code (timeout→504, client_abort→499,\n * too_large→413, shed→503+Retry-After, compute_error→generic 500/503).\n * `durationMs` on error variants is the solve wall time up to the failure, for\n * the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** Scheduler backpressure rejected the solve before it ran: queue was full (`queue_full`) or the wait exceeded the deadline (`queue_timeout`). Retryable — `retryAfterSeconds` is a backoff hint. */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/** The transformed input tree exactly as handed to the scheduler; `runSolvePipeline` calls this itself unless the caller supplies {@link SolvePipelineArgs.inputTree}. */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// client.solveMeta/rhinoTiming live on the warm client and are written by\n\t// every request on this server; with scheduler maxConcurrent > 1, another\n\t// request can write them mid-flight. A slot is attributed to THIS request\n\t// only if exactly one write happened since this snapshot — for solveMeta\n\t// that single settle is necessarily ours, since onSettle fires before\n\t// scheduler.solve() resolves. Anything ambiguous is dropped from\n\t// Server-Timing rather than misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// AbortError with the request signal NOT aborted means the scheduler's own\n\t\t// deadline timer fired, not a client disconnect.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.solveDeadlineMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tif (err instanceof RhinoComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t// here. Treat it the same as the byte cap: too_large.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// Buffered, not streamed, so Content-Length is known upfront and a connection\n\t// cut mid-transfer fails hard instead of silently truncating the JSON. Below\n\t// 1 KB gzip isn't worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// rhino is only meaningful when settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope},\n * baked from the FIRST caller's `Accept-Encoding`. A later waiter with a\n * different `Accept-Encoding` would otherwise get a body labelled with the\n * wrong encoding — `Vary` can't help here, since it's one shared object, not a\n * cache lookup. Encoding is deliberately left out of the coalesce key so mixed\n * clients still coalesce; this adapts the shared result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// Only mismatch that corrupts the response: gzip body, non-gzip waiter.\n\t// Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own\n * decode/solve/encode (VektorNode fork), `rhino_*` is time on the compute\n * server and `compute_link` is everything between it (network + queue wait).\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// skipped = scheduler never materialized bytes (pointer-known solve, the\n\t// whole point of this path); hit = warm byte cache; miss = fell through to\n\t// storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * `SchemaInput` has no `values`/`acceptedFormats` (those live on the layout item's config), so\n * valueList/file inputs here carry just the selected value with no option list.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// processInput has no dynamicValueList handler — the plugin's GetDynamicValueListParameter\n\t\t// reuses TypeName \"ValueList\" on the wire, so map it here or it falls back to Geometry with\n\t\t// a null default and TreeBuilder drops the input (Grasshopper solves on a stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// processInput drops a default in two cases we need to override, or the input is omitted from\n\t// the solve and the user's selection never reaches Grasshopper: a plain array default (e.g. a\n\t// dynamic value list checklist) is rejected as malformed; and ValueList drops a scalar default\n\t// it can't validate against a `values` map — which SchemaInput never carries, so every app\n\t// value-list selection hits this path.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Restoring for other types would mean shipping a value the package rejected as\n\t\t\t// genuinely unparseable — scoped to ValueList only.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n","import {\n\tGrasshopperClient,\n\tenableDebugLogging,\n\ttype GrasshopperComputeConfig,\n\ttype SolveScheduler\n} from '@selvajs/compute';\n\n// ============================================================================\n// Shared compute client cache\n// ============================================================================\n//\n// One warm `GrasshopperClient` (+ its `SolveScheduler`) per distinct compute\n// server, keyed by the server's `id`, never its URL — a rotated URL/apiKey\n// keeps the same key with stale connection details, so the config-write path\n// MUST call `evict(id)` when a server's config changes.\n//\n// Per-server LRU, not a single shared client: definitions can pin different\n// servers, so two definitions on two servers keep two warm clients, with\n// churn from one-off servers bounded by `maxWarmComputeServers`.\n//\n// Both hot paths share this cache — the solve endpoint uses the entry's\n// `scheduler`; the definition-viewer render path uses the entry's `client`\n// for `getIO` — so a definition that was just solved renders from the same\n// warm client instead of re-handshaking Rhino.Compute per page load.\n\n/**\n * Opaque identity of a compute server — construct only via `serverIdentity()`.\n * Kept opaque so \"a server is now a pool of URLs behind one id\" stays additive.\n */\nexport type ServerIdentity = string & { readonly __brand: 'ServerIdentity' };\n\n/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */\nexport interface ResolvedServer {\n\tid: string;\n\tserverUrl: string;\n\t/** Sent as the `RhinoComputeKey` header. */\n\tapiKey?: string;\n}\n\nexport function serverIdentity(server: Pick<ResolvedServer, 'id'>): ServerIdentity {\n\treturn server.id as ServerIdentity;\n}\n\nexport interface CachedClient {\n\tclient: GrasshopperClient;\n\tscheduler: SolveScheduler;\n\t/**\n\t * Last Server-Timing decode/solve/encode, written by `onServerTiming`. The\n\t * scheduler runs up to `maxConcurrent` solves at once, so `last` alone can't\n\t * be trusted per-request: callers snapshot `seq` before their solve and only\n\t * attribute `last` to themselves if exactly one write happened since (see\n\t * the guard in `runSolvePipeline`), dropping it otherwise rather than risk\n\t * misattributing another request's timing.\n\t */\n\trhinoTiming: { last: { decode: number; solve: number; encode: number } | null; seq: number };\n\t/**\n\t * Same snapshot-and-attribute pattern as `rhinoTiming`, for the scheduler's\n\t * `onSettle` cache verdict. `seq` increments on every settle (success or\n\t * error); `last` is written only on success.\n\t */\n\tsolveMeta: {\n\t\tlast: { fromCache: boolean; definitionReuploaded?: boolean } | null;\n\t\tseq: number;\n\t};\n}\n\n/**\n * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs\n * (`onDebugLog`), `'verbose'` also enables full lib-level request/response\n * dumps (incl. geometry) — a superset of `true`, not an independent setting.\n */\nexport type ClientCacheDebug = boolean | 'verbose';\n\n/** Config injected by the consuming app, so this module stays env-agnostic and testable. */\nexport interface ClientCacheConfig {\n\t/** Per-solve timeout forwarded to the scheduler (`ComputeLimits.solveDeadlineMs`). */\n\tsolveDeadlineMs: number;\n\tcachesolve: boolean;\n\t/** Only meaningful with `cachesolve`. */\n\tcacheerroredsolves: boolean;\n\t/** Reference large definitions by server cache key instead of re-uploading. */\n\treuseServerDefinitionCache: boolean;\n\t/**\n\t * Max solves that may wait in the FIFO queue, excluding in-flight solves\n\t * (capped at `maxConcurrent`, itself driven by the server's probed child\n\t * count). `0` = unbounded (`ComputeLimits.computeMaxQueueDepth`); a full\n\t * queue rejects new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Max ms a solve may sit queued before executing; `0` = no deadline\n\t * (`ComputeLimits.computeQueueWaitMs`). Too long a wait rejects with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache. Applies per warm\n\t * client — worst-case heap is this × `maxWarmComputeServers`. `0` disables\n\t * the cache (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\tdebug: ClientCacheDebug;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */\n\tmaxWarmComputeServers?: number;\n\t/** Sink for the debug lines. `onDebugLog` only fires when `debug` is not `false`. */\n\tonDebugLog?: (message: string) => void;\n}\n\nexport interface ClientCache {\n\t/**\n\t * Get (or create) the warm client + scheduler for a resolved compute server,\n\t * keyed by its `id`. `definitionGuid`, when present, is stamped as the\n\t * `X-Selva-Definition` header on this client's outbound solve/IO requests —\n\t * inert routing/telemetry metadata until a pool router exists.\n\t */\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient>;\n\t/** Dispose and drop the warm client for `id`, so the next request rebuilds against fresh connection details. */\n\tevict(id: string | ServerIdentity): void;\n\t/** Solve-cache counters summed across every warm client. Counters die with the client that owns them, so totals can fall over time. */\n\tsolveCacheStats(): SolveCacheStats;\n\t/**\n\t * Drop every retained solve result, keeping the warm clients (and their\n\t * connections and server-side definition pointers) intact. Nothing expires\n\t * these on its own — the byte budget is the only other pressure — so this is\n\t * the operator's release valve when a definition's inputs no longer describe\n\t * its output, e.g. it reads an external source that has since changed.\n\t */\n\tclearSolveCaches(): void;\n\t/** Dispose every warm client. Test seam / shutdown hook. */\n\tdisposeAll(): void;\n}\n\nexport interface SolveCacheStats {\n\twarmClients: number;\n\tentries: number;\n\tbytes: number;\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped under size/byte pressure (not replacement or manual clears). */\n\tevictions: number;\n}\n\nconst DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;\n\n/**\n * Fallback concurrency while the first probe is still in flight, and the value\n * kept whenever a probe fails or reports 0 — never send more than a single\n * solve to a server whose real capacity is unknown.\n */\nconst FALLBACK_CONCURRENCY = 1;\n\n/**\n * How stale the compute server's child count may get before a solve triggers a\n * re-read. Short enough that a resized pool is picked up within a few solves,\n * long enough that a busy server is probed rarely — the probe rides on solve\n * completion, so an idle server is never polled at all.\n */\nconst CONCURRENCY_PROBE_INTERVAL_MS = 5 * 60 * 1000;\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxWarmComputeServers = config.maxWarmComputeServers ?? DEFAULT_MAX_WARM_COMPUTE_SERVERS;\n\tconst cache = new Map<string, CachedClient>();\n\t// Builds in flight, keyed like `cache`. Concurrent `getClient` calls for the\n\t// same id must share ONE build: without this, both miss, both handshake, and\n\t// the loser's entry is overwritten in the map without ever being disposed.\n\tconst pending = new Map<string, Promise<CachedClient>>();\n\n\tif (config.debug === 'verbose') enableDebugLogging();\n\n\tconst debugLog = (message: string) => {\n\t\tif (config.debug) config.onDebugLog?.(message);\n\t};\n\n\tasync function build(server: ResolvedServer, definitionGuid?: string): Promise<CachedClient> {\n\t\tconst rhinoTiming: CachedClient['rhinoTiming'] = { last: null, seq: 0 };\n\t\tconst solveMeta: CachedClient['solveMeta'] = { last: null, seq: 0 };\n\t\tconst debugVerbose = config.debug === 'verbose';\n\n\t\tconst clientConfig: GrasshopperComputeConfig = {\n\t\t\tserverUrl: server.serverUrl,\n\t\t\tapiKey: server.apiKey,\n\t\t\t// Lib-level debug is the verbose one (full payload logs); keep it on the\n\t\t\t// verbose flag so the concise cache logs can run without the noise.\n\t\t\tdebug: debugVerbose,\n\t\t\tcachesolve: config.cachesolve,\n\t\t\tcacheerroredsolves: config.cacheerroredsolves,\n\t\t\tonServerTiming: (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\trhinoTiming.seq += 1;\n\t\t\t\trhinoTiming.last = { decode, solve, encode };\n\t\t\t\tif (!config.debug) return;\n\t\t\t\t// Heuristic thresholds: a genuine parse/solve is many ms; a cache hit\n\t\t\t\t// is 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 =\n\t\t\t\t\tconfig.cachesolve && solve <= 1\n\t\t\t\t\t\t? 'HIT (cached result, solve skipped)'\n\t\t\t\t\t\t: config.cachesolve\n\t\t\t\t\t\t\t? 'miss (solved fresh)'\n\t\t\t\t\t\t\t: 'off';\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}`\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\n\t\t// Baked at client-create time because the scheduler has no per-request\n\t\t// header hook.\n\t\tif (definitionGuid) {\n\t\t\tclientConfig.headers = { 'X-Selva-Definition': definitionGuid };\n\t\t}\n\n\t\tconst client = await GrasshopperClient.create(clientConfig);\n\n\t\t// rhino.compute hands out children in strict rotation and never reports \"busy\",\n\t\t// so nothing downstream enforces a concurrency cap — send more than it has\n\t\t// children and requests silently double up on one. Rhino.Compute is the only\n\t\t// source of truth for its own child count, so this always probes rather than\n\t\t// trusting an operator-supplied guess.\n\t\t//\n\t\t// The count is not static: children exit on their own under `-idlespan`, and\n\t\t// `/launch-child` + `/recycle-children` resize the pool at runtime. So the probe\n\t\t// re-runs after solves (see `refreshConcurrency`) rather than only at connect.\n\t\tlet maxConcurrent = FALLBACK_CONCURRENCY;\n\t\tlet lastProbeAt = 0;\n\t\tlet probing = false;\n\n\t\t/**\n\t\t * Best-effort re-read of the server's child count. Never awaited by a caller:\n\t\t * a probe must not add a round-trip to the solve path, so a changed count takes\n\t\t * effect on the NEXT solve rather than the one that triggered the probe.\n\t\t *\n\t\t * `initialize: false` keeps this from spawning children on an idle server —\n\t\t * which also means a cold server honestly reports 0. Both a failed probe and a\n\t\t * 0 result fall back to `FALLBACK_CONCURRENCY` (1): an unknown-capacity server\n\t\t * is safer under-used than oversent.\n\t\t */\n\t\tasync function refreshConcurrency(scheduler?: { setMaxConcurrent(n: number): void }) {\n\t\t\tif (probing) return;\n\t\t\tprobing = true;\n\t\t\ttry {\n\t\t\t\tconst children = await Promise.resolve(\n\t\t\t\t\tclient.serverStats?.getActiveChildren({ initialize: false })\n\t\t\t\t).catch(() => null);\n\t\t\t\tlastProbeAt = Date.now();\n\t\t\t\tconst next = !children || children <= 0 ? FALLBACK_CONCURRENCY : children;\n\t\t\t\tif (next === maxConcurrent) return;\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/client-cache] concurrency ${maxConcurrent} → ${next} (server child count)`\n\t\t\t\t);\n\t\t\t\tmaxConcurrent = next;\n\t\t\t\tscheduler?.setMaxConcurrent(next);\n\t\t\t} finally {\n\t\t\t\tprobing = false;\n\t\t\t}\n\t\t}\n\n\t\t// One blocking probe at connect so the first solves already use the right cap.\n\t\tawait refreshConcurrency();\n\n\t\tconst scheduler = client.createScheduler({\n\t\t\tmode: 'queue',\n\t\t\t// Queue mode defaults maxConcurrent to 1, serializing every solve on\n\t\t\t// this server through a single slot while the compute VM's other\n\t\t\t// compute.geometry children idle — always pass it explicitly.\n\t\t\tmaxConcurrent,\n\t\t\t// The scheduler treats undefined as unbounded/no-deadline, so map our\n\t\t\t// `0`-means-off convention to undefined rather than passing 0 (which\n\t\t\t// would reject EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: config.solveDeadlineMs,\n\t\t\t// The byte budget is the only eviction pressure. A solve is a pure\n\t\t\t// function of (definition, inputs), both immutable, so a retained\n\t\t\t// result can never go stale — expiring one only forces a paid re-solve\n\t\t\t// of the identical answer.\n\t\t\tcache: config.responseCacheMaxBytes > 0 ? { maxBytes: config.responseCacheMaxBytes } : false,\n\t\t\treuseServerDefinitionCache: config.reuseServerDefinitionCache,\n\t\t\tonSettle: (_ctx, result) => {\n\t\t\t\tsolveMeta.seq += 1;\n\t\t\t\tif (result.status !== 'success') return;\n\t\t\t\t// Piggyback the pool re-read on real traffic: only servers actually\n\t\t\t\t// being used get probed, at most once per window, and never on the\n\t\t\t\t// request path. A cache hit means we never reached the server, so it\n\t\t\t\t// says nothing about the pool — don't spend a probe on it.\n\t\t\t\tif (\n\t\t\t\t\tresult.fromCache !== true &&\n\t\t\t\t\tDate.now() - lastProbeAt >= CONCURRENCY_PROBE_INTERVAL_MS\n\t\t\t\t) {\n\t\t\t\t\t// Safe despite being referenced from inside its own initializer:\n\t\t\t\t\t// onSettle only ever fires during a solve, long after binding.\n\t\t\t\t\tvoid refreshConcurrency(scheduler);\n\t\t\t\t}\n\t\t\t\tsolveMeta.last = {\n\t\t\t\t\tfromCache: result.fromCache === true,\n\t\t\t\t\tdefinitionReuploaded: result.definitionReuploaded\n\t\t\t\t};\n\t\t\t\tif (!config.debug) return;\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/selva-cache] ${result.fromCache ? 'HIT — served from cache (no compute call)' : 'miss — went to Rhino.Compute'} (${Math.round(result.durationMs)}ms)`\n\t\t\t\t);\n\t\t\t\tif (!result.fromCache) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\tresult.definitionReuploaded === true\n\t\t\t\t\t\t\t? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'\n\t\t\t\t\t\t\t: result.definitionReuploaded === false\n\t\t\t\t\t\t\t\t? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'\n\t\t\t\t\t\t\t\t: '[Compute/def-cache] n/a — reuse disabled or non-reusable definition'\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!result.fromCache) {\n\t\t\t\t\tconst errs = (result.response as { errors?: unknown[] })?.errors;\n\t\t\t\t\tconst errCount = Array.isArray(errs) ? errs.length : 0;\n\t\t\t\t\tif (errCount > 0) {\n\t\t\t\t\t\tconst cacheNote = config.cacheerroredsolves\n\t\t\t\t\t\t\t? 'cached (cacheerroredsolves on)'\n\t\t\t\t\t\t\t: 'not cached (cacheerroredsolves off) — re-run until fixed';\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/rhino-cache] ${errCount} GH error(s); ${cacheNote}. First: ${JSON.stringify((errs as unknown[])[0])}`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { client, scheduler, rhinoTiming, solveMeta };\n\t}\n\n\treturn {\n\t\tasync getClient(server, opts): Promise<CachedClient> {\n\t\t\tconst key = server.id;\n\t\t\tconst existing = cache.get(key);\n\t\t\tif (existing) {\n\t\t\t\t// Refresh LRU position.\n\t\t\t\tcache.delete(key);\n\t\t\t\tcache.set(key, existing);\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst inFlight = pending.get(key);\n\t\t\tif (inFlight) return inFlight;\n\n\t\t\t// A build is a Rhino.Compute handshake — expensive, and repeated builds for\n\t\t\t// the same id mean churn (LRU thrash or config-rotation evictions).\n\t\t\tdebugLog(`[Compute/client-cache] miss — building warm client for server ${key}`);\n\t\t\tconst buildPromise = (async () => {\n\t\t\t\tconst entry = await build(server, opts?.definitionGuid);\n\n\t\t\t\tif (cache.size >= maxWarmComputeServers) {\n\t\t\t\t\tconst oldestKey = cache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/client-cache] LRU evicted warm client for server ${oldestKey} (cap ${maxWarmComputeServers})`\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcache.get(oldestKey)?.scheduler.dispose();\n\t\t\t\t\t\tcache.delete(oldestKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcache.set(key, entry);\n\t\t\t\treturn entry;\n\t\t\t})().finally(() => pending.delete(key));\n\t\t\tpending.set(key, buildPromise);\n\t\t\treturn buildPromise;\n\t\t},\n\n\t\tevict(id): void {\n\t\t\tconst key = id as string;\n\t\t\tconst entry = cache.get(key);\n\t\t\tif (entry) {\n\t\t\t\tdebugLog(`[Compute/client-cache] evicted warm client for server ${key} (config change)`);\n\t\t\t\tentry.scheduler.dispose();\n\t\t\t\tcache.delete(key);\n\t\t\t}\n\t\t},\n\n\t\tsolveCacheStats(): SolveCacheStats {\n\t\t\tconst total: SolveCacheStats = {\n\t\t\t\twarmClients: cache.size,\n\t\t\t\tentries: 0,\n\t\t\t\tbytes: 0,\n\t\t\t\thits: 0,\n\t\t\t\tmisses: 0,\n\t\t\t\tevictions: 0\n\t\t\t};\n\t\t\tfor (const entry of cache.values()) {\n\t\t\t\tconst s = entry.scheduler.cacheStats();\n\t\t\t\ttotal.entries += s.entries;\n\t\t\t\ttotal.bytes += s.bytes;\n\t\t\t\ttotal.hits += s.hits;\n\t\t\t\ttotal.misses += s.misses;\n\t\t\t\ttotal.evictions += s.evictions;\n\t\t\t}\n\t\t\treturn total;\n\t\t},\n\n\t\tclearSolveCaches(): void {\n\t\t\tdebugLog(`[Compute/client-cache] cleared solve caches on ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.clearCache();\n\t\t},\n\n\t\tdisposeAll(): void {\n\t\t\tdebugLog(`[Compute/client-cache] disposing all ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.dispose();\n\t\t\tcache.clear();\n\t\t}\n\t};\n}\n","/**\n * In-process cache of `.gh` definition bytes, keyed by immutable **version id**.\n *\n * Turns an eager `storage.get(fileKey)` on every solve request into a lazy\n * `DefinitionRef.load()`: the scheduler calls `load()` only when an upload is\n * unavoidable (a pointer-known re-solve never touches storage or this cache).\n *\n * The key MUST be the version id, never a `fileKey` — a delete-latest-then-\n * reupload can reuse a `fileKey` for different content, which would silently\n * serve one version's bytes for another with no diagnostic. Callers pass\n * `version.id`, and the returned `DefinitionRef.key` is that same id, so the\n * scheduler's own result/pointer cache keys on the same immutable identity.\n *\n * Eviction is LRU by total byte budget, not entry count — definitions range\n * from tens of KB to hundreds of MB, so a count cap would either pin\n * gigabytes or evict uselessly. `COMPUTE_DEFINITION_CACHE_MB` sets the\n * budget (see `createDefinitionByteCache`). No TTL: version ids are\n * immutable, so a cached entry can never go stale. An entry larger than the\n * whole budget is served but never retained.\n */\n\n/**\n * Per-`getOrLoad` outcome, observable after the scheduler has (or hasn't)\n * called `load()` — lets the solve pipeline emit a `def_bytes` Server-Timing\n * verdict (`skipped` / `hit` / `miss`).\n */\nexport interface ByteRefOutcome {\n\tloaded: boolean;\n\tfromCache: boolean;\n}\n\n/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */\nexport interface ByteCacheRef {\n\tkey: string;\n\tload: () => Promise<Uint8Array>;\n\toutcome: ByteRefOutcome;\n}\n\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\tgetOrLoad(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\tclear(): void;\n}\n\ninterface Entry {\n\tbytes: Uint8Array;\n}\n\n/**\n * @param maxBytes total retained-byte budget; `0` (or negative) disables caching\n * entirely — every `load()` calls the loader and nothing is retained.\n */\nexport function createDefinitionByteCache(maxBytes: number): DefinitionByteCache {\n\tconst budget = Math.max(0, Math.floor(maxBytes));\n\t// Map iteration order is insertion order, so it doubles as LRU order:\n\t// re-inserting a key on access moves it to the most-recently-used end.\n\tconst cache = new Map<string, Entry>();\n\tlet retainedBytes = 0;\n\tconst stats = { hits: 0, misses: 0, evictions: 0 };\n\n\tfunction touch(key: string, entry: Entry): void {\n\t\tcache.delete(key);\n\t\tcache.set(key, entry);\n\t}\n\n\tfunction evictToFit(): void {\n\t\twhile (retainedBytes > budget && cache.size > 0) {\n\t\t\tconst oldestKey = cache.keys().next().value as string;\n\t\t\tconst oldest = cache.get(oldestKey)!;\n\t\t\tcache.delete(oldestKey);\n\t\t\tretainedBytes -= oldest.bytes.byteLength;\n\t\t\tstats.evictions += 1;\n\t\t}\n\t}\n\n\tfunction store(key: string, bytes: Uint8Array): void {\n\t\tif (budget === 0) return;\n\t\t// Caching an entry bigger than the budget would evict everything\n\t\t// (including itself) on the next evictToFit — serve it, cache nothing.\n\t\tif (bytes.byteLength > budget) return;\n\t\tconst existing = cache.get(key);\n\t\tif (existing) retainedBytes -= existing.bytes.byteLength;\n\t\tcache.set(key, { bytes });\n\t\tretainedBytes += bytes.byteLength;\n\t\tevictToFit();\n\t}\n\n\treturn {\n\t\tgetOrLoad(versionId, load) {\n\t\t\tconst outcome: ByteRefOutcome = { loaded: false, fromCache: false };\n\t\t\treturn {\n\t\t\t\tkey: versionId,\n\t\t\t\toutcome,\n\t\t\t\tload: async () => {\n\t\t\t\t\tconst hit = cache.get(versionId);\n\t\t\t\t\tif (hit) {\n\t\t\t\t\t\ttouch(versionId, hit);\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn hit.bytes;\n\t\t\t\t\t}\n\t\t\t\t\tstats.misses += 1;\n\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\toutcome.fromCache = false;\n\t\t\t\t\tconst bytes = await load();\n\t\t\t\t\tstore(versionId, bytes);\n\t\t\t\t\treturn bytes;\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tstats() {\n\t\t\treturn {\n\t\t\t\thits: stats.hits,\n\t\t\t\tmisses: stats.misses,\n\t\t\t\tevictions: stats.evictions,\n\t\t\t\tentries: cache.size,\n\t\t\t\tbytes: retainedBytes\n\t\t\t};\n\t\t},\n\t\tclear() {\n\t\t\tcache.clear();\n\t\t\tretainedBytes = 0;\n\t\t}\n\t};\n}\n","/**\n * In-process single-flight — dogpile protection for every solve.\n *\n * Always on, not gated behind a result cache being configured: the dogpile is worst\n * when nothing else is caching, since N concurrent identical solves each pay a full\n * Rhino round trip (the scheduler doesn't coalesce in-flight requests itself, and\n * Rhino's `cachesolve` still costs a round trip per repeat). An earlier version\n * gated this on cache config and so disabled it in exactly the deployments most\n * exposed to a cold-key stampede — hot public definition plus a deploy.\n *\n * Per app instance, above `ISolveResultCache`. No cross-instance lease (Redis\n * `SET NX`) yet.\n *\n * The shared promise resolves to one value for every waiter, so `work` must return\n * something safe to share by reference — the solve pipeline's envelope qualifies\n * because the app serializes it per response.\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Coalesces concurrent calls under the same key: the first caller runs `work`,\n\t * later callers for that key await the same promise, and the key frees as soon\n\t * as it settles.\n\t *\n\t * `onWaiterJoined` fires only on the owner's call, only while `work` is still\n\t * running, each time another caller joins. Ownership changes what the owner's\n\t * abort signal should mean: a solo run can cancel on its own client's\n\t * disconnect, but a shared run can't without 499-ing every waiter.\n\t */\n\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T>;\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\tonJoin?: (key: string) => void;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\tinterface Flight {\n\t\tpromise: Promise<unknown>;\n\t\tnotifyOwner?: () => void;\n\t}\n\tconst inflight = new Map<string, Flight>();\n\n\treturn {\n\t\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T> {\n\t\t\tconst existing = inflight.get(key);\n\t\t\tif (existing) {\n\t\t\t\toptions.onJoin?.(key);\n\t\t\t\texisting.notifyOwner?.();\n\t\t\t\treturn existing.promise as Promise<T>;\n\t\t\t}\n\n\t\t\t// Wrapping in an async IIFE means a synchronous throw in `work` still\n\t\t\t// rejects `p` instead of escaping `run` directly.\n\t\t\tconst flight: Flight = { notifyOwner: onWaiterJoined, promise: undefined! };\n\t\t\tconst p = (async () => work())().finally(() => {\n\t\t\t\t// Clear it here so a join after `work` settles can't fire it late.\n\t\t\t\tflight.notifyOwner = undefined;\n\t\t\t\t// Guard against deleting a key a later caller has already re-taken.\n\t\t\t\tif (inflight.get(key) === flight) inflight.delete(key);\n\t\t\t});\n\t\t\tflight.promise = p;\n\t\t\tinflight.set(key, flight);\n\t\t\treturn p;\n\t\t},\n\t\tinFlight(): number {\n\t\t\treturn inflight.size;\n\t\t}\n\t};\n}\n","/**\n * Facade over the warm-client cache, definition-byte cache, single-flight\n * coalescing, and the solve pipeline — the primitives an app route would\n * otherwise wire up by hand.\n *\n * Doesn't read env itself: `@selvajs/server` owns `resolveComputeLimits` and\n * depends on `@selvajs/solve`, so calling it from here would be a circular\n * import. Callers resolve their own limits and pass in the fields below.\n *\n * Stays out of auth, DB reads, share tokens, rate limiting, metrics, and\n * compute-server selection — all app policy, supplied per-call via\n * `SolveEngineSolveArgs.server`.\n */\n\nimport {\n\tisDefinitionRef,\n\tstableStringify,\n\ttype DefinitionRef,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport { NoopLogger, type ILogger } from '@selvajs/platform';\nimport {\n\tcreateClientCache,\n\ttype CachedClient,\n\ttype ClientCache,\n\ttype ClientCacheDebug,\n\ttype ResolvedServer,\n\ttype SolveCacheStats\n} from './client-cache.js';\nimport {\n\tcreateDefinitionByteCache,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype DefinitionByteCache\n} from './definition-byte-cache.js';\nimport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight\n} from './solve-cache-single-flight.js';\nimport {\n\tadaptEnvelopeToEncoding,\n\tbuildSolveInputTree,\n\trunSolvePipeline,\n\ttype PipelineInput,\n\ttype SolveOutcome\n} from './solve-pipeline.js';\n\n/**\n * Subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs.\n * Pass the resolved object through as-is — extra fields are ignored.\n */\nexport interface SolveEngineLimits {\n\tsolveDeadlineMs: number;\n\tcomputeResponseMaxBytes: number;\n\tcomputeReuseDefinitionCache: boolean;\n\tcomputeServerCachesolve: boolean;\n\tcomputeCacheErroredSolves: boolean;\n\tcomputeMaxQueueDepth: number;\n\tcomputeQueueWaitMs: number;\n\tcomputeDefinitionCacheBytes: number;\n\tcomputeSolveCacheBytes: number;\n}\n\nexport interface SolveEngineOptions {\n\tlimits: SolveEngineLimits;\n\tlogger?: ILogger;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */\n\tmaxWarmComputeServers?: number;\n\t/** Concise cache/timing logs when truthy; `'verbose'` also dumps full lib-level requests/responses. Default off. */\n\tdebug?: ClientCacheDebug;\n\tonDebugLog?: (message: string) => void;\n\t/** Fired when a caller joins an already-in-flight solve instead of running its own. */\n\tonSolveCoalesced?: (key: string) => void;\n}\n\n/**\n * Accepts every form `runSolvePipeline` does, plus `{versionId, load}` sugar\n * that builds (and caches) a `ByteCacheRef` internally. A `ByteCacheRef`\n * obtained ahead of time from `engine.definitionRef()` (e.g. to read bytes for\n * schema extraction before solving) is passed through as-is — detected by its\n * `outcome` field, which a plain `DefinitionRef` never has.\n */\nexport type SolveEngineDefinitionSource =\n\t| Uint8Array\n\t| string\n\t| DefinitionRef\n\t| ByteCacheRef\n\t| { versionId: string; load: () => Promise<Uint8Array> };\n\nexport interface SolveEngineSolveArgs {\n\tserver: ResolvedServer;\n\tdefinitionSource: SolveEngineDefinitionSource;\n\t/**\n\t * Coalesce-key identity for a raw `Uint8Array`/`string` source, which has\n\t * no natural identity the way a `DefinitionRef`'s `.key` does. Required in\n\t * that case — `solve()` throws rather than silently hashing the bytes.\n\t * Ignored for the other source forms.\n\t */\n\tdefinitionKey?: string;\n\tinputs: PipelineInput[];\n\tvalues: Record<string, unknown>;\n\tsignal: AbortSignal;\n\tacceptEncoding?: string;\n\t/** Stamped as `X-Selva-Definition` on this solve's client. */\n\tdefinitionGuid?: string;\n\tloadStartMs?: number;\n\tdefLoadMs?: number;\n\tprepMarks?: [string, number][];\n}\n\nexport interface FrameworkAgnosticResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string | Uint8Array;\n}\n\nexport interface SolveEngineStats {\n\tclient: SolveCacheStats;\n\tdefinitionBytes: ByteCacheStats;\n\tcoalescing: { inFlight: number };\n}\n\nfunction isByteCacheRef(source: SolveEngineDefinitionSource): source is ByteCacheRef {\n\treturn (\n\t\ttypeof source === 'object' &&\n\t\tsource !== null &&\n\t\t!(source instanceof Uint8Array) &&\n\t\t'outcome' in source\n\t);\n}\n\nfunction isVersionLoadPair(\n\tsource: SolveEngineDefinitionSource\n): source is { versionId: string; load: () => Promise<Uint8Array> } {\n\treturn (\n\t\ttypeof source === 'object' &&\n\t\tsource !== null &&\n\t\t!(source instanceof Uint8Array) &&\n\t\t!('outcome' in source) &&\n\t\t!('key' in source) &&\n\t\t'versionId' in source\n\t);\n}\n\nexport class SolveEngine {\n\tprivate readonly limits: SolveEngineLimits;\n\tprivate readonly clientCache: ClientCache;\n\tprivate readonly byteCache: DefinitionByteCache;\n\tprivate readonly singleFlight: SolveCacheSingleFlight;\n\n\tconstructor(options: SolveEngineOptions) {\n\t\tthis.limits = options.limits;\n\t\tconst logger = options.logger ?? new NoopLogger();\n\n\t\tthis.clientCache = createClientCache({\n\t\t\tsolveDeadlineMs: this.limits.solveDeadlineMs,\n\t\t\tcachesolve: this.limits.computeServerCachesolve,\n\t\t\tcacheerroredsolves: this.limits.computeCacheErroredSolves,\n\t\t\treuseServerDefinitionCache: this.limits.computeReuseDefinitionCache,\n\t\t\tmaxQueueDepth: this.limits.computeMaxQueueDepth,\n\t\t\tqueueWaitMs: this.limits.computeQueueWaitMs,\n\t\t\tresponseCacheMaxBytes: this.limits.computeSolveCacheBytes,\n\t\t\tdebug: options.debug ?? false,\n\t\t\tmaxWarmComputeServers: options.maxWarmComputeServers,\n\t\t\tonDebugLog:\n\t\t\t\toptions.onDebugLog ??\n\t\t\t\t((message) => logger.debug(message, { component: 'Compute/client-cache' }))\n\t\t});\n\n\t\tthis.byteCache = createDefinitionByteCache(this.limits.computeDefinitionCacheBytes);\n\n\t\tthis.singleFlight = createSolveCacheSingleFlight({\n\t\t\tonJoin: options.onSolveCoalesced\n\t\t});\n\t}\n\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient> {\n\t\treturn this.clientCache.getClient(server, opts);\n\t}\n\n\tdefinitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef {\n\t\treturn this.byteCache.getOrLoad(versionId, load);\n\t}\n\n\tevictServer(id: string): void {\n\t\tthis.clientCache.evict(id);\n\t}\n\n\tclearSolveCaches(): void {\n\t\tthis.clientCache.clearSolveCaches();\n\t}\n\n\tstats(): SolveEngineStats {\n\t\treturn {\n\t\t\tclient: this.clientCache.solveCacheStats(),\n\t\t\tdefinitionBytes: this.byteCache.stats(),\n\t\t\tcoalescing: { inFlight: this.singleFlight.inFlight() }\n\t\t};\n\t}\n\n\tasync solve(args: SolveEngineSolveArgs): Promise<SolveOutcome> {\n\t\tconst {\n\t\t\tdefinitionSource: resolvedSource,\n\t\t\tbyteRefOutcome,\n\t\t\tidentity\n\t\t} = this.resolveDefinitionSource(args);\n\n\t\tconst client = await this.clientCache.getClient(args.server, {\n\t\t\tdefinitionGuid: args.definitionGuid\n\t\t});\n\t\tconst inputTree = buildSolveInputTree(args.inputs, args.values);\n\t\tconst coalesceKey = `${identity}:${args.server.id}:${stableStringify(inputTree)}`;\n\n\t\t// A coalesced solve is shared, so one caller's disconnect can't 499 every\n\t\t// other waiter. Only propagate abort if no one else has joined yet.\n\t\tconst abortController = new AbortController();\n\t\tlet hasWaiters = false;\n\t\tconst onCallerAbort = () => {\n\t\t\tif (!hasWaiters) abortController.abort();\n\t\t};\n\t\t// `once: true` doesn't detach on a normal completion, and the caller's signal\n\t\t// outlives this call — a request signal fires `abort` after the response is\n\t\t// consumed, and a session-scoped one never settles at all. Without the\n\t\t// `finally` below, every solve leaks a listener onto it.\n\t\targs.signal.addEventListener('abort', onCallerAbort, { once: true });\n\n\t\tconst acceptEncoding = args.acceptEncoding ?? '';\n\t\tlet outcome: SolveOutcome;\n\t\ttry {\n\t\t\toutcome = await this.singleFlight.run(\n\t\t\t\tcoalesceKey,\n\t\t\t\t() =>\n\t\t\t\t\trunSolvePipeline({\n\t\t\t\t\t\tdefinitionSource: resolvedSource,\n\t\t\t\t\t\tbyteRefOutcome,\n\t\t\t\t\t\tinputs: args.inputs,\n\t\t\t\t\t\tvalues: args.values,\n\t\t\t\t\t\tinputTree,\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tresponseMaxBytes: this.limits.computeResponseMaxBytes,\n\t\t\t\t\t\tsolveDeadlineMs: this.limits.solveDeadlineMs,\n\t\t\t\t\t\tacceptEncoding,\n\t\t\t\t\t\tsignal: abortController.signal,\n\t\t\t\t\t\tloadStartMs: args.loadStartMs ?? performance.now(),\n\t\t\t\t\t\tdefLoadMs: args.defLoadMs ?? 0,\n\t\t\t\t\t\tprepMarks: args.prepMarks\n\t\t\t\t\t}),\n\t\t\t\t() => {\n\t\t\t\t\thasWaiters = true;\n\t\t\t\t}\n\t\t\t);\n\t\t} finally {\n\t\t\targs.signal.removeEventListener('abort', onCallerAbort);\n\t\t}\n\n\t\tif (outcome.kind !== 'ok') return outcome;\n\n\t\t// The envelope may be shared across waiters; re-key it to this caller's\n\t\t// own Accept-Encoding so each one gets the right wire form.\n\t\tconst wire = adaptEnvelopeToEncoding(outcome.envelope, acceptEncoding);\n\t\treturn {\n\t\t\t...outcome,\n\t\t\tenvelope: { ...outcome.envelope, body: wire.body, headers: wire.headers }\n\t\t};\n\t}\n\n\ttoResponse(\n\t\toutcome: SolveOutcome,\n\t\topts?: { onError?: (status: number, body: { message: string; retryAfter?: number }) => never }\n\t): FrameworkAgnosticResponse {\n\t\tconst fail = (\n\t\t\tstatus: number,\n\t\t\tbody: { message: string; retryAfter?: number }\n\t\t): FrameworkAgnosticResponse => {\n\t\t\tif (opts?.onError) opts.onError(status, body);\n\t\t\tconst headers: Record<string, string> = { 'Content-Type': 'application/json' };\n\t\t\tif (body.retryAfter !== undefined) headers['Retry-After'] = String(body.retryAfter);\n\t\t\treturn { status, headers, body: JSON.stringify(body) };\n\t\t};\n\n\t\tswitch (outcome.kind) {\n\t\t\tcase 'ok':\n\t\t\t\treturn { status: 200, headers: outcome.envelope.headers, body: outcome.envelope.body };\n\t\t\tcase 'timeout':\n\t\t\t\treturn fail(504, { message: outcome.message });\n\t\t\tcase 'client_abort':\n\t\t\t\treturn fail(499, { message: 'Client closed request' });\n\t\t\tcase 'too_large':\n\t\t\t\treturn fail(413, {\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'Solve result is too large to return. This usually means a file output exceeds the supported size.'\n\t\t\t\t});\n\t\t\tcase 'shed':\n\t\t\t\treturn fail(503, { message: outcome.message, retryAfter: outcome.retryAfterSeconds });\n\t\t\tcase 'compute_error':\n\t\t\t\tthrow outcome.error;\n\t\t}\n\t}\n\n\ttoWebResponse(outcome: SolveOutcome): Response {\n\t\tconst r = this.toResponse(outcome);\n\t\treturn new Response(typeof r.body === 'string' ? r.body : new Uint8Array(r.body), {\n\t\t\tstatus: r.status,\n\t\t\theaders: r.headers\n\t\t});\n\t}\n\n\tprivate resolveDefinitionSource(args: SolveEngineSolveArgs): {\n\t\tdefinitionSource: SolveDefinition;\n\t\tbyteRefOutcome?: ByteCacheRef['outcome'];\n\t\tidentity: string;\n\t} {\n\t\tconst source = args.definitionSource;\n\n\t\tif (isVersionLoadPair(source)) {\n\t\t\tconst ref = this.byteCache.getOrLoad(source.versionId, source.load);\n\t\t\treturn { definitionSource: ref, byteRefOutcome: ref.outcome, identity: ref.key };\n\t\t}\n\n\t\tif (isByteCacheRef(source)) {\n\t\t\treturn { definitionSource: source, byteRefOutcome: source.outcome, identity: source.key };\n\t\t}\n\n\t\tif (isDefinitionRef(source)) {\n\t\t\treturn { definitionSource: source, identity: source.key };\n\t\t}\n\n\t\t// Raw Uint8Array | string — no natural identity; require the caller's own.\n\t\tif (!args.definitionKey) {\n\t\t\tthrow new Error(\n\t\t\t\t'SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.'\n\t\t\t);\n\t\t}\n\t\treturn { definitionSource: source, identity: args.definitionKey };\n\t}\n}\n","// ============================================================================\n// server/ — the solve core behind the wire\n// ============================================================================\n//\n// Given a resolved solve context (definition bytes, inputs + values, a warm scheduler), runs a\n// solve and returns a ready-to-send envelope: build input tree → solve → serialize → gzip →\n// Server-Timing, plus the caches that make a repeat solve cheap (single-flight coalescing,\n// definition-byte cache, Rhino.Compute client cache).\n//\n// **Node-only** (`node:zlib`, `node:crypto`, `process.env`, platform-provider cache backends).\n// `client/` must never import from here — enforced by `no-restricted-imports` in\n// `eslint.config.mjs`, and on the built artifact by having no root barrel join the two halves.\n//\n// Not here, on purpose: HTTP status mapping, auth, tenancy, rate limiting, SSRF guards. Those are\n// request policy and stay in `@selvajs/server` (`/compute`, `/http`) and in the app route.\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\nexport {\n\trunSolvePipeline,\n\tbuildSolveInputTree,\n\tadaptEnvelopeToEncoding,\n\tCOMPUTE_CONTRACT_VERSION,\n\tCOMPUTE_VERSION_HEADER,\n\ttype SolvePipelineArgs,\n\ttype SolveOutcome,\n\ttype SolveEnvelope,\n\ttype SolvePhaseMetrics,\n\ttype PipelineInput\n} from './solve-pipeline.js';\n\nexport { transformInputParameter } from './transform-input.js';\n\n// ============================================================================\n// Caches\n// ============================================================================\n\nexport {\n\tcreateClientCache,\n\tserverIdentity,\n\ttype ClientCache,\n\ttype ClientCacheConfig,\n\ttype SolveCacheStats,\n\ttype CachedClient,\n\ttype ResolvedServer,\n\ttype ServerIdentity\n} from './client-cache.js';\n\nexport {\n\tcreateDefinitionByteCache,\n\ttype DefinitionByteCache,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype ByteRefOutcome\n} from './definition-byte-cache.js';\n\nexport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight,\n\ttype SolveCacheSingleFlightOptions\n} from './solve-cache-single-flight.js';\n\n// ============================================================================\n// Facade\n// ============================================================================\n\nexport {\n\tSolveEngine,\n\ttype SolveEngineOptions,\n\ttype SolveEngineLimits,\n\ttype SolveEngineSolveArgs,\n\ttype SolveEngineDefinitionSource,\n\ttype SolveEngineStats,\n\ttype FrameworkAgnosticResponse\n} from './solve-engine.js';\n\nexport type { DefinitionRef, SolveDefinition } from '@selvajs/compute';\nexport { isDefinitionRef } from '@selvajs/compute';\n\n// ============================================================================\n// Shared vocabulary\n// ============================================================================\n//\n// Re-exported so a server-only consumer needs one import path, matching `client/`.\n\nexport type { SolveInput } from '../shared/solve-input.js';\n"],"mappings":"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","names":["ErrorCodes","RhinoComputeError","TreeBuilder","gzip","gunzipSync","promisify","processInput","transformInputParameter","input","value","raw","processed","effective","gzipAsync","promisify","gzip","COMPUTE_CONTRACT_VERSION","COMPUTE_VERSION_HEADER","buildSolveInputTree","inputs","values","TreeBuilder","input","transformInputParameter","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","onRhinoMs","computeLinkMs","label","ms","byteRef","verdict","GrasshopperClient","enableDebugLogging","serverIdentity","server","DEFAULT_MAX_WARM_COMPUTE_SERVERS","FALLBACK_CONCURRENCY","CONCURRENCY_PROBE_INTERVAL_MS","createClientCache","config","maxWarmComputeServers","cache","pending","debugLog","message","build","definitionGuid","rhinoTiming","solveMeta","debugVerbose","clientConfig","t","decode","solve","encode","defCache","solveCache","client","maxConcurrent","lastProbeAt","probing","refreshConcurrency","scheduler","children","next","_ctx","result","errs","errCount","cacheNote","opts","key","existing","inFlight","buildPromise","entry","oldestKey","id","total","s","createDefinitionByteCache","maxBytes","budget","cache","retainedBytes","stats","touch","key","entry","evictToFit","oldestKey","oldest","store","bytes","existing","versionId","load","outcome","hit","createSolveCacheSingleFlight","options","inflight","key","work","onWaiterJoined","existing","flight","p","isDefinitionRef","stableStringify","NoopLogger","isByteCacheRef","source","isVersionLoadPair","SolveEngine","options","logger","NoopLogger","createClientCache","message","createDefinitionByteCache","createSolveCacheSingleFlight","server","opts","versionId","load","id","args","resolvedSource","byteRefOutcome","identity","client","inputTree","buildSolveInputTree","coalesceKey","stableStringify","abortController","hasWaiters","onCallerAbort","acceptEncoding","outcome","runSolvePipeline","wire","adaptEnvelopeToEncoding","fail","status","body","headers","r","ref","isDefinitionRef","isDefinitionRef"]}
|
|
1
|
+
{"version":3,"sources":["../src/server/solve-pipeline.ts","../src/server/transform-input.ts","../src/server/client-cache.ts","../src/server/definition-byte-cache.ts","../src/server/solve-cache-single-flight.ts","../src/server/solve-engine.ts","../src/server/index.ts"],"sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * solve and returns a discriminated {@link SolveOutcome} instead of throwing\n * for expected failures. Auth, the database, share tokens, rate limits, and\n * metric sinks are the calling route's job, not this file's.\n */\n\nimport { ErrorCodes, ComputeError, type SolveDefinition } from '@selvajs/compute/core';\nimport {\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam\n} from '@selvajs/compute/grasshopper';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n// gzipSync blocks the event loop for multi-MB bodies; promisify(gzip) doesn't.\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler\n\t * materializes only when an upload is unavoidable — a pointer-known solve\n\t * moves zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/** Mutable load outcome for a `DefinitionRef` source, so the `def_bytes` Server-Timing verdict can be emitted. Omit for raw-bytes solves. */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}; skips the\n\t * pipeline's own build and solves this object directly.\n\t *\n\t * A caller coalescing concurrent solves needs this: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Only phrases the timeout message — the scheduler enforces the deadline itself. */\n\tsolveDeadlineMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Forwarded to the scheduler so a client disconnect cancels the upstream\n\t * compute call. Its `aborted` flag also distinguishes a client disconnect\n\t * from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/** Wall-clock origin (`performance.now()` at the top of the request) so `load`/`total` phase timings line up with the caller's pre-solve prep. */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/** Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*` Server-Timing entries. */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * `ok` carries the envelope; every other variant names an expected failure the\n * calling route maps to a status code (timeout→504, client_abort→499,\n * too_large→413, shed→503+Retry-After, compute_error→generic 500/503).\n * `durationMs` on error variants is the solve wall time up to the failure, for\n * the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/** Scheduler backpressure rejected the solve before it ran: queue was full (`queue_full`) or the wait exceeded the deadline (`queue_timeout`). Retryable — `retryAfterSeconds` is a backoff hint. */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/** The transformed input tree exactly as handed to the scheduler; `runSolvePipeline` calls this itself unless the caller supplies {@link SolvePipelineArgs.inputTree}. */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// client.solveMeta/rhinoTiming live on the warm client and are written by\n\t// every request on this server; with scheduler maxConcurrent > 1, another\n\t// request can write them mid-flight. A slot is attributed to THIS request\n\t// only if exactly one write happened since this snapshot — for solveMeta\n\t// that single settle is necessarily ours, since onSettle fires before\n\t// scheduler.solve() resolves. Anything ambiguous is dropped from\n\t// Server-Timing rather than misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// AbortError with the request signal NOT aborted means the scheduler's own\n\t\t// deadline timer fired, not a client disconnect.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.solveDeadlineMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// Queue-full or queue-timeout rejects the solve before compute runs — a load\n\t\t// signal, not a failure, so it's `shed` rather than `compute_error`.\n\t\tif (err instanceof ComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a string at ~512 MB; an oversized `file` output trips a RangeError\n\t\t// here. Treat it the same as the byte cap: too_large.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// Buffered, not streamed, so Content-Length is known upfront and a connection\n\t// cut mid-transfer fails hard instead of silently truncating the JSON. Below\n\t// 1 KB gzip isn't worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// rhino is only meaningful when settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// Content-Encoding tells a downstream proxy the body is already compressed,\n\t\t// so it won't gzip it again.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope},\n * baked from the FIRST caller's `Accept-Encoding`. A later waiter with a\n * different `Accept-Encoding` would otherwise get a body labelled with the\n * wrong encoding — `Vary` can't help here, since it's one shared object, not a\n * cache lookup. Encoding is deliberately left out of the coalesce key so mixed\n * clients still coalesce; this adapts the shared result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// Only mismatch that corrupts the response: gzip body, non-gzip waiter.\n\t// Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * `total` is the server's headers-to-out wall time, so browser `ttfb − total`\n * ≈ network+send latency. When the compute server reports its own\n * decode/solve/encode (VektorNode fork), `rhino_*` is time on the compute\n * server and `compute_link` is everything between it (network + queue wait).\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// skipped = scheduler never materialized bytes (pointer-known solve, the\n\t// whole point of this path); hit = warm byte cache; miss = fell through to\n\t// storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute/grasshopper';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * `SchemaInput` has no `values`/`acceptedFormats` (those live on the layout item's config), so\n * valueList/file inputs here carry just the selected value with no option list.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// processInput has no dynamicValueList handler — the plugin's GetDynamicValueListParameter\n\t\t// reuses TypeName \"ValueList\" on the wire, so map it here or it falls back to Geometry with\n\t\t// a null default and TreeBuilder drops the input (Grasshopper solves on a stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// processInput drops a default in two cases we need to override, or the input is omitted from\n\t// the solve and the user's selection never reaches Grasshopper: a plain array default (e.g. a\n\t// dynamic value list checklist) is rejected as malformed; and ValueList drops a scalar default\n\t// it can't validate against a `values` map — which SchemaInput never carries, so every app\n\t// value-list selection hits this path.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Restoring for other types would mean shipping a value the package rejected as\n\t\t\t// genuinely unparseable — scoped to ValueList only.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n","import {\n\tGrasshopperClient,\n\ttype GrasshopperComputeConfig,\n\ttype SolveScheduler\n} from '@selvajs/compute/grasshopper';\nimport { enableDebugLogging } from '@selvajs/compute/core';\n\n// ============================================================================\n// Shared compute client cache\n// ============================================================================\n//\n// One warm `GrasshopperClient` (+ its `SolveScheduler`) per distinct compute\n// server, keyed by the server's `id`, never its URL — a rotated URL/apiKey\n// keeps the same key with stale connection details, so the config-write path\n// MUST call `evict(id)` when a server's config changes.\n//\n// Per-server LRU, not a single shared client: definitions can pin different\n// servers, so two definitions on two servers keep two warm clients, with\n// churn from one-off servers bounded by `maxWarmComputeServers`.\n//\n// Both hot paths share this cache — the solve endpoint uses the entry's\n// `scheduler`; the definition-viewer render path uses the entry's `client`\n// for `getIO` — so a definition that was just solved renders from the same\n// warm client instead of re-handshaking Rhino.Compute per page load.\n\n/**\n * Opaque identity of a compute server — construct only via `serverIdentity()`.\n * Kept opaque so \"a server is now a pool of URLs behind one id\" stays additive.\n */\nexport type ServerIdentity = string & { readonly __brand: 'ServerIdentity' };\n\n/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */\nexport interface ResolvedServer {\n\tid: string;\n\tserverUrl: string;\n\t/** Sent as the `RhinoComputeKey` header. */\n\tapiKey?: string;\n}\n\nexport function serverIdentity(server: Pick<ResolvedServer, 'id'>): ServerIdentity {\n\treturn server.id as ServerIdentity;\n}\n\nexport interface CachedClient {\n\tclient: GrasshopperClient;\n\tscheduler: SolveScheduler;\n\t/**\n\t * Last Server-Timing decode/solve/encode, written by `onServerTiming`. The\n\t * scheduler runs up to `maxConcurrent` solves at once, so `last` alone can't\n\t * be trusted per-request: callers snapshot `seq` before their solve and only\n\t * attribute `last` to themselves if exactly one write happened since (see\n\t * the guard in `runSolvePipeline`), dropping it otherwise rather than risk\n\t * misattributing another request's timing.\n\t */\n\trhinoTiming: { last: { decode: number; solve: number; encode: number } | null; seq: number };\n\t/**\n\t * Same snapshot-and-attribute pattern as `rhinoTiming`, for the scheduler's\n\t * `onSettle` cache verdict. `seq` increments on every settle (success or\n\t * error); `last` is written only on success.\n\t */\n\tsolveMeta: {\n\t\tlast: { fromCache: boolean; definitionReuploaded?: boolean } | null;\n\t\tseq: number;\n\t};\n}\n\n/**\n * Debug verbosity: `false` is silent, `true` gives concise cache/timing logs\n * (`onDebugLog`), `'verbose'` also enables full lib-level request/response\n * dumps (incl. geometry) — a superset of `true`, not an independent setting.\n */\nexport type ClientCacheDebug = boolean | 'verbose';\n\n/** Config injected by the consuming app, so this module stays env-agnostic and testable. */\nexport interface ClientCacheConfig {\n\t/** Per-solve timeout forwarded to the scheduler (`ComputeLimits.solveDeadlineMs`). */\n\tsolveDeadlineMs: number;\n\tcachesolve: boolean;\n\t/** Only meaningful with `cachesolve`. */\n\tcacheerroredsolves: boolean;\n\t/** Reference large definitions by server cache key instead of re-uploading. */\n\treuseServerDefinitionCache: boolean;\n\t/**\n\t * Max solves that may wait in the FIFO queue, excluding in-flight solves\n\t * (capped at `maxConcurrent`, itself driven by the server's probed child\n\t * count). `0` = unbounded (`ComputeLimits.computeMaxQueueDepth`); a full\n\t * queue rejects new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Max ms a solve may sit queued before executing; `0` = no deadline\n\t * (`ComputeLimits.computeQueueWaitMs`). Too long a wait rejects with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache. Applies per warm\n\t * client — worst-case heap is this × `maxWarmComputeServers`. `0` disables\n\t * the cache (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\tdebug: ClientCacheDebug;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */\n\tmaxWarmComputeServers?: number;\n\t/** Sink for the debug lines. `onDebugLog` only fires when `debug` is not `false`. */\n\tonDebugLog?: (message: string) => void;\n}\n\nexport interface ClientCache {\n\t/**\n\t * Get (or create) the warm client + scheduler for a resolved compute server,\n\t * keyed by its `id`. `definitionGuid`, when present, is stamped as the\n\t * `X-Selva-Definition` header on this client's outbound solve/IO requests —\n\t * inert routing/telemetry metadata until a pool router exists.\n\t */\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient>;\n\t/** Dispose and drop the warm client for `id`, so the next request rebuilds against fresh connection details. */\n\tevict(id: string | ServerIdentity): void;\n\t/** Solve-cache counters summed across every warm client. Counters die with the client that owns them, so totals can fall over time. */\n\tsolveCacheStats(): SolveCacheStats;\n\t/**\n\t * Drop every retained solve result, keeping the warm clients (and their\n\t * connections and server-side definition pointers) intact. Nothing expires\n\t * these on its own — the byte budget is the only other pressure — so this is\n\t * the operator's release valve when a definition's inputs no longer describe\n\t * its output, e.g. it reads an external source that has since changed.\n\t */\n\tclearSolveCaches(): void;\n\t/** Dispose every warm client. Test seam / shutdown hook. */\n\tdisposeAll(): void;\n}\n\nexport interface SolveCacheStats {\n\twarmClients: number;\n\tentries: number;\n\tbytes: number;\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped under size/byte pressure (not replacement or manual clears). */\n\tevictions: number;\n}\n\nconst DEFAULT_MAX_WARM_COMPUTE_SERVERS = 16;\n\n/**\n * Fallback concurrency while the first probe is still in flight, and the value\n * kept whenever a probe fails or reports 0 — never send more than a single\n * solve to a server whose real capacity is unknown.\n */\nconst FALLBACK_CONCURRENCY = 1;\n\n/**\n * How stale the compute server's child count may get before a solve triggers a\n * re-read. Short enough that a resized pool is picked up within a few solves,\n * long enough that a busy server is probed rarely — the probe rides on solve\n * completion, so an idle server is never polled at all.\n */\nconst CONCURRENCY_PROBE_INTERVAL_MS = 5 * 60 * 1000;\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxWarmComputeServers = config.maxWarmComputeServers ?? DEFAULT_MAX_WARM_COMPUTE_SERVERS;\n\tconst cache = new Map<string, CachedClient>();\n\t// Builds in flight, keyed like `cache`. Concurrent `getClient` calls for the\n\t// same id must share ONE build: without this, both miss, both handshake, and\n\t// the loser's entry is overwritten in the map without ever being disposed.\n\tconst pending = new Map<string, Promise<CachedClient>>();\n\n\tif (config.debug === 'verbose') enableDebugLogging();\n\n\tconst debugLog = (message: string) => {\n\t\tif (config.debug) config.onDebugLog?.(message);\n\t};\n\n\tasync function build(server: ResolvedServer, definitionGuid?: string): Promise<CachedClient> {\n\t\tconst rhinoTiming: CachedClient['rhinoTiming'] = { last: null, seq: 0 };\n\t\tconst solveMeta: CachedClient['solveMeta'] = { last: null, seq: 0 };\n\t\tconst debugVerbose = config.debug === 'verbose';\n\n\t\tconst clientConfig: GrasshopperComputeConfig = {\n\t\t\tserverUrl: server.serverUrl,\n\t\t\tapiKey: server.apiKey,\n\t\t\t// Lib-level debug is the verbose one (full payload logs); keep it on the\n\t\t\t// verbose flag so the concise cache logs can run without the noise.\n\t\t\tdebug: debugVerbose,\n\t\t\tcachesolve: config.cachesolve,\n\t\t\tcacheerroredsolves: config.cacheerroredsolves,\n\t\t\tonServerTiming: (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\trhinoTiming.seq += 1;\n\t\t\t\trhinoTiming.last = { decode, solve, encode };\n\t\t\t\tif (!config.debug) return;\n\t\t\t\t// Heuristic thresholds: a genuine parse/solve is many ms; a cache hit\n\t\t\t\t// is 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 =\n\t\t\t\t\tconfig.cachesolve && solve <= 1\n\t\t\t\t\t\t? 'HIT (cached result, solve skipped)'\n\t\t\t\t\t\t: config.cachesolve\n\t\t\t\t\t\t\t? 'miss (solved fresh)'\n\t\t\t\t\t\t\t: 'off';\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}`\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\n\t\t// Baked at client-create time because the scheduler has no per-request\n\t\t// header hook.\n\t\tif (definitionGuid) {\n\t\t\tclientConfig.headers = { 'X-Selva-Definition': definitionGuid };\n\t\t}\n\n\t\tconst client = await GrasshopperClient.create(clientConfig);\n\n\t\t// rhino.compute hands out children in strict rotation and never reports \"busy\",\n\t\t// so nothing downstream enforces a concurrency cap — send more than it has\n\t\t// children and requests silently double up on one. Rhino.Compute is the only\n\t\t// source of truth for its own child count, so this always probes rather than\n\t\t// trusting an operator-supplied guess.\n\t\t//\n\t\t// The count is not static: children exit on their own under `-idlespan`, and\n\t\t// `/launch-child` + `/recycle-children` resize the pool at runtime. So the probe\n\t\t// re-runs after solves (see `refreshConcurrency`) rather than only at connect.\n\t\tlet maxConcurrent = FALLBACK_CONCURRENCY;\n\t\tlet lastProbeAt = 0;\n\t\tlet probing = false;\n\n\t\t/**\n\t\t * Best-effort re-read of the server's child count. Never awaited by a caller:\n\t\t * a probe must not add a round-trip to the solve path, so a changed count takes\n\t\t * effect on the NEXT solve rather than the one that triggered the probe.\n\t\t *\n\t\t * `initialize: false` keeps this from spawning children on an idle server —\n\t\t * which also means a cold server honestly reports 0. Both a failed probe and a\n\t\t * 0 result fall back to `FALLBACK_CONCURRENCY` (1): an unknown-capacity server\n\t\t * is safer under-used than oversent.\n\t\t */\n\t\tasync function refreshConcurrency(scheduler?: { setMaxConcurrent(n: number): void }) {\n\t\t\tif (probing) return;\n\t\t\tprobing = true;\n\t\t\ttry {\n\t\t\t\tconst children = await Promise.resolve(\n\t\t\t\t\tclient.serverStats?.getActiveChildren({ initialize: false })\n\t\t\t\t).catch(() => null);\n\t\t\t\tlastProbeAt = Date.now();\n\t\t\t\tconst next = !children || children <= 0 ? FALLBACK_CONCURRENCY : children;\n\t\t\t\tif (next === maxConcurrent) return;\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/client-cache] concurrency ${maxConcurrent} → ${next} (server child count)`\n\t\t\t\t);\n\t\t\t\tmaxConcurrent = next;\n\t\t\t\tscheduler?.setMaxConcurrent(next);\n\t\t\t} finally {\n\t\t\t\tprobing = false;\n\t\t\t}\n\t\t}\n\n\t\t// One blocking probe at connect so the first solves already use the right cap.\n\t\tawait refreshConcurrency();\n\n\t\tconst scheduler = client.createScheduler({\n\t\t\tmode: 'queue',\n\t\t\t// Queue mode defaults maxConcurrent to 1, serializing every solve on\n\t\t\t// this server through a single slot while the compute VM's other\n\t\t\t// compute.geometry children idle — always pass it explicitly.\n\t\t\tmaxConcurrent,\n\t\t\t// The scheduler treats undefined as unbounded/no-deadline, so map our\n\t\t\t// `0`-means-off convention to undefined rather than passing 0 (which\n\t\t\t// would reject EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: config.solveDeadlineMs,\n\t\t\t// The byte budget is the only eviction pressure. A solve is a pure\n\t\t\t// function of (definition, inputs), both immutable, so a retained\n\t\t\t// result can never go stale — expiring one only forces a paid re-solve\n\t\t\t// of the identical answer.\n\t\t\tcache: config.responseCacheMaxBytes > 0 ? { maxBytes: config.responseCacheMaxBytes } : false,\n\t\t\treuseServerDefinitionCache: config.reuseServerDefinitionCache,\n\t\t\tonSettle: (_ctx, result) => {\n\t\t\t\tsolveMeta.seq += 1;\n\t\t\t\tif (result.status !== 'success') return;\n\t\t\t\t// Piggyback the pool re-read on real traffic: only servers actually\n\t\t\t\t// being used get probed, at most once per window, and never on the\n\t\t\t\t// request path. A cache hit means we never reached the server, so it\n\t\t\t\t// says nothing about the pool — don't spend a probe on it.\n\t\t\t\tif (\n\t\t\t\t\tresult.fromCache !== true &&\n\t\t\t\t\tDate.now() - lastProbeAt >= CONCURRENCY_PROBE_INTERVAL_MS\n\t\t\t\t) {\n\t\t\t\t\t// Safe despite being referenced from inside its own initializer:\n\t\t\t\t\t// onSettle only ever fires during a solve, long after binding.\n\t\t\t\t\tvoid refreshConcurrency(scheduler);\n\t\t\t\t}\n\t\t\t\tsolveMeta.last = {\n\t\t\t\t\tfromCache: result.fromCache === true,\n\t\t\t\t\tdefinitionReuploaded: result.definitionReuploaded\n\t\t\t\t};\n\t\t\t\tif (!config.debug) return;\n\t\t\t\tdebugLog(\n\t\t\t\t\t`[Compute/selva-cache] ${result.fromCache ? 'HIT — served from cache (no compute call)' : 'miss — went to Rhino.Compute'} (${Math.round(result.durationMs)}ms)`\n\t\t\t\t);\n\t\t\t\tif (!result.fromCache) {\n\t\t\t\t\tdebugLog(\n\t\t\t\t\t\tresult.definitionReuploaded === true\n\t\t\t\t\t\t\t? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'\n\t\t\t\t\t\t\t: result.definitionReuploaded === false\n\t\t\t\t\t\t\t\t? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'\n\t\t\t\t\t\t\t\t: '[Compute/def-cache] n/a — reuse disabled or non-reusable definition'\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!result.fromCache) {\n\t\t\t\t\tconst errs = (result.response as { errors?: unknown[] })?.errors;\n\t\t\t\t\tconst errCount = Array.isArray(errs) ? errs.length : 0;\n\t\t\t\t\tif (errCount > 0) {\n\t\t\t\t\t\tconst cacheNote = config.cacheerroredsolves\n\t\t\t\t\t\t\t? 'cached (cacheerroredsolves on)'\n\t\t\t\t\t\t\t: 'not cached (cacheerroredsolves off) — re-run until fixed';\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/rhino-cache] ${errCount} GH error(s); ${cacheNote}. First: ${JSON.stringify((errs as unknown[])[0])}`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { client, scheduler, rhinoTiming, solveMeta };\n\t}\n\n\treturn {\n\t\tasync getClient(server, opts): Promise<CachedClient> {\n\t\t\tconst key = server.id;\n\t\t\tconst existing = cache.get(key);\n\t\t\tif (existing) {\n\t\t\t\t// Refresh LRU position.\n\t\t\t\tcache.delete(key);\n\t\t\t\tcache.set(key, existing);\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst inFlight = pending.get(key);\n\t\t\tif (inFlight) return inFlight;\n\n\t\t\t// A build is a Rhino.Compute handshake — expensive, and repeated builds for\n\t\t\t// the same id mean churn (LRU thrash or config-rotation evictions).\n\t\t\tdebugLog(`[Compute/client-cache] miss — building warm client for server ${key}`);\n\t\t\tconst buildPromise = (async () => {\n\t\t\t\tconst entry = await build(server, opts?.definitionGuid);\n\n\t\t\t\tif (cache.size >= maxWarmComputeServers) {\n\t\t\t\t\tconst oldestKey = cache.keys().next().value;\n\t\t\t\t\tif (oldestKey !== undefined) {\n\t\t\t\t\t\tdebugLog(\n\t\t\t\t\t\t\t`[Compute/client-cache] LRU evicted warm client for server ${oldestKey} (cap ${maxWarmComputeServers})`\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcache.get(oldestKey)?.scheduler.dispose();\n\t\t\t\t\t\tcache.delete(oldestKey);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcache.set(key, entry);\n\t\t\t\treturn entry;\n\t\t\t})().finally(() => pending.delete(key));\n\t\t\tpending.set(key, buildPromise);\n\t\t\treturn buildPromise;\n\t\t},\n\n\t\tevict(id): void {\n\t\t\tconst key = id as string;\n\t\t\tconst entry = cache.get(key);\n\t\t\tif (entry) {\n\t\t\t\tdebugLog(`[Compute/client-cache] evicted warm client for server ${key} (config change)`);\n\t\t\t\tentry.scheduler.dispose();\n\t\t\t\tcache.delete(key);\n\t\t\t}\n\t\t},\n\n\t\tsolveCacheStats(): SolveCacheStats {\n\t\t\tconst total: SolveCacheStats = {\n\t\t\t\twarmClients: cache.size,\n\t\t\t\tentries: 0,\n\t\t\t\tbytes: 0,\n\t\t\t\thits: 0,\n\t\t\t\tmisses: 0,\n\t\t\t\tevictions: 0\n\t\t\t};\n\t\t\tfor (const entry of cache.values()) {\n\t\t\t\tconst s = entry.scheduler.cacheStats();\n\t\t\t\ttotal.entries += s.entries;\n\t\t\t\ttotal.bytes += s.bytes;\n\t\t\t\ttotal.hits += s.hits;\n\t\t\t\ttotal.misses += s.misses;\n\t\t\t\ttotal.evictions += s.evictions;\n\t\t\t}\n\t\t\treturn total;\n\t\t},\n\n\t\tclearSolveCaches(): void {\n\t\t\tdebugLog(`[Compute/client-cache] cleared solve caches on ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.clearCache();\n\t\t},\n\n\t\tdisposeAll(): void {\n\t\t\tdebugLog(`[Compute/client-cache] disposing all ${cache.size} warm client(s)`);\n\t\t\tfor (const entry of cache.values()) entry.scheduler.dispose();\n\t\t\tcache.clear();\n\t\t}\n\t};\n}\n","/**\n * In-process cache of `.gh` definition bytes, keyed by immutable **version id**.\n *\n * Turns an eager `storage.get(fileKey)` on every solve request into a lazy\n * `DefinitionRef.load()`: the scheduler calls `load()` only when an upload is\n * unavoidable (a pointer-known re-solve never touches storage or this cache).\n *\n * The key MUST be the version id, never a `fileKey` — a delete-latest-then-\n * reupload can reuse a `fileKey` for different content, which would silently\n * serve one version's bytes for another with no diagnostic. Callers pass\n * `version.id`, and the returned `DefinitionRef.key` is that same id, so the\n * scheduler's own result/pointer cache keys on the same immutable identity.\n *\n * Eviction is LRU by total byte budget, not entry count — definitions range\n * from tens of KB to hundreds of MB, so a count cap would either pin\n * gigabytes or evict uselessly. `COMPUTE_DEFINITION_CACHE_MB` sets the\n * budget (see `createDefinitionByteCache`). No TTL: version ids are\n * immutable, so a cached entry can never go stale. An entry larger than the\n * whole budget is served but never retained.\n */\n\n/**\n * Per-`getOrLoad` outcome, observable after the scheduler has (or hasn't)\n * called `load()` — lets the solve pipeline emit a `def_bytes` Server-Timing\n * verdict (`skipped` / `hit` / `miss`).\n */\nexport interface ByteRefOutcome {\n\tloaded: boolean;\n\tfromCache: boolean;\n}\n\n/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */\nexport interface ByteCacheRef {\n\tkey: string;\n\tload: () => Promise<Uint8Array>;\n\toutcome: ByteRefOutcome;\n}\n\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\tgetOrLoad(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\tclear(): void;\n}\n\ninterface Entry {\n\tbytes: Uint8Array;\n}\n\n/**\n * @param maxBytes total retained-byte budget; `0` (or negative) disables caching\n * entirely — every `load()` calls the loader and nothing is retained.\n */\nexport function createDefinitionByteCache(maxBytes: number): DefinitionByteCache {\n\tconst budget = Math.max(0, Math.floor(maxBytes));\n\t// Map iteration order is insertion order, so it doubles as LRU order:\n\t// re-inserting a key on access moves it to the most-recently-used end.\n\tconst cache = new Map<string, Entry>();\n\tlet retainedBytes = 0;\n\tconst stats = { hits: 0, misses: 0, evictions: 0 };\n\n\tfunction touch(key: string, entry: Entry): void {\n\t\tcache.delete(key);\n\t\tcache.set(key, entry);\n\t}\n\n\tfunction evictToFit(): void {\n\t\twhile (retainedBytes > budget && cache.size > 0) {\n\t\t\tconst oldestKey = cache.keys().next().value as string;\n\t\t\tconst oldest = cache.get(oldestKey)!;\n\t\t\tcache.delete(oldestKey);\n\t\t\tretainedBytes -= oldest.bytes.byteLength;\n\t\t\tstats.evictions += 1;\n\t\t}\n\t}\n\n\tfunction store(key: string, bytes: Uint8Array): void {\n\t\tif (budget === 0) return;\n\t\t// Caching an entry bigger than the budget would evict everything\n\t\t// (including itself) on the next evictToFit — serve it, cache nothing.\n\t\tif (bytes.byteLength > budget) return;\n\t\tconst existing = cache.get(key);\n\t\tif (existing) retainedBytes -= existing.bytes.byteLength;\n\t\tcache.set(key, { bytes });\n\t\tretainedBytes += bytes.byteLength;\n\t\tevictToFit();\n\t}\n\n\treturn {\n\t\tgetOrLoad(versionId, load) {\n\t\t\tconst outcome: ByteRefOutcome = { loaded: false, fromCache: false };\n\t\t\treturn {\n\t\t\t\tkey: versionId,\n\t\t\t\toutcome,\n\t\t\t\tload: async () => {\n\t\t\t\t\tconst hit = cache.get(versionId);\n\t\t\t\t\tif (hit) {\n\t\t\t\t\t\ttouch(versionId, hit);\n\t\t\t\t\t\tstats.hits += 1;\n\t\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\t\toutcome.fromCache = true;\n\t\t\t\t\t\treturn hit.bytes;\n\t\t\t\t\t}\n\t\t\t\t\tstats.misses += 1;\n\t\t\t\t\toutcome.loaded = true;\n\t\t\t\t\toutcome.fromCache = false;\n\t\t\t\t\tconst bytes = await load();\n\t\t\t\t\tstore(versionId, bytes);\n\t\t\t\t\treturn bytes;\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t\tstats() {\n\t\t\treturn {\n\t\t\t\thits: stats.hits,\n\t\t\t\tmisses: stats.misses,\n\t\t\t\tevictions: stats.evictions,\n\t\t\t\tentries: cache.size,\n\t\t\t\tbytes: retainedBytes\n\t\t\t};\n\t\t},\n\t\tclear() {\n\t\t\tcache.clear();\n\t\t\tretainedBytes = 0;\n\t\t}\n\t};\n}\n","/**\n * In-process single-flight — dogpile protection for every solve.\n *\n * Always on, not gated behind a result cache being configured: the dogpile is worst\n * when nothing else is caching, since N concurrent identical solves each pay a full\n * Rhino round trip (the scheduler doesn't coalesce in-flight requests itself, and\n * Rhino's `cachesolve` still costs a round trip per repeat). An earlier version\n * gated this on cache config and so disabled it in exactly the deployments most\n * exposed to a cold-key stampede — hot public definition plus a deploy.\n *\n * Per app instance, above `ISolveResultCache`. No cross-instance lease (Redis\n * `SET NX`) yet.\n *\n * The shared promise resolves to one value for every waiter, so `work` must return\n * something safe to share by reference — the solve pipeline's envelope qualifies\n * because the app serializes it per response.\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Coalesces concurrent calls under the same key: the first caller runs `work`,\n\t * later callers for that key await the same promise, and the key frees as soon\n\t * as it settles.\n\t *\n\t * `onWaiterJoined` fires only on the owner's call, only while `work` is still\n\t * running, each time another caller joins. Ownership changes what the owner's\n\t * abort signal should mean: a solo run can cancel on its own client's\n\t * disconnect, but a shared run can't without 499-ing every waiter.\n\t */\n\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T>;\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\tonJoin?: (key: string) => void;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\tinterface Flight {\n\t\tpromise: Promise<unknown>;\n\t\tnotifyOwner?: () => void;\n\t}\n\tconst inflight = new Map<string, Flight>();\n\n\treturn {\n\t\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T> {\n\t\t\tconst existing = inflight.get(key);\n\t\t\tif (existing) {\n\t\t\t\toptions.onJoin?.(key);\n\t\t\t\texisting.notifyOwner?.();\n\t\t\t\treturn existing.promise as Promise<T>;\n\t\t\t}\n\n\t\t\t// Wrapping in an async IIFE means a synchronous throw in `work` still\n\t\t\t// rejects `p` instead of escaping `run` directly.\n\t\t\tconst flight: Flight = { notifyOwner: onWaiterJoined, promise: undefined! };\n\t\t\tconst p = (async () => work())().finally(() => {\n\t\t\t\t// Clear it here so a join after `work` settles can't fire it late.\n\t\t\t\tflight.notifyOwner = undefined;\n\t\t\t\t// Guard against deleting a key a later caller has already re-taken.\n\t\t\t\tif (inflight.get(key) === flight) inflight.delete(key);\n\t\t\t});\n\t\t\tflight.promise = p;\n\t\t\tinflight.set(key, flight);\n\t\t\treturn p;\n\t\t},\n\t\tinFlight(): number {\n\t\t\treturn inflight.size;\n\t\t}\n\t};\n}\n","/**\n * Facade over the warm-client cache, definition-byte cache, single-flight\n * coalescing, and the solve pipeline — the primitives an app route would\n * otherwise wire up by hand.\n *\n * Doesn't read env itself: `@selvajs/server` owns `resolveComputeLimits` and\n * depends on `@selvajs/solve`, so calling it from here would be a circular\n * import. Callers resolve their own limits and pass in the fields below.\n *\n * Stays out of auth, DB reads, share tokens, rate limiting, metrics, and\n * compute-server selection — all app policy, supplied per-call via\n * `SolveEngineSolveArgs.server`.\n */\n\nimport {\n\tisDefinitionRef,\n\tstableStringify,\n\ttype DefinitionRef,\n\ttype SolveDefinition\n} from '@selvajs/compute/grasshopper';\nimport { NoopLogger, type ILogger } from '@selvajs/platform';\nimport {\n\tcreateClientCache,\n\ttype CachedClient,\n\ttype ClientCache,\n\ttype ClientCacheDebug,\n\ttype ResolvedServer,\n\ttype SolveCacheStats\n} from './client-cache.js';\nimport {\n\tcreateDefinitionByteCache,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype DefinitionByteCache\n} from './definition-byte-cache.js';\nimport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight\n} from './solve-cache-single-flight.js';\nimport {\n\tadaptEnvelopeToEncoding,\n\tbuildSolveInputTree,\n\trunSolvePipeline,\n\ttype PipelineInput,\n\ttype SolveOutcome\n} from './solve-pipeline.js';\n\n/**\n * Subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs.\n * Pass the resolved object through as-is — extra fields are ignored.\n */\nexport interface SolveEngineLimits {\n\tsolveDeadlineMs: number;\n\tcomputeResponseMaxBytes: number;\n\tcomputeReuseDefinitionCache: boolean;\n\tcomputeServerCachesolve: boolean;\n\tcomputeCacheErroredSolves: boolean;\n\tcomputeMaxQueueDepth: number;\n\tcomputeQueueWaitMs: number;\n\tcomputeDefinitionCacheBytes: number;\n\tcomputeSolveCacheBytes: number;\n}\n\nexport interface SolveEngineOptions {\n\tlimits: SolveEngineLimits;\n\tlogger?: ILogger;\n\t/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */\n\tmaxWarmComputeServers?: number;\n\t/** Concise cache/timing logs when truthy; `'verbose'` also dumps full lib-level requests/responses. Default off. */\n\tdebug?: ClientCacheDebug;\n\tonDebugLog?: (message: string) => void;\n\t/** Fired when a caller joins an already-in-flight solve instead of running its own. */\n\tonSolveCoalesced?: (key: string) => void;\n}\n\n/**\n * Accepts every form `runSolvePipeline` does, plus `{versionId, load}` sugar\n * that builds (and caches) a `ByteCacheRef` internally. A `ByteCacheRef`\n * obtained ahead of time from `engine.definitionRef()` (e.g. to read bytes for\n * schema extraction before solving) is passed through as-is — detected by its\n * `outcome` field, which a plain `DefinitionRef` never has.\n */\nexport type SolveEngineDefinitionSource =\n\t| Uint8Array\n\t| string\n\t| DefinitionRef\n\t| ByteCacheRef\n\t| { versionId: string; load: () => Promise<Uint8Array> };\n\nexport interface SolveEngineSolveArgs {\n\tserver: ResolvedServer;\n\tdefinitionSource: SolveEngineDefinitionSource;\n\t/**\n\t * Coalesce-key identity for a raw `Uint8Array`/`string` source, which has\n\t * no natural identity the way a `DefinitionRef`'s `.key` does. Required in\n\t * that case — `solve()` throws rather than silently hashing the bytes.\n\t * Ignored for the other source forms.\n\t */\n\tdefinitionKey?: string;\n\tinputs: PipelineInput[];\n\tvalues: Record<string, unknown>;\n\tsignal: AbortSignal;\n\tacceptEncoding?: string;\n\t/** Stamped as `X-Selva-Definition` on this solve's client. */\n\tdefinitionGuid?: string;\n\tloadStartMs?: number;\n\tdefLoadMs?: number;\n\tprepMarks?: [string, number][];\n}\n\nexport interface FrameworkAgnosticResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string | Uint8Array;\n}\n\nexport interface SolveEngineStats {\n\tclient: SolveCacheStats;\n\tdefinitionBytes: ByteCacheStats;\n\tcoalescing: { inFlight: number };\n}\n\nfunction isByteCacheRef(source: SolveEngineDefinitionSource): source is ByteCacheRef {\n\treturn (\n\t\ttypeof source === 'object' &&\n\t\tsource !== null &&\n\t\t!(source instanceof Uint8Array) &&\n\t\t'outcome' in source\n\t);\n}\n\nfunction isVersionLoadPair(\n\tsource: SolveEngineDefinitionSource\n): source is { versionId: string; load: () => Promise<Uint8Array> } {\n\treturn (\n\t\ttypeof source === 'object' &&\n\t\tsource !== null &&\n\t\t!(source instanceof Uint8Array) &&\n\t\t!('outcome' in source) &&\n\t\t!('key' in source) &&\n\t\t'versionId' in source\n\t);\n}\n\nexport class SolveEngine {\n\tprivate readonly limits: SolveEngineLimits;\n\tprivate readonly clientCache: ClientCache;\n\tprivate readonly byteCache: DefinitionByteCache;\n\tprivate readonly singleFlight: SolveCacheSingleFlight;\n\n\tconstructor(options: SolveEngineOptions) {\n\t\tthis.limits = options.limits;\n\t\tconst logger = options.logger ?? new NoopLogger();\n\n\t\tthis.clientCache = createClientCache({\n\t\t\tsolveDeadlineMs: this.limits.solveDeadlineMs,\n\t\t\tcachesolve: this.limits.computeServerCachesolve,\n\t\t\tcacheerroredsolves: this.limits.computeCacheErroredSolves,\n\t\t\treuseServerDefinitionCache: this.limits.computeReuseDefinitionCache,\n\t\t\tmaxQueueDepth: this.limits.computeMaxQueueDepth,\n\t\t\tqueueWaitMs: this.limits.computeQueueWaitMs,\n\t\t\tresponseCacheMaxBytes: this.limits.computeSolveCacheBytes,\n\t\t\tdebug: options.debug ?? false,\n\t\t\tmaxWarmComputeServers: options.maxWarmComputeServers,\n\t\t\tonDebugLog:\n\t\t\t\toptions.onDebugLog ??\n\t\t\t\t((message) => logger.debug(message, { component: 'Compute/client-cache' }))\n\t\t});\n\n\t\tthis.byteCache = createDefinitionByteCache(this.limits.computeDefinitionCacheBytes);\n\n\t\tthis.singleFlight = createSolveCacheSingleFlight({\n\t\t\tonJoin: options.onSolveCoalesced\n\t\t});\n\t}\n\n\tgetClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient> {\n\t\treturn this.clientCache.getClient(server, opts);\n\t}\n\n\tdefinitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef {\n\t\treturn this.byteCache.getOrLoad(versionId, load);\n\t}\n\n\tevictServer(id: string): void {\n\t\tthis.clientCache.evict(id);\n\t}\n\n\tclearSolveCaches(): void {\n\t\tthis.clientCache.clearSolveCaches();\n\t}\n\n\tstats(): SolveEngineStats {\n\t\treturn {\n\t\t\tclient: this.clientCache.solveCacheStats(),\n\t\t\tdefinitionBytes: this.byteCache.stats(),\n\t\t\tcoalescing: { inFlight: this.singleFlight.inFlight() }\n\t\t};\n\t}\n\n\tasync solve(args: SolveEngineSolveArgs): Promise<SolveOutcome> {\n\t\tconst {\n\t\t\tdefinitionSource: resolvedSource,\n\t\t\tbyteRefOutcome,\n\t\t\tidentity\n\t\t} = this.resolveDefinitionSource(args);\n\n\t\tconst client = await this.clientCache.getClient(args.server, {\n\t\t\tdefinitionGuid: args.definitionGuid\n\t\t});\n\t\tconst inputTree = buildSolveInputTree(args.inputs, args.values);\n\t\tconst coalesceKey = `${identity}:${args.server.id}:${stableStringify(inputTree)}`;\n\n\t\t// A coalesced solve is shared, so one caller's disconnect can't 499 every\n\t\t// other waiter. Only propagate abort if no one else has joined yet.\n\t\tconst abortController = new AbortController();\n\t\tlet hasWaiters = false;\n\t\tconst onCallerAbort = () => {\n\t\t\tif (!hasWaiters) abortController.abort();\n\t\t};\n\t\t// `once: true` doesn't detach on a normal completion, and the caller's signal\n\t\t// outlives this call — a request signal fires `abort` after the response is\n\t\t// consumed, and a session-scoped one never settles at all. Without the\n\t\t// `finally` below, every solve leaks a listener onto it.\n\t\targs.signal.addEventListener('abort', onCallerAbort, { once: true });\n\n\t\tconst acceptEncoding = args.acceptEncoding ?? '';\n\t\tlet outcome: SolveOutcome;\n\t\ttry {\n\t\t\toutcome = await this.singleFlight.run(\n\t\t\t\tcoalesceKey,\n\t\t\t\t() =>\n\t\t\t\t\trunSolvePipeline({\n\t\t\t\t\t\tdefinitionSource: resolvedSource,\n\t\t\t\t\t\tbyteRefOutcome,\n\t\t\t\t\t\tinputs: args.inputs,\n\t\t\t\t\t\tvalues: args.values,\n\t\t\t\t\t\tinputTree,\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tresponseMaxBytes: this.limits.computeResponseMaxBytes,\n\t\t\t\t\t\tsolveDeadlineMs: this.limits.solveDeadlineMs,\n\t\t\t\t\t\tacceptEncoding,\n\t\t\t\t\t\tsignal: abortController.signal,\n\t\t\t\t\t\tloadStartMs: args.loadStartMs ?? performance.now(),\n\t\t\t\t\t\tdefLoadMs: args.defLoadMs ?? 0,\n\t\t\t\t\t\tprepMarks: args.prepMarks\n\t\t\t\t\t}),\n\t\t\t\t() => {\n\t\t\t\t\thasWaiters = true;\n\t\t\t\t}\n\t\t\t);\n\t\t} finally {\n\t\t\targs.signal.removeEventListener('abort', onCallerAbort);\n\t\t}\n\n\t\tif (outcome.kind !== 'ok') return outcome;\n\n\t\t// The envelope may be shared across waiters; re-key it to this caller's\n\t\t// own Accept-Encoding so each one gets the right wire form.\n\t\tconst wire = adaptEnvelopeToEncoding(outcome.envelope, acceptEncoding);\n\t\treturn {\n\t\t\t...outcome,\n\t\t\tenvelope: { ...outcome.envelope, body: wire.body, headers: wire.headers }\n\t\t};\n\t}\n\n\ttoResponse(\n\t\toutcome: SolveOutcome,\n\t\topts?: { onError?: (status: number, body: { message: string; retryAfter?: number }) => never }\n\t): FrameworkAgnosticResponse {\n\t\tconst fail = (\n\t\t\tstatus: number,\n\t\t\tbody: { message: string; retryAfter?: number }\n\t\t): FrameworkAgnosticResponse => {\n\t\t\tif (opts?.onError) opts.onError(status, body);\n\t\t\tconst headers: Record<string, string> = { 'Content-Type': 'application/json' };\n\t\t\tif (body.retryAfter !== undefined) headers['Retry-After'] = String(body.retryAfter);\n\t\t\treturn { status, headers, body: JSON.stringify(body) };\n\t\t};\n\n\t\tswitch (outcome.kind) {\n\t\t\tcase 'ok':\n\t\t\t\treturn { status: 200, headers: outcome.envelope.headers, body: outcome.envelope.body };\n\t\t\tcase 'timeout':\n\t\t\t\treturn fail(504, { message: outcome.message });\n\t\t\tcase 'client_abort':\n\t\t\t\treturn fail(499, { message: 'Client closed request' });\n\t\t\tcase 'too_large':\n\t\t\t\treturn fail(413, {\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'Solve result is too large to return. This usually means a file output exceeds the supported size.'\n\t\t\t\t});\n\t\t\tcase 'shed':\n\t\t\t\treturn fail(503, { message: outcome.message, retryAfter: outcome.retryAfterSeconds });\n\t\t\tcase 'compute_error':\n\t\t\t\tthrow outcome.error;\n\t\t}\n\t}\n\n\ttoWebResponse(outcome: SolveOutcome): Response {\n\t\tconst r = this.toResponse(outcome);\n\t\treturn new Response(typeof r.body === 'string' ? r.body : new Uint8Array(r.body), {\n\t\t\tstatus: r.status,\n\t\t\theaders: r.headers\n\t\t});\n\t}\n\n\tprivate resolveDefinitionSource(args: SolveEngineSolveArgs): {\n\t\tdefinitionSource: SolveDefinition;\n\t\tbyteRefOutcome?: ByteCacheRef['outcome'];\n\t\tidentity: string;\n\t} {\n\t\tconst source = args.definitionSource;\n\n\t\tif (isVersionLoadPair(source)) {\n\t\t\tconst ref = this.byteCache.getOrLoad(source.versionId, source.load);\n\t\t\treturn { definitionSource: ref, byteRefOutcome: ref.outcome, identity: ref.key };\n\t\t}\n\n\t\tif (isByteCacheRef(source)) {\n\t\t\treturn { definitionSource: source, byteRefOutcome: source.outcome, identity: source.key };\n\t\t}\n\n\t\tif (isDefinitionRef(source)) {\n\t\t\treturn { definitionSource: source, identity: source.key };\n\t\t}\n\n\t\t// Raw Uint8Array | string — no natural identity; require the caller's own.\n\t\tif (!args.definitionKey) {\n\t\t\tthrow new Error(\n\t\t\t\t'SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.'\n\t\t\t);\n\t\t}\n\t\treturn { definitionSource: source, identity: args.definitionKey };\n\t}\n}\n","// ============================================================================\n// server/ — the solve core behind the wire\n// ============================================================================\n//\n// Given a resolved solve context (definition bytes, inputs + values, a warm scheduler), runs a\n// solve and returns a ready-to-send envelope: build input tree → solve → serialize → gzip →\n// Server-Timing, plus the caches that make a repeat solve cheap (single-flight coalescing,\n// definition-byte cache, Rhino.Compute client cache).\n//\n// **Node-only** (`node:zlib`, `node:crypto`, `process.env`, platform-provider cache backends).\n// `client/` must never import from here — enforced by `no-restricted-imports` in\n// `eslint.config.mjs`, and on the built artifact by having no root barrel join the two halves.\n//\n// Not here, on purpose: HTTP status mapping, auth, tenancy, rate limiting, SSRF guards. Those are\n// request policy and stay in `@selvajs/server` (`/compute`, `/http`) and in the app route.\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\nexport {\n\trunSolvePipeline,\n\tbuildSolveInputTree,\n\tadaptEnvelopeToEncoding,\n\tCOMPUTE_CONTRACT_VERSION,\n\tCOMPUTE_VERSION_HEADER,\n\ttype SolvePipelineArgs,\n\ttype SolveOutcome,\n\ttype SolveEnvelope,\n\ttype SolvePhaseMetrics,\n\ttype PipelineInput\n} from './solve-pipeline.js';\n\nexport { transformInputParameter } from './transform-input.js';\n\n// ============================================================================\n// Caches\n// ============================================================================\n\nexport {\n\tcreateClientCache,\n\tserverIdentity,\n\ttype ClientCache,\n\ttype ClientCacheConfig,\n\ttype SolveCacheStats,\n\ttype CachedClient,\n\ttype ResolvedServer,\n\ttype ServerIdentity\n} from './client-cache.js';\n\nexport {\n\tcreateDefinitionByteCache,\n\ttype DefinitionByteCache,\n\ttype ByteCacheRef,\n\ttype ByteCacheStats,\n\ttype ByteRefOutcome\n} from './definition-byte-cache.js';\n\nexport {\n\tcreateSolveCacheSingleFlight,\n\ttype SolveCacheSingleFlight,\n\ttype SolveCacheSingleFlightOptions\n} from './solve-cache-single-flight.js';\n\n// ============================================================================\n// Facade\n// ============================================================================\n\nexport {\n\tSolveEngine,\n\ttype SolveEngineOptions,\n\ttype SolveEngineLimits,\n\ttype SolveEngineSolveArgs,\n\ttype SolveEngineDefinitionSource,\n\ttype SolveEngineStats,\n\ttype FrameworkAgnosticResponse\n} from './solve-engine.js';\n\nexport type { DefinitionRef, SolveDefinition } from '@selvajs/compute/core';\nexport { isDefinitionRef } from '@selvajs/compute/core';\n\n// ============================================================================\n// Shared vocabulary\n// ============================================================================\n//\n// Re-exported so a server-only consumer needs one import path, matching `client/`.\n\nexport type { SolveInput } from '../shared/solve-input.js';\n"],"mappings":"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","names":["ErrorCodes","ComputeError","TreeBuilder","gzip","gunzipSync","promisify","processInput","transformInputParameter","input","value","raw","processed","effective","gzipAsync","promisify","gzip","COMPUTE_CONTRACT_VERSION","COMPUTE_VERSION_HEADER","buildSolveInputTree","inputs","values","TreeBuilder","input","transformInputParameter","runSolvePipeline","args","client","signal","treeBuildStart","inputTree","treeBuildMs","settleSeqBefore","rhinoSeqBefore","result","solveStart","err","durationMs","ComputeError","ErrorCodes","solveMs","serializeStart","serialized","serializeMs","worthSendingGzip","compressed","gzipMs","gzipStart","serverTotalMs","settle","rhino","headers","buildServerTiming","metrics","envelope","adaptEnvelopeToEncoding","acceptEncoding","waiterWantsGzip","json","gunzipSync","parts","header","onRhinoMs","computeLinkMs","label","ms","byteRef","verdict","GrasshopperClient","enableDebugLogging","serverIdentity","server","DEFAULT_MAX_WARM_COMPUTE_SERVERS","FALLBACK_CONCURRENCY","CONCURRENCY_PROBE_INTERVAL_MS","createClientCache","config","maxWarmComputeServers","cache","pending","debugLog","message","build","definitionGuid","rhinoTiming","solveMeta","debugVerbose","clientConfig","t","decode","solve","encode","defCache","solveCache","client","maxConcurrent","lastProbeAt","probing","refreshConcurrency","scheduler","children","next","_ctx","result","errs","errCount","cacheNote","opts","key","existing","inFlight","buildPromise","entry","oldestKey","id","total","s","createDefinitionByteCache","maxBytes","budget","cache","retainedBytes","stats","touch","key","entry","evictToFit","oldestKey","oldest","store","bytes","existing","versionId","load","outcome","hit","createSolveCacheSingleFlight","options","inflight","key","work","onWaiterJoined","existing","flight","p","isDefinitionRef","stableStringify","NoopLogger","isByteCacheRef","source","isVersionLoadPair","SolveEngine","options","logger","NoopLogger","createClientCache","message","createDefinitionByteCache","createSolveCacheSingleFlight","server","opts","versionId","load","id","args","resolvedSource","byteRefOutcome","identity","client","inputTree","buildSolveInputTree","coalesceKey","stableStringify","abortController","hasWaiters","onCallerAbort","acceptEncoding","outcome","runSolvePipeline","wire","adaptEnvelopeToEncoding","fail","status","body","headers","r","ref","isDefinitionRef","isDefinitionRef"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/solve",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.7",
|
|
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",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
],
|
|
60
60
|
"sideEffects": false,
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@selvajs/compute": "^4.0.0-beta.
|
|
63
|
-
"@selvajs/platform": "^0.16.0",
|
|
64
|
-
"@selvajs/schemas": "
|
|
62
|
+
"@selvajs/compute": "^4.0.0-beta.3",
|
|
63
|
+
"@selvajs/platform": "^0.16.1-beta.0",
|
|
64
|
+
"@selvajs/schemas": "5.0.0-beta.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@eslint/js": "^10.0.1",
|