@selvajs/solve 0.2.0-beta.0 → 0.2.0-beta.1
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.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +9 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/shared.cjs.map +1 -1
- package/package.json +3 -3
package/dist/client.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["
|
|
1
|
+
{"version":3,"sources":["../src/client/external-storage.ts"],"names":["STORAGE_PREFIX","makeKey","scopeKey","inputId"],"mappings":"AAMA,4tBAA8B,IAExBA,CAAAA,CAAiB,UAAA,CAEvB,SAASC,CAAAA,CAAQC,CAAAA,CAAkBC,CAAAA,CAAyB,CAC3D,MAAO,CAAA,EAAA","file":"/home/runner/work/selva/selva/packages/solve/dist/client.cjs","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"]}
|
package/dist/server.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _compute = require('@selvajs/compute');var _zlib = require('zlib');var _util = require('util');function T(e,r){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:_nullishCoalesce(e.minimum, () => (null)),maximum:_nullishCoalesce(e.maximum, () => (null)),atLeast:1,atMost:1,stepSize:e.stepSize,default:_nullishCoalesce(r, () => (e.default))},s=_compute.processInput.call(void 0, t),o=_nullishCoalesce(r, () => (e.default));return s.default==null&&o!=null&&(Array.isArray(o)||s.paramType==="ValueList"&&typeof o!="object")&&(s.default=o),s}var $=_util.promisify.call(void 0, _zlib.gzip),E= exports.COMPUTE_CONTRACT_VERSION =1,P= exports.COMPUTE_VERSION_HEADER ="X-Selva-Compute-Version";function B(e,r){return _compute.TreeBuilder.fromInputParams(e.filter(t=>t.paramType).map(t=>T(t,r[t.id])))}async function D(e){let{client:r,signal:t}=e,s=performance.now(),o=_nullishCoalesce(e.inputTree, () => (B(e.inputs,e.values))),u=performance.now()-s,i=r.solveMeta.seq,l=r.rhinoTiming.seq,n,a=performance.now();try{n=await r.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let b=performance.now()-a;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:b}:{kind:"timeout",durationMs:b,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof _compute.RhinoComputeError){if(v.code===_compute.ErrorCodes.QUEUE_FULL)return{kind:"shed",durationMs:b,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===_compute.ErrorCodes.QUEUE_TIMEOUT)return{kind:"shed",durationMs:b,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:b,error:v}}let c=performance.now()-a,h=performance.now(),d;try{d=JSON.stringify(n)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,y=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,f=null,g=0;if(y){let v=performance.now();f=await $(Buffer.from(d)),g=performance.now()-v}let S=performance.now()-e.loadStartMs,M=r.solveMeta.seq===i+1?r.solveMeta.last:null,k=M&&!M.fromCache&&r.rhinoTiming.seq===l+1?r.rhinoTiming.last:null,C={"Content-Type":"application/json","Server-Timing":U({defLoadMs:e.defLoadMs,treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,rhino:k,settle:M,byteRefOutcome:e.byteRefOutcome,prepMarks:_nullishCoalesce(e.prepMarks, () => ([]))}),[P]:String(E),Vary:"Accept-Encoding"},R={treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,serializedBytes:d.length,compressedBytes:_nullishCoalesce(_optionalChain([f, 'optionalAccess', _2 => _2.byteLength]), () => (null))},x;return f?(C["Content-Encoding"]="gzip",C["Content-Length"]=String(f.byteLength),x={body:new Uint8Array(f),encoding:"gzip",headers:C,result:n,metrics:R}):(C["Content-Length"]=String(Buffer.byteLength(d)),x={body:d,headers:C,result:n,metrics:R}),{kind:"ok",envelope:x,solveMs:c,errorCount:_nullishCoalesce(_optionalChain([n, 'access', _3 => _3.errors, 'optionalAccess', _4 => _4.length]), () => (0)),warningCount:_nullishCoalesce(_optionalChain([n, 'access', _5 => _5.warnings, 'optionalAccess', _6 => _6.length]), () => (0))}}function F(e,r){let t=/\bgzip\b/i.test(r);if(e.encoding==="gzip"&&!t){let s=_zlib.gunzipSync.call(void 0, e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(s)),{body:s,headers:o}}return{body:e.body,headers:e.headers}}function U(e){let r=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let u=t.decode+t.solve+t.encode,i=Math.max(0,e.solveMs-u);r+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[u,i]of e.prepMarks)r+=`, p_${u};dur=${i.toFixed(1)}`;let s=e.settle;s&&(r+=`, selva_cache;dur=${s.fromCache?1:0}`,s.definitionReuploaded!==void 0&&(r+=`, def_reupload;dur=${s.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let u=o.loaded?o.fromCache?"hit":"miss":"skipped";r+=`, def_bytes;desc=${u}`}return r}function V(e){return e.id}var N=16;function j(e){let r=_nullishCoalesce(e.maxCachedClients, () => (N)),t=new Map,s=new Map;e.debugVerbose&&_compute.enableDebugLogging.call(void 0, );let o=i=>{e.debug&&_optionalChain([e, 'access', _7 => _7.onDebugLog, 'optionalCall', _8 => _8(i)])};async function u(i,l){let n={last:null,seq:0},a={last:null,seq:0},c={serverUrl:i.serverUrl,apiKey:i.apiKey,debug:e.debugVerbose,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:p=>{let m=_nullishCoalesce(p.decode, () => (0)),y=_nullishCoalesce(p.solve, () => (0)),f=_nullishCoalesce(p.encode, () => (0));if(n.seq+=1,n.last={decode:m,solve:y,encode:f},!e.debug)return;let g=m<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",S=e.cachesolve&&y<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${m}ms solve=${y}ms encode=${f}ms | definition-cache: ${g} | solve-cache (cachesolve): ${S}`)}};l&&(c.headers={"X-Selva-Definition":l});let h=await _compute.GrasshopperClient.create(c),d=h.createScheduler({mode:"queue",maxConcurrent:e.maxConcurrentSolves,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxEntries:20,ttlMs:5*6e4,maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(p,m)=>{if(a.seq+=1,m.status==="success"&&(a.last={fromCache:m.fromCache===!0,definitionReuploaded:m.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${m.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(m.durationMs)}ms)`),m.fromCache||o(m.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":m.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!m.fromCache))){let y=_optionalChain([m, 'access', _9 => _9.response, 'optionalAccess', _10 => _10.errors]),f=Array.isArray(y)?y.length:0;if(f>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${f} GH error(s); ${g}. First: ${JSON.stringify(y[0])}`)}}}});return{client:h,scheduler:d,rhinoTiming:n,solveMeta:a}}return{async getClient(i,l){let n=i.id,a=t.get(n);if(a)return t.delete(n),t.set(n,a),a;let c=s.get(n);if(c)return c;o(`[Compute/client-cache] miss \u2014 building warm client for server ${n}`);let h=(async()=>{let d=await u(i,_optionalChain([l, 'optionalAccess', _11 => _11.definitionGuid]));if(t.size>=r){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${r})`),_optionalChain([t, 'access', _12 => _12.get, 'call', _13 => _13(p), 'optionalAccess', _14 => _14.scheduler, 'access', _15 => _15.dispose, 'call', _16 => _16()]),t.delete(p))}return t.set(n,d),d})().finally(()=>s.delete(n));return s.set(n,h),h},evict(i){let l=i,n=t.get(l);n&&(o(`[Compute/client-cache] evicted warm client for server ${l} (config change)`),n.scheduler.dispose(),t.delete(l))},solveCacheStats(){let i={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let l of t.values()){let n=l.scheduler.cacheStats();i.entries+=n.entries,i.bytes+=n.bytes,i.hits+=n.hits,i.misses+=n.misses,i.evictions+=n.evictions}return i},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.dispose();t.clear()}}}function H(e){let r=Math.max(0,Math.floor(e)),t=new Map,s=0,o={hits:0,misses:0,evictions:0};function u(n,a){t.delete(n),t.set(n,a)}function i(){for(;s>r&&t.size>0;){let n=t.keys().next().value,a=t.get(n);t.delete(n),s-=a.bytes.byteLength,o.evictions+=1}}function l(n,a){if(r===0||a.byteLength>r)return;let c=t.get(n);c&&(s-=c.bytes.byteLength),t.set(n,{bytes:a}),s+=a.byteLength,i()}return{getOrLoad(n,a){let c={loaded:!1,fromCache:!1};return{key:n,outcome:c,load:async()=>{let h=t.get(n);if(h)return u(n,h),o.hits+=1,c.loaded=!0,c.fromCache=!0,h.bytes;o.misses+=1,c.loaded=!0,c.fromCache=!1;let d=await a();return l(n,d),d}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:s}},clear(){t.clear(),s=0}}}function W(e={}){let r=new Map;return{run(t,s,o){let u=r.get(t);if(u)return _optionalChain([e, 'access', _17 => _17.onJoin, 'optionalCall', _18 => _18(t)]),_optionalChain([u, 'access', _19 => _19.notifyOwner, 'optionalCall', _20 => _20()]),u.promise;let i={notifyOwner:o,promise:void 0},l=(async()=>s())().finally(()=>{i.notifyOwner=void 0,r.get(t)===i&&r.delete(t)});return i.promise=l,r.set(t,i),l},inFlight(){return r.size}}}exports.COMPUTE_CONTRACT_VERSION = E; exports.COMPUTE_VERSION_HEADER = P; exports.adaptEnvelopeToEncoding = F; exports.buildSolveInputTree = B; exports.createClientCache = j; exports.createDefinitionByteCache = H; exports.createSolveCacheSingleFlight = W; exports.runSolvePipeline = D; exports.serverIdentity = V; exports.transformInputParameter = T;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _compute = require('@selvajs/compute');var _zlib = require('zlib');var _util = require('util');function T(e,r){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:_nullishCoalesce(e.minimum, () => (null)),maximum:_nullishCoalesce(e.maximum, () => (null)),atLeast:1,atMost:1,stepSize:e.stepSize,default:_nullishCoalesce(r, () => (e.default))},s=_compute.processInput.call(void 0, t),o=_nullishCoalesce(r, () => (e.default));return s.default==null&&o!=null&&(Array.isArray(o)||s.paramType==="ValueList"&&typeof o!="object")&&(s.default=o),s}var $=_util.promisify.call(void 0, _zlib.gzip),P= exports.COMPUTE_CONTRACT_VERSION =1,E= exports.COMPUTE_VERSION_HEADER ="X-Selva-Compute-Version";function B(e,r){return _compute.TreeBuilder.fromInputParams(e.filter(t=>t.paramType).map(t=>T(t,r[t.id])))}async function D(e){let{client:r,signal:t}=e,s=performance.now(),o=_nullishCoalesce(e.inputTree, () => (B(e.inputs,e.values))),u=performance.now()-s,i=r.solveMeta.seq,l=r.rhinoTiming.seq,n,a=performance.now();try{n=await r.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let b=performance.now()-a;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:b}:{kind:"timeout",durationMs:b,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof _compute.RhinoComputeError){if(v.code===_compute.ErrorCodes.QUEUE_FULL)return{kind:"shed",durationMs:b,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===_compute.ErrorCodes.QUEUE_TIMEOUT)return{kind:"shed",durationMs:b,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:b,error:v}}let c=performance.now()-a,h=performance.now(),d;try{d=JSON.stringify(n)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,y=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,f=null,g=0;if(y){let v=performance.now();f=await $(Buffer.from(d)),g=performance.now()-v}let S=performance.now()-e.loadStartMs,M=r.solveMeta.seq===i+1?r.solveMeta.last:null,k=M&&!M.fromCache&&r.rhinoTiming.seq===l+1?r.rhinoTiming.last:null,C={"Content-Type":"application/json","Server-Timing":U({defLoadMs:e.defLoadMs,treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,rhino:k,settle:M,byteRefOutcome:e.byteRefOutcome,prepMarks:_nullishCoalesce(e.prepMarks, () => ([]))}),[E]:String(P),Vary:"Accept-Encoding"},w={treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,serializedBytes:d.length,compressedBytes:_nullishCoalesce(_optionalChain([f, 'optionalAccess', _2 => _2.byteLength]), () => (null))},x;return f?(C["Content-Encoding"]="gzip",C["Content-Length"]=String(f.byteLength),x={body:new Uint8Array(f),encoding:"gzip",headers:C,result:n,metrics:w}):(C["Content-Length"]=String(Buffer.byteLength(d)),x={body:d,headers:C,result:n,metrics:w}),{kind:"ok",envelope:x,solveMs:c,errorCount:_nullishCoalesce(_optionalChain([n, 'access', _3 => _3.errors, 'optionalAccess', _4 => _4.length]), () => (0)),warningCount:_nullishCoalesce(_optionalChain([n, 'access', _5 => _5.warnings, 'optionalAccess', _6 => _6.length]), () => (0))}}function F(e,r){let t=/\bgzip\b/i.test(r);if(e.encoding==="gzip"&&!t){let s=_zlib.gunzipSync.call(void 0, e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(s)),{body:s,headers:o}}return{body:e.body,headers:e.headers}}function U(e){let r=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let u=t.decode+t.solve+t.encode,i=Math.max(0,e.solveMs-u);r+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[u,i]of e.prepMarks)r+=`, p_${u};dur=${i.toFixed(1)}`;let s=e.settle;s&&(r+=`, selva_cache;dur=${s.fromCache?1:0}`,s.definitionReuploaded!==void 0&&(r+=`, def_reupload;dur=${s.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let u=o.loaded?o.fromCache?"hit":"miss":"skipped";r+=`, def_bytes;desc=${u}`}return r}function V(e){return e.id}var N=16;function j(e){let r=_nullishCoalesce(e.maxCachedClients, () => (N)),t=new Map,s=new Map;e.debugVerbose&&_compute.enableDebugLogging.call(void 0, );let o=i=>{e.debug&&_optionalChain([e, 'access', _7 => _7.onDebugLog, 'optionalCall', _8 => _8(i)])};async function u(i,l){let n={last:null,seq:0},a={last:null,seq:0},c={serverUrl:i.serverUrl,apiKey:i.apiKey,debug:e.debugVerbose,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:p=>{let m=_nullishCoalesce(p.decode, () => (0)),y=_nullishCoalesce(p.solve, () => (0)),f=_nullishCoalesce(p.encode, () => (0));if(n.seq+=1,n.last={decode:m,solve:y,encode:f},!e.debug)return;let g=m<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",S=e.cachesolve&&y<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${m}ms solve=${y}ms encode=${f}ms | definition-cache: ${g} | solve-cache (cachesolve): ${S}`)}};l&&(c.headers={"X-Selva-Definition":l});let h=await _compute.GrasshopperClient.create(c),d=h.createScheduler({mode:"queue",maxConcurrent:e.maxConcurrentSolves,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(p,m)=>{if(a.seq+=1,m.status==="success"&&(a.last={fromCache:m.fromCache===!0,definitionReuploaded:m.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${m.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(m.durationMs)}ms)`),m.fromCache||o(m.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":m.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!m.fromCache))){let y=_optionalChain([m, 'access', _9 => _9.response, 'optionalAccess', _10 => _10.errors]),f=Array.isArray(y)?y.length:0;if(f>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${f} GH error(s); ${g}. First: ${JSON.stringify(y[0])}`)}}}});return{client:h,scheduler:d,rhinoTiming:n,solveMeta:a}}return{async getClient(i,l){let n=i.id,a=t.get(n);if(a)return t.delete(n),t.set(n,a),a;let c=s.get(n);if(c)return c;o(`[Compute/client-cache] miss \u2014 building warm client for server ${n}`);let h=(async()=>{let d=await u(i,_optionalChain([l, 'optionalAccess', _11 => _11.definitionGuid]));if(t.size>=r){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${r})`),_optionalChain([t, 'access', _12 => _12.get, 'call', _13 => _13(p), 'optionalAccess', _14 => _14.scheduler, 'access', _15 => _15.dispose, 'call', _16 => _16()]),t.delete(p))}return t.set(n,d),d})().finally(()=>s.delete(n));return s.set(n,h),h},evict(i){let l=i,n=t.get(l);n&&(o(`[Compute/client-cache] evicted warm client for server ${l} (config change)`),n.scheduler.dispose(),t.delete(l))},solveCacheStats(){let i={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let l of t.values()){let n=l.scheduler.cacheStats();i.entries+=n.entries,i.bytes+=n.bytes,i.hits+=n.hits,i.misses+=n.misses,i.evictions+=n.evictions}return i},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.dispose();t.clear()}}}function H(e){let r=Math.max(0,Math.floor(e)),t=new Map,s=0,o={hits:0,misses:0,evictions:0};function u(n,a){t.delete(n),t.set(n,a)}function i(){for(;s>r&&t.size>0;){let n=t.keys().next().value,a=t.get(n);t.delete(n),s-=a.bytes.byteLength,o.evictions+=1}}function l(n,a){if(r===0||a.byteLength>r)return;let c=t.get(n);c&&(s-=c.bytes.byteLength),t.set(n,{bytes:a}),s+=a.byteLength,i()}return{getOrLoad(n,a){let c={loaded:!1,fromCache:!1};return{key:n,outcome:c,load:async()=>{let h=t.get(n);if(h)return u(n,h),o.hits+=1,c.loaded=!0,c.fromCache=!0,h.bytes;o.misses+=1,c.loaded=!0,c.fromCache=!1;let d=await a();return l(n,d),d}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:s}},clear(){t.clear(),s=0}}}function W(e={}){let r=new Map;return{run(t,s,o){let u=r.get(t);if(u)return _optionalChain([e, 'access', _17 => _17.onJoin, 'optionalCall', _18 => _18(t)]),_optionalChain([u, 'access', _19 => _19.notifyOwner, 'optionalCall', _20 => _20()]),u.promise;let i={notifyOwner:o,promise:void 0},l=(async()=>s())().finally(()=>{i.notifyOwner=void 0,r.get(t)===i&&r.delete(t)});return i.promise=l,r.set(t,i),l},inFlight(){return r.size}}}exports.COMPUTE_CONTRACT_VERSION = P; exports.COMPUTE_VERSION_HEADER = E; exports.adaptEnvelopeToEncoding = F; exports.buildSolveInputTree = B; exports.createClientCache = j; exports.createDefinitionByteCache = H; exports.createSolveCacheSingleFlight = W; exports.runSolvePipeline = D; exports.serverIdentity = V; exports.transformInputParameter = T;
|
|
2
2
|
//# sourceMappingURL=server.cjs.map
|
package/dist/server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["c:\\Users\\felix\\coding\\selva\\packages\\solve\\dist\\server.cjs"],"names":[],"mappings":"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","file":"C:\\Users\\felix\\coding\\selva\\packages\\solve\\dist\\server.cjs","sourcesContent":[null]}
|
|
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":"AASA,4tBAQO,4BAE0B,4BACP,SCVVA,CAAAA,CACfC,CAAAA,CACAC,CAAAA,CACa,CACb,IAAMC,CAAAA,CAAwB,CAC7B,EAAA,CAAIF,CAAAA,CAAM,EAAA,CACV,IAAA,CAAMA,CAAAA,CAAM,QAAA,CACZ,QAAA,CAAUA,CAAAA,CAAM,QAAA,EAAY,IAAA,CAC5B,WAAA,CAAaA,CAAAA,CAAM,WAAA,EAAe,EAAA,CAOlC,SAAA,CAAWA,CAAAA,CAAM,SAAA,GAAc,kBAAA,CAAqB,WAAA,CAAcA,CAAAA,CAAM,SAAA,CACxE,UAAA,CAAYA,CAAAA,CAAM,cAAA,GAAmB,MAAA,CACrC,OAAA,kBAASA,CAAAA,CAAM,OAAA,SAAW,MAAA,CAC1B,OAAA,kBAASA,CAAAA,CAAM,OAAA,SAAW,MAAA,CAC1B,OAAA,CAAS,CAAA,CACT,MAAA,CAAQ,CAAA,CACR,QAAA,CAAUA,CAAAA,CAAM,QAAA,CAChB,OAAA,kBAASC,CAAAA,SAASD,CAAAA,CAAM,SACzB,CAAA,CAEMG,CAAAA,CAAYC,mCAAAA,CAAgB,CAAA,CAU5BC,CAAAA,kBAAYJ,CAAAA,SAASD,CAAAA,CAAM,SAAA,CACjC,OAAIG,CAAAA,CAAU,OAAA,EAAW,IAAA,EAAQE,CAAAA,EAAa,IAAA,EAAA,CACzC,KAAA,CAAM,OAAA,CAAQA,CAAS,CAAA,EAEhBF,CAAAA,CAAU,SAAA,GAAc,WAAA,EAAe,OAAOE,CAAAA,EAAc,QAAA,CAAA,EAAA,CAIrEF,CAAAA,CAAmC,OAAA,CAAUE,CAAAA,CAAAA,CAGzCF,CACR,CD/BA,IAAMG,CAAAA,CAAYC,6BAAAA,UAAc,CAAA,CAOnBC,CAAAA,oCAA2B,CAAA,CAC3BC,CAAAA,kCAAyB,yBAAA,CAmI/B,SAASC,CAAAA,CACfC,CAAAA,CACAC,CAAAA,CACa,CACb,OAAOC,oBAAAA,CAAY,eAAA,CAClBF,CAAAA,CACE,MAAA,CAAQX,CAAAA,EAAUA,CAAAA,CAAM,SAAS,CAAA,CACjC,GAAA,CAAKA,CAAAA,EAAsBD,CAAAA,CAAwBC,CAAAA,CAAOY,CAAAA,CAAOZ,CAAAA,CAAM,EAAE,CAAC,CAAC,CAC9E,CACD,CAEA,MAAA,SAAsBc,CAAAA,CAAiBC,CAAAA,CAAgD,CACtF,GAAM,CAAE,MAAA,CAAAC,CAAAA,CAAQ,MAAA,CAAAC,CAAO,CAAA,CAAIF,CAAAA,CAGrBG,CAAAA,CAAiB,WAAA,CAAY,GAAA,CAAI,CAAA,CACjCC,CAAAA,kBAAYJ,CAAAA,CAAK,SAAA,SAAaL,CAAAA,CAAoBK,CAAAA,CAAK,MAAA,CAAQA,CAAAA,CAAK,MAAM,GAAA,CAC1EK,CAAAA,CAAc,WAAA,CAAY,GAAA,CAAI,CAAA,CAAIF,CAAAA,CAUlCG,CAAAA,CAAkBL,CAAAA,CAAO,SAAA,CAAU,GAAA,CACnCM,CAAAA,CAAiBN,CAAAA,CAAO,WAAA,CAAY,GAAA,CAEtCO,CAAAA,CACEC,CAAAA,CAAa,WAAA,CAAY,GAAA,CAAI,CAAA,CACnC,GAAI,CACHD,CAAAA,CAAS,MAAMP,CAAAA,CAAO,SAAA,CAAU,KAAA,CAAMD,CAAAA,CAAK,gBAAA,CAAkBI,CAAAA,CAAW,CAAE,MAAA,CAAAF,CAAO,CAAC,CACnF,CAAA,KAAA,CAASQ,CAAAA,CAAK,CACb,IAAMC,CAAAA,CAAa,WAAA,CAAY,GAAA,CAAI,CAAA,CAAIF,CAAAA,CAKvC,EAAA,CADgBC,EAAAA,WAAe,KAAA,EAASA,CAAAA,CAAI,IAAA,GAAS,YAAA,CAEpD,OAAIR,CAAAA,CAAO,OAAA,CAAgB,CAAE,IAAA,CAAM,cAAA,CAAgB,UAAA,CAAAS,CAAW,CAAA,CACvD,CACN,IAAA,CAAM,SAAA,CACN,UAAA,CAAAA,CAAAA,CACA,OAAA,CAAS,CAAA,mBAAA,EAAsB,IAAA,CAAK,KAAA,CAAMX,CAAAA,CAAK,kBAAA,CAAqB,GAAI,CAAC,CAAA,WAAA,CAC1E,CAAA,CAKD,EAAA,CAAIU,EAAAA,WAAeE,0BAAAA,CAAmB,CACrC,EAAA,CAAIF,CAAAA,CAAI,IAAA,GAASG,mBAAAA,CAAW,UAAA,CAC3B,MAAO,CACN,IAAA,CAAM,MAAA,CACN,UAAA,CAAAF,CAAAA,CACA,MAAA,CAAQ,YAAA,CACR,iBAAA,CAAmB,CAAA,CACnB,OAAA,CAAS,+CACV,CAAA,CAED,EAAA,CAAID,CAAAA,CAAI,IAAA,GAASG,mBAAAA,CAAW,aAAA,CAC3B,MAAO,CACN,IAAA,CAAM,MAAA,CACN,UAAA,CAAAF,CAAAA,CACA,MAAA,CAAQ,eAAA,CACR,iBAAA,CAAmB,CAAA,CACnB,OAAA,CAAS,8DACV,CAEF,CACA,MAAO,CAAE,IAAA,CAAM,eAAA,CAAiB,UAAA,CAAAA,CAAAA,CAAY,KAAA,CAAOD,CAAI,CACxD,CACA,IAAMI,CAAAA,CAAU,WAAA,CAAY,GAAA,CAAI,CAAA,CAAIL,CAAAA,CAG9BM,CAAAA,CAAiB,WAAA,CAAY,GAAA,CAAI,CAAA,CACnCC,CAAAA,CACJ,GAAI,CACHA,CAAAA,CAAa,IAAA,CAAK,SAAA,CAAUR,CAAM,CACnC,CAAA,KAAA,CAASE,CAAAA,CAAK,CAGb,EAAA,CAAIA,EAAAA,WAAe,UAAA,CAAY,MAAO,CAAE,IAAA,CAAM,WAAY,CAAA,CAC1D,MAAMA,CACP,CACA,EAAA,CAAIM,CAAAA,CAAW,MAAA,CAAShB,CAAAA,CAAK,gBAAA,CAAkB,MAAO,CAAE,IAAA,CAAM,WAAY,CAAA,CAC1E,IAAMiB,CAAAA,CAAc,WAAA,CAAY,GAAA,CAAI,CAAA,CAAIF,CAAAA,CASlCG,CAAAA,CADkB,WAAA,CAAY,IAAA,CAAKlB,CAAAA,CAAK,cAAc,CAAA,EAChBgB,CAAAA,CAAW,MAAA,CAAS,IAAA,CAC5DG,CAAAA,CAA4B,IAAA,CAC5BC,CAAAA,CAAS,CAAA,CACb,EAAA,CAAIF,CAAAA,CAAkB,CACrB,IAAMG,CAAAA,CAAY,WAAA,CAAY,GAAA,CAAI,CAAA,CAClCF,CAAAA,CAAa,MAAM5B,CAAAA,CAAU,MAAA,CAAO,IAAA,CAAKyB,CAAU,CAAC,CAAA,CACpDI,CAAAA,CAAS,WAAA,CAAY,GAAA,CAAI,CAAA,CAAIC,CAC9B,CAEA,IAAMC,CAAAA,CAAgB,WAAA,CAAY,GAAA,CAAI,CAAA,CAAItB,CAAAA,CAAK,WAAA,CAOzCuB,CAAAA,CAAStB,CAAAA,CAAO,SAAA,CAAU,GAAA,GAAQK,CAAAA,CAAkB,CAAA,CAAIL,CAAAA,CAAO,SAAA,CAAU,IAAA,CAAO,IAAA,CAChFuB,CAAAA,CACLD,CAAAA,EAAU,CAACA,CAAAA,CAAO,SAAA,EAAatB,CAAAA,CAAO,WAAA,CAAY,GAAA,GAAQM,CAAAA,CAAiB,CAAA,CACxEN,CAAAA,CAAO,WAAA,CAAY,IAAA,CACnB,IAAA,CAgBEwB,CAAAA,CAAkC,CACvC,cAAA,CAAgB,kBAAA,CAChB,eAAA,CAfoBC,CAAAA,CAAkB,CACtC,SAAA,CAAW1B,CAAAA,CAAK,SAAA,CAChB,WAAA,CAAAK,CAAAA,CACA,OAAA,CAAAS,CAAAA,CACA,WAAA,CAAAG,CAAAA,CACA,MAAA,CAAAG,CAAAA,CACA,aAAA,CAAAE,CAAAA,CACA,KAAA,CAAAE,CAAAA,CACA,MAAA,CAAAD,CAAAA,CACA,cAAA,CAAgBvB,CAAAA,CAAK,cAAA,CACrB,SAAA,kBAAWA,CAAAA,CAAK,SAAA,SAAa,CAAC,GAC/B,CAAC,CAAA,CAKA,CAACN,CAAsB,CAAA,CAAG,MAAA,CAAOD,CAAwB,CAAA,CACzD,IAAA,CAAM,iBACP,CAAA,CAEMkC,CAAAA,CAA6B,CAClC,WAAA,CAAAtB,CAAAA,CACA,OAAA,CAAAS,CAAAA,CACA,WAAA,CAAAG,CAAAA,CACA,MAAA,CAAAG,CAAAA,CACA,aAAA,CAAAE,CAAAA,CACA,eAAA,CAAiBN,CAAAA,CAAW,MAAA,CAC5B,eAAA,kCAAiBG,CAAAA,6BAAY,YAAA,SAAc,MAC5C,CAAA,CAEIS,CAAAA,CACJ,OAAIT,CAAAA,CAAAA,CAGHM,CAAAA,CAAQ,kBAAkB,CAAA,CAAI,MAAA,CAC9BA,CAAAA,CAAQ,gBAAgB,CAAA,CAAI,MAAA,CAAON,CAAAA,CAAW,UAAU,CAAA,CACxDS,CAAAA,CAAW,CACV,IAAA,CAAM,IAAI,UAAA,CAAWT,CAAU,CAAA,CAC/B,QAAA,CAAU,MAAA,CACV,OAAA,CAAAM,CAAAA,CACA,MAAA,CAAAjB,CAAAA,CACA,OAAA,CAAAmB,CACD,CAAA,CAAA,CAAA,CAEAF,CAAAA,CAAQ,gBAAgB,CAAA,CAAI,MAAA,CAAO,MAAA,CAAO,UAAA,CAAWT,CAAU,CAAC,CAAA,CAChEY,CAAAA,CAAW,CAAE,IAAA,CAAMZ,CAAAA,CAAY,OAAA,CAAAS,CAAAA,CAAS,MAAA,CAAAjB,CAAAA,CAAQ,OAAA,CAAAmB,CAAQ,CAAA,CAAA,CAGlD,CACN,IAAA,CAAM,IAAA,CACN,QAAA,CAAAC,CAAAA,CACA,OAAA,CAAAd,CAAAA,CACA,UAAA,kCAAYN,CAAAA,qBAAO,MAAA,6BAAQ,QAAA,SAAU,GAAA,CACrC,YAAA,kCAAcA,CAAAA,qBAAO,QAAA,6BAAU,QAAA,SAAU,GAC1C,CACD,CAsBO,SAASqB,CAAAA,CACfD,CAAAA,CACAE,CAAAA,CACiE,CACjE,IAAMC,CAAAA,CAAkB,WAAA,CAAY,IAAA,CAAKD,CAAc,CAAA,CAKvD,EAAA,CAAIF,CAAAA,CAAS,QAAA,GAAa,MAAA,EAAU,CAACG,CAAAA,CAAiB,CACrD,IAAMC,CAAAA,CAAOC,8BAAAA,CACZL,CAAS,KAAA,WAAgB,UAAA,CAAaA,CAAAA,CAAS,IAAA,CAAO,MAAA,CAAO,IAAA,CAAKA,CAAAA,CAAS,IAAI,CAChF,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,CACXH,CAAAA,CAAU,CAAE,GAAGG,CAAAA,CAAS,OAAQ,CAAA,CACtC,OAAA,OAAOH,CAAAA,CAAQ,kBAAkB,CAAA,CACjCA,CAAAA,CAAQ,gBAAgB,CAAA,CAAI,MAAA,CAAO,MAAA,CAAO,UAAA,CAAWO,CAAI,CAAC,CAAA,CACnD,CAAE,IAAA,CAAMA,CAAAA,CAAM,OAAA,CAAAP,CAAQ,CAC9B,CAEA,MAAO,CAAE,IAAA,CAAMG,CAAAA,CAAS,IAAA,CAAM,OAAA,CAASA,CAAAA,CAAS,OAAQ,CACzD,CA2BA,SAASF,CAAAA,CAAkBQ,CAAAA,CAAkC,CAC5D,IAAIC,CAAAA,CACH,CAAA,SAAA,EAAYD,CAAAA,CAAM,SAAA,CAAU,OAAA,CAAQ,CAAC,CAAC,CAAA,WAAA,EAC1BA,CAAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,CAAC,CAAC,CAAA,YAAA,EAC3BA,CAAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAC,CAAA,gBAAA,EACpBA,CAAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,CAAC,CAAC,CAAA,WAAA,EACjCA,CAAAA,CAAM,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAC,CAAA,YAAA,EACtBA,CAAAA,CAAM,aAAA,CAAc,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA","file":"/home/runner/work/selva/selva/packages/solve/dist/server.cjs","sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * framework-free half of a solve and returns a discriminated {@link SolveOutcome}\n * instead of throwing for expected failures. Nothing here touches auth, the\n * database, share tokens, rate limits, or metric sinks — those stay app policy\n * in the route that calls this.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n/** Async gzip — off the event loop, unlike `gzipSync`, which blocks it for multi-MB bodies. */\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from\n\t * the definition-byte cache) whose bytes the scheduler materializes ONLY when\n\t * an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves\n\t * zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/**\n\t * When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome\n\t * so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for\n\t * raw-bytes solves.\n\t */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}. When present\n\t * the pipeline skips its own build and solves this exact object.\n\t *\n\t * Needed whenever the caller coalesces concurrent solves: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */\n\tmaxSolveDurationMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Request abort signal, forwarded to the scheduler so a client disconnect\n\t * cancels the upstream compute call. Its `aborted` flag also disambiguates a\n\t * client disconnect from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/**\n\t * Wall-clock origin (`performance.now()` captured at the top of the request)\n\t * so the pipeline's `load`/`total` phase timings line up with the caller's\n\t * pre-solve prep.\n\t */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/**\n\t * Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`\n\t * Server-Timing entries.\n\t */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * Discriminated result. `ok` carries the envelope; every other variant names an\n * expected failure the transport maps to a status code (the app route maps\n * timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;\n * `compute_error` re-surfaces the original error for the generic 500/503 path).\n * `durationMs` on the error variants is the solve wall time up to the failure,\n * for the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/**\n\t * Scheduler backpressure shed the solve before it executed — the per-server\n\t * queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline\n\t * (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.\n\t * `retryAfterSeconds` is a suggested backoff hint for the client.\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/**\n * Build the transformed input tree — the exact object handed to the scheduler.\n * `runSolvePipeline` calls this itself; a caller only needs it directly to see\n * the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).\n */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\t// --- input tree build ---------------------------------------------------\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// --- solve --------------------------------------------------------------\n\t// Snapshot the shared telemetry sequence counters. The holders live on the\n\t// warm client and are written by every request on this server; with\n\t// maxConcurrentSolves > 1 another request can write them mid-flight. After\n\t// the solve, a slot is attributed to THIS request only when exactly one\n\t// write happened since this snapshot — for solveMeta that single settle is\n\t// necessarily ours (onSettle fires before scheduler.solve() resolves).\n\t// Anything ambiguous is dropped from Server-Timing, never misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// Distinguish timeout (scheduler deadline timer) from client disconnect\n\t\t// (request signal). AbortError with the request signal NOT aborted means\n\t\t// the scheduler's own timeout fired — the solve genuinely timed out.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.maxSolveDurationMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// A full queue or an over-deadline queue wait sheds the solve BEFORE compute\n\t\t// runs. Classify these as `shed` rather than the generic `compute_error` —\n\t\t// they're load signals, not failures, and the client should back off and retry.\n\t\tif (err instanceof RhinoComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\t// --- serialize (size-guarded) ------------------------------------------\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a single string at ~512 MB; an oversized `file` output trips a\n\t\t// RangeError here. Surface it as the same too-large outcome as the byte cap.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// --- gzip ---------------------------------------------------------------\n\t// Compress BEFORE the timing snapshot so its cost is a measured phase (`gzip`\n\t// in Server-Timing, included in `total`). Buffered (not streamed) so\n\t// Content-Length is known and a connection cut mid-transfer fails hard\n\t// instead of truncating the JSON. Skip gzip for bodies under the 1 KB\n\t// break-even — not worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// --- per-request telemetry attribution (see snapshot above) -------------\n\t// settle: ours iff exactly one settle happened since the snapshot. rhino:\n\t// only meaningful when the settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\t// --- Server-Timing envelope ----------------------------------------------\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// A downstream proxy skips re-encoding an already-`Content-Encoding`d body,\n\t\t// so this never double-compresses.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope}. That\n * envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a\n * later waiter with a different `Accept-Encoding` would otherwise get a body\n * labelled with the wrong encoding (`Vary` can't help — this is one object\n * shared across waiters, not a cache lookup). Encoding is deliberately not part\n * of the coalesce key, so mixed clients still coalesce; this adapts the shared\n * result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// The only mismatch that corrupts the response: a gzip body handed to a waiter\n\t// that did not advertise gzip. Gunzip it back to the JSON string and drop the\n\t// encoding headers. Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * Assemble the `Server-Timing` header. The browser reads these to attribute its\n * round-trip: `total` here == the server's headers-to-out wall time, so browser\n * `ttfb − total` ≈ network+send latency. When the compute server reported its\n * own decode/solve/encode (VektorNode fork), `rhino_*` is time ON the compute\n * server and `compute_link` is everything between (network + queue wait). Prep\n * sub-phases ride as `p_*`; cache verdicts as 0/1 `selva_cache`/`def_reupload`.\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// Byte-cache verdict for `DefinitionRef` solves: skipped = the scheduler never\n\t// materialized bytes (pointer-known solve — the whole point of this path), hit\n\t// = warm byte cache, miss = fell through to storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`\n * and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`\n * are absent on `SchemaInput` (they live on the layout item's config), so\n * valueList/file inputs fall back to carrying the selected value with no option\n * list — the same value that reaches Grasshopper.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// Dynamic value lists ride the ValueList contract on the wire — the plugin's\n\t\t// GetDynamicValueListParameter deliberately reuses TypeName \"ValueList\" so the\n\t\t// compute fork's existing ValueList input case assigns it. processInput has no\n\t\t// dynamicValueList handler; without this mapping it falls back to Geometry with\n\t\t// a null default and TreeBuilder DROPS the input, so the user's selection never\n\t\t// reaches the definition (the param then solves on its own stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// Two cases where processInput drops a default we must carry anyway — a\n\t// dropped default means the input is omitted from the solve and the user's\n\t// selection never reaches Grasshopper:\n\t// - a plain array default (multi-select widgets, e.g. a dynamic value list\n\t// checklist) is dropped as \"malformed\".\n\t// - the ValueList fallback drops a scalar default it can't validate against\n\t// a `values` map — and `SchemaInput` never carries the option map (it\n\t// lives on the layout item's config), so every app value-list selection\n\t// lands here.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Scoped to ValueList: for other types a dropped scalar default means\n\t\t\t// the package rejected it as unparseable — restoring it would ship\n\t\t\t// garbage to Grasshopper.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n"]}
|
package/dist/server.d.cts
CHANGED
|
@@ -112,6 +112,14 @@ interface ClientCache {
|
|
|
112
112
|
* with the client that owns them, so totals can fall over time.
|
|
113
113
|
*/
|
|
114
114
|
solveCacheStats(): SolveCacheStats;
|
|
115
|
+
/**
|
|
116
|
+
* Drop every retained solve result, keeping the warm clients (and their
|
|
117
|
+
* connections and server-side definition pointers) intact. Nothing expires
|
|
118
|
+
* these on its own — the byte budget is the only other pressure — so this is
|
|
119
|
+
* the operator's release valve when a definition's inputs no longer describe
|
|
120
|
+
* its output, e.g. it reads an external source that has since changed.
|
|
121
|
+
*/
|
|
122
|
+
clearSolveCaches(): void;
|
|
115
123
|
/** Dispose every warm client. Test seam / shutdown hook. */
|
|
116
124
|
disposeAll(): void;
|
|
117
125
|
}
|
|
@@ -122,7 +130,7 @@ interface SolveCacheStats {
|
|
|
122
130
|
bytes: number;
|
|
123
131
|
hits: number;
|
|
124
132
|
misses: number;
|
|
125
|
-
/** Entries dropped under size/byte pressure (not
|
|
133
|
+
/** Entries dropped under size/byte pressure (not replacement or manual clears). */
|
|
126
134
|
evictions: number;
|
|
127
135
|
}
|
|
128
136
|
declare function createClientCache(config: ClientCacheConfig): ClientCache;
|
package/dist/server.d.ts
CHANGED
|
@@ -112,6 +112,14 @@ interface ClientCache {
|
|
|
112
112
|
* with the client that owns them, so totals can fall over time.
|
|
113
113
|
*/
|
|
114
114
|
solveCacheStats(): SolveCacheStats;
|
|
115
|
+
/**
|
|
116
|
+
* Drop every retained solve result, keeping the warm clients (and their
|
|
117
|
+
* connections and server-side definition pointers) intact. Nothing expires
|
|
118
|
+
* these on its own — the byte budget is the only other pressure — so this is
|
|
119
|
+
* the operator's release valve when a definition's inputs no longer describe
|
|
120
|
+
* its output, e.g. it reads an external source that has since changed.
|
|
121
|
+
*/
|
|
122
|
+
clearSolveCaches(): void;
|
|
115
123
|
/** Dispose every warm client. Test seam / shutdown hook. */
|
|
116
124
|
disposeAll(): void;
|
|
117
125
|
}
|
|
@@ -122,7 +130,7 @@ interface SolveCacheStats {
|
|
|
122
130
|
bytes: number;
|
|
123
131
|
hits: number;
|
|
124
132
|
misses: number;
|
|
125
|
-
/** Entries dropped under size/byte pressure (not
|
|
133
|
+
/** Entries dropped under size/byte pressure (not replacement or manual clears). */
|
|
126
134
|
evictions: number;
|
|
127
135
|
}
|
|
128
136
|
declare function createClientCache(config: ClientCacheConfig): ClientCache;
|
package/dist/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ErrorCodes as
|
|
1
|
+
import{ErrorCodes as R,RhinoComputeError as I,TreeBuilder as O}from"@selvajs/compute";import{gzip as A,gunzipSync as _}from"zlib";import{promisify as L}from"util";import{processInput as z}from"@selvajs/compute";function T(e,r){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:e.minimum??null,maximum:e.maximum??null,atLeast:1,atMost:1,stepSize:e.stepSize,default:r??e.default},s=z(t),o=r??e.default;return s.default==null&&o!=null&&(Array.isArray(o)||s.paramType==="ValueList"&&typeof o!="object")&&(s.default=o),s}var $=L(A),P=1,E="X-Selva-Compute-Version";function B(e,r){return O.fromInputParams(e.filter(t=>t.paramType).map(t=>T(t,r[t.id])))}async function D(e){let{client:r,signal:t}=e,s=performance.now(),o=e.inputTree??B(e.inputs,e.values),u=performance.now()-s,i=r.solveMeta.seq,l=r.rhinoTiming.seq,n,a=performance.now();try{n=await r.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let b=performance.now()-a;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:b}:{kind:"timeout",durationMs:b,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof I){if(v.code===R.QUEUE_FULL)return{kind:"shed",durationMs:b,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===R.QUEUE_TIMEOUT)return{kind:"shed",durationMs:b,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:b,error:v}}let c=performance.now()-a,h=performance.now(),d;try{d=JSON.stringify(n)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,y=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,f=null,g=0;if(y){let v=performance.now();f=await $(Buffer.from(d)),g=performance.now()-v}let S=performance.now()-e.loadStartMs,M=r.solveMeta.seq===i+1?r.solveMeta.last:null,k=M&&!M.fromCache&&r.rhinoTiming.seq===l+1?r.rhinoTiming.last:null,C={"Content-Type":"application/json","Server-Timing":U({defLoadMs:e.defLoadMs,treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,rhino:k,settle:M,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[E]:String(P),Vary:"Accept-Encoding"},w={treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,serializedBytes:d.length,compressedBytes:f?.byteLength??null},x;return f?(C["Content-Encoding"]="gzip",C["Content-Length"]=String(f.byteLength),x={body:new Uint8Array(f),encoding:"gzip",headers:C,result:n,metrics:w}):(C["Content-Length"]=String(Buffer.byteLength(d)),x={body:d,headers:C,result:n,metrics:w}),{kind:"ok",envelope:x,solveMs:c,errorCount:n.errors?.length??0,warningCount:n.warnings?.length??0}}function F(e,r){let t=/\bgzip\b/i.test(r);if(e.encoding==="gzip"&&!t){let s=_(e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(s)),{body:s,headers:o}}return{body:e.body,headers:e.headers}}function U(e){let r=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,t=e.rhino;if(t){let u=t.decode+t.solve+t.encode,i=Math.max(0,e.solveMs-u);r+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[u,i]of e.prepMarks)r+=`, p_${u};dur=${i.toFixed(1)}`;let s=e.settle;s&&(r+=`, selva_cache;dur=${s.fromCache?1:0}`,s.definitionReuploaded!==void 0&&(r+=`, def_reupload;dur=${s.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let u=o.loaded?o.fromCache?"hit":"miss":"skipped";r+=`, def_bytes;desc=${u}`}return r}import{GrasshopperClient as q,enableDebugLogging as G}from"@selvajs/compute";function V(e){return e.id}var N=16;function j(e){let r=e.maxCachedClients??N,t=new Map,s=new Map;e.debugVerbose&&G();let o=i=>{e.debug&&e.onDebugLog?.(i)};async function u(i,l){let n={last:null,seq:0},a={last:null,seq:0},c={serverUrl:i.serverUrl,apiKey:i.apiKey,debug:e.debugVerbose,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:p=>{let m=p.decode??0,y=p.solve??0,f=p.encode??0;if(n.seq+=1,n.last={decode:m,solve:y,encode:f},!e.debug)return;let g=m<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",S=e.cachesolve&&y<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${m}ms solve=${y}ms encode=${f}ms | definition-cache: ${g} | solve-cache (cachesolve): ${S}`)}};l&&(c.headers={"X-Selva-Definition":l});let h=await q.create(c),d=h.createScheduler({mode:"queue",maxConcurrent:e.maxConcurrentSolves,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(p,m)=>{if(a.seq+=1,m.status==="success"&&(a.last={fromCache:m.fromCache===!0,definitionReuploaded:m.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${m.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(m.durationMs)}ms)`),m.fromCache||o(m.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":m.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!m.fromCache))){let y=m.response?.errors,f=Array.isArray(y)?y.length:0;if(f>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${f} GH error(s); ${g}. First: ${JSON.stringify(y[0])}`)}}}});return{client:h,scheduler:d,rhinoTiming:n,solveMeta:a}}return{async getClient(i,l){let n=i.id,a=t.get(n);if(a)return t.delete(n),t.set(n,a),a;let c=s.get(n);if(c)return c;o(`[Compute/client-cache] miss \u2014 building warm client for server ${n}`);let h=(async()=>{let d=await u(i,l?.definitionGuid);if(t.size>=r){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${r})`),t.get(p)?.scheduler.dispose(),t.delete(p))}return t.set(n,d),d})().finally(()=>s.delete(n));return s.set(n,h),h},evict(i){let l=i,n=t.get(l);n&&(o(`[Compute/client-cache] evicted warm client for server ${l} (config change)`),n.scheduler.dispose(),t.delete(l))},solveCacheStats(){let i={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let l of t.values()){let n=l.scheduler.cacheStats();i.entries+=n.entries,i.bytes+=n.bytes,i.hits+=n.hits,i.misses+=n.misses,i.evictions+=n.evictions}return i},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.dispose();t.clear()}}}function H(e){let r=Math.max(0,Math.floor(e)),t=new Map,s=0,o={hits:0,misses:0,evictions:0};function u(n,a){t.delete(n),t.set(n,a)}function i(){for(;s>r&&t.size>0;){let n=t.keys().next().value,a=t.get(n);t.delete(n),s-=a.bytes.byteLength,o.evictions+=1}}function l(n,a){if(r===0||a.byteLength>r)return;let c=t.get(n);c&&(s-=c.bytes.byteLength),t.set(n,{bytes:a}),s+=a.byteLength,i()}return{getOrLoad(n,a){let c={loaded:!1,fromCache:!1};return{key:n,outcome:c,load:async()=>{let h=t.get(n);if(h)return u(n,h),o.hits+=1,c.loaded=!0,c.fromCache=!0,h.bytes;o.misses+=1,c.loaded=!0,c.fromCache=!1;let d=await a();return l(n,d),d}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:s}},clear(){t.clear(),s=0}}}function W(e={}){let r=new Map;return{run(t,s,o){let u=r.get(t);if(u)return e.onJoin?.(t),u.notifyOwner?.(),u.promise;let i={notifyOwner:o,promise:void 0},l=(async()=>s())().finally(()=>{i.notifyOwner=void 0,r.get(t)===i&&r.delete(t)});return i.promise=l,r.set(t,i),l},inFlight(){return r.size}}}export{P as COMPUTE_CONTRACT_VERSION,E as COMPUTE_VERSION_HEADER,F as adaptEnvelopeToEncoding,B as buildSolveInputTree,j as createClientCache,H as createDefinitionByteCache,W as createSolveCacheSingleFlight,D as runSolvePipeline,V as serverIdentity,T as transformInputParameter};
|
|
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"],"sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * framework-free half of a solve and returns a discriminated {@link SolveOutcome}\n * instead of throwing for expected failures. Nothing here touches auth, the\n * database, share tokens, rate limits, or metric sinks — those stay app policy\n * in the route that calls this.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n/** Async gzip — off the event loop, unlike `gzipSync`, which blocks it for multi-MB bodies. */\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from\n\t * the definition-byte cache) whose bytes the scheduler materializes ONLY when\n\t * an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves\n\t * zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/**\n\t * When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome\n\t * so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for\n\t * raw-bytes solves.\n\t */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}. When present\n\t * the pipeline skips its own build and solves this exact object.\n\t *\n\t * Needed whenever the caller coalesces concurrent solves: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */\n\tmaxSolveDurationMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Request abort signal, forwarded to the scheduler so a client disconnect\n\t * cancels the upstream compute call. Its `aborted` flag also disambiguates a\n\t * client disconnect from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/**\n\t * Wall-clock origin (`performance.now()` captured at the top of the request)\n\t * so the pipeline's `load`/`total` phase timings line up with the caller's\n\t * pre-solve prep.\n\t */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/**\n\t * Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`\n\t * Server-Timing entries.\n\t */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * Discriminated result. `ok` carries the envelope; every other variant names an\n * expected failure the transport maps to a status code (the app route maps\n * timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;\n * `compute_error` re-surfaces the original error for the generic 500/503 path).\n * `durationMs` on the error variants is the solve wall time up to the failure,\n * for the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/**\n\t * Scheduler backpressure shed the solve before it executed — the per-server\n\t * queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline\n\t * (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.\n\t * `retryAfterSeconds` is a suggested backoff hint for the client.\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/**\n * Build the transformed input tree — the exact object handed to the scheduler.\n * `runSolvePipeline` calls this itself; a caller only needs it directly to see\n * the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).\n */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\t// --- input tree build ---------------------------------------------------\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// --- solve --------------------------------------------------------------\n\t// Snapshot the shared telemetry sequence counters. The holders live on the\n\t// warm client and are written by every request on this server; with\n\t// maxConcurrentSolves > 1 another request can write them mid-flight. After\n\t// the solve, a slot is attributed to THIS request only when exactly one\n\t// write happened since this snapshot — for solveMeta that single settle is\n\t// necessarily ours (onSettle fires before scheduler.solve() resolves).\n\t// Anything ambiguous is dropped from Server-Timing, never misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// Distinguish timeout (scheduler deadline timer) from client disconnect\n\t\t// (request signal). AbortError with the request signal NOT aborted means\n\t\t// the scheduler's own timeout fired — the solve genuinely timed out.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.maxSolveDurationMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// A full queue or an over-deadline queue wait sheds the solve BEFORE compute\n\t\t// runs. Classify these as `shed` rather than the generic `compute_error` —\n\t\t// they're load signals, not failures, and the client should back off and retry.\n\t\tif (err instanceof RhinoComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\t// --- serialize (size-guarded) ------------------------------------------\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a single string at ~512 MB; an oversized `file` output trips a\n\t\t// RangeError here. Surface it as the same too-large outcome as the byte cap.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// --- gzip ---------------------------------------------------------------\n\t// Compress BEFORE the timing snapshot so its cost is a measured phase (`gzip`\n\t// in Server-Timing, included in `total`). Buffered (not streamed) so\n\t// Content-Length is known and a connection cut mid-transfer fails hard\n\t// instead of truncating the JSON. Skip gzip for bodies under the 1 KB\n\t// break-even — not worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// --- per-request telemetry attribution (see snapshot above) -------------\n\t// settle: ours iff exactly one settle happened since the snapshot. rhino:\n\t// only meaningful when the settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\t// --- Server-Timing envelope ----------------------------------------------\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// A downstream proxy skips re-encoding an already-`Content-Encoding`d body,\n\t\t// so this never double-compresses.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope}. That\n * envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a\n * later waiter with a different `Accept-Encoding` would otherwise get a body\n * labelled with the wrong encoding (`Vary` can't help — this is one object\n * shared across waiters, not a cache lookup). Encoding is deliberately not part\n * of the coalesce key, so mixed clients still coalesce; this adapts the shared\n * result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// The only mismatch that corrupts the response: a gzip body handed to a waiter\n\t// that did not advertise gzip. Gunzip it back to the JSON string and drop the\n\t// encoding headers. Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * Assemble the `Server-Timing` header. The browser reads these to attribute its\n * round-trip: `total` here == the server's headers-to-out wall time, so browser\n * `ttfb − total` ≈ network+send latency. When the compute server reported its\n * own decode/solve/encode (VektorNode fork), `rhino_*` is time ON the compute\n * server and `compute_link` is everything between (network + queue wait). Prep\n * sub-phases ride as `p_*`; cache verdicts as 0/1 `selva_cache`/`def_reupload`.\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// Byte-cache verdict for `DefinitionRef` solves: skipped = the scheduler never\n\t// materialized bytes (pointer-known solve — the whole point of this path), hit\n\t// = warm byte cache, miss = fell through to storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`\n * and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`\n * are absent on `SchemaInput` (they live on the layout item's config), so\n * valueList/file inputs fall back to carrying the selected value with no option\n * list — the same value that reaches Grasshopper.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// Dynamic value lists ride the ValueList contract on the wire — the plugin's\n\t\t// GetDynamicValueListParameter deliberately reuses TypeName \"ValueList\" so the\n\t\t// compute fork's existing ValueList input case assigns it. processInput has no\n\t\t// dynamicValueList handler; without this mapping it falls back to Geometry with\n\t\t// a null default and TreeBuilder DROPS the input, so the user's selection never\n\t\t// reaches the definition (the param then solves on its own stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// Two cases where processInput drops a default we must carry anyway — a\n\t// dropped default means the input is omitted from the solve and the user's\n\t// selection never reaches Grasshopper:\n\t// - a plain array default (multi-select widgets, e.g. a dynamic value list\n\t// checklist) is dropped as \"malformed\".\n\t// - the ValueList fallback drops a scalar default it can't validate against\n\t// a `values` map — and `SchemaInput` never carries the option map (it\n\t// lives on the layout item's config), so every app value-list selection\n\t// lands here.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Scoped to ValueList: for other types a dropped scalar default means\n\t\t\t// the package rejected it as unparseable — restoring it would ship\n\t\t\t// garbage to Grasshopper.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n","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 (ADR 0004) — a rotated\n// URL/apiKey keeps the same key with stale connection details, so the\n// config-write path MUST call `evict(id)` when a server's config changes.\n//\n// Deliberately a per-server LRU, not a single shared client: definitions can\n// pin different servers, so two definitions on two servers keep two warm\n// clients, with churn from one-off servers bounded by `maxCachedClients`.\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 `RhinoComputeKey`. */\n\tapiKey?: string;\n}\n\n/** Derive the opaque cache identity from a resolved server. Identity is the `id`. */\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.\n\t * The scheduler runs up to `maxConcurrentSolves` at once, so `last` alone\n\t * can't be trusted per-request: callers snapshot `seq` before their solve\n\t * and only attribute `last` to themselves if exactly one write happened\n\t * since (see the guard in `runSolvePipeline`), dropping it otherwise\n\t * rather than risk 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/** 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.maxSolveDurationMs`). */\n\tmaxSolveDurationMs: number;\n\t/** Ask Rhino.Compute to cache solve results and return them on identical repeats. */\n\tcachesolve: boolean;\n\t/** Also cache solves that reported GH errors (only meaningful with `cachesolve`). */\n\tcacheerroredsolves: boolean;\n\t/** Reference large definitions by server cache key (pointer) instead of re-uploading. */\n\treuseServerDefinitionCache: boolean;\n\t/**\n\t * Max in-flight solves per compute server (scheduler `maxConcurrent`; excess\n\t * FIFO-queues). Size to the server's `compute.geometry` child count\n\t * (`ComputeLimits.computeMaxConcurrentSolves`).\n\t */\n\tmaxConcurrentSolves: number;\n\t/**\n\t * Backpressure — max solves that may WAIT in the FIFO queue (excludes the\n\t * in-flight `maxConcurrentSolves`). `0` = unbounded (`ComputeLimits.\n\t * computeMaxQueueDepth`); a full queue sheds new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Backpressure — max ms a solve may sit queued before executing; `0` = no\n\t * deadline (`ComputeLimits.computeQueueWaitMs`). A too-long wait sheds with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache, evicted LRU alongside\n\t * its entry-count cap. Applies per warm client — total worst-case heap is this\n\t * × `maxCachedClients`. `0` disables the cache entirely\n\t * (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\t/** Concise cache/timing logs. When false, `onDebugLog` is never invoked. */\n\tdebug: boolean;\n\t/** VERBOSE lib-level logging (full solve request/response incl. geometry). */\n\tdebugVerbose: boolean;\n\t/** Max distinct warm clients before the LRU evicts the oldest. Default 16. */\n\tmaxCachedClients?: number;\n\t/** Sink for the concise debug lines (the app wires `console.log`). */\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/**\n\t * Solve-cache counters summed across every warm client. `warmClients` is\n\t * reported alongside since each client owns its own cache. Counters die\n\t * with the client that owns them, so totals can fall over time.\n\t */\n\tsolveCacheStats(): SolveCacheStats;\n\t/** Dispose every warm client. Test seam / shutdown hook. */\n\tdisposeAll(): void;\n}\n\n/** Aggregate solve-cache counters across the warm clients (see `solveCacheStats`). */\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 TTL expiry or replacement). */\n\tevictions: number;\n}\n\nconst DEFAULT_MAX_CACHED_CLIENTS = 16;\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxCachedClients = config.maxCachedClients ?? DEFAULT_MAX_CACHED_CLIENTS;\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.debugVerbose) 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\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: config.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\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: config.maxConcurrentSolves,\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 shed EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: config.maxSolveDurationMs,\n\t\t\tcache:\n\t\t\t\tconfig.responseCacheMaxBytes > 0\n\t\t\t\t\t? { maxEntries: 20, ttlMs: 5 * 60_000, maxBytes: config.responseCacheMaxBytes }\n\t\t\t\t\t: 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\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 >= maxCachedClients) {\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 ${maxCachedClients})`\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\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\t/** When `loaded`, whether the bytes came from a warm cache entry. */\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\n/** Hit/miss/eviction counters for observability (Server-Timing, admin debug). */\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped by the byte-budget LRU. */\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\t/**\n\t * `DefinitionRef` whose `load()` serves `versionId`'s bytes from the cache\n\t * when warm, otherwise calls `load` and caches the result. Cheap to build\n\t * and moves no bytes until `load()` runs.\n\t */\n\tgetOrLoad(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\t/** Test seam / eviction on definition delete if ever needed. */\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 preserves insertion order → iteration order IS LRU order; re-inserting\n\t// on access moves an entry to the most-recently-used (last) position.\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 * Deliberately NOT conditional on a result cache being configured: the dogpile is\n * worst precisely when nothing else is caching (N concurrent identical solves each\n * paying a full Rhino round trip — the scheduler has no in-flight coalescing of its\n * own, and Rhino's `cachesolve` still costs a round trip per repeat). Gating this on\n * cache configuration, as an earlier version did, turned it off in exactly the\n * deployments most exposed to a cold-key stampede (hot public definition + a deploy).\n *\n * In-process only, sitting above `ISolveResultCache` (per app instance). A\n * cross-instance lease (Redis `SET NX`) is a later backend capability.\n *\n * Correctness note: the shared promise resolves to ONE value for all waiters, so\n * the wrapped work must return an immutable / independently-serializable result\n * (the solve pipeline's envelope is safe to share by reference here because the\n * app serializes it per response).\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Run `work` under `key`, coalescing concurrent identical calls: the first\n\t * caller executes, overlapping callers for the same key await the same promise\n\t * and result, and the key is freed as soon as it settles.\n\t *\n\t * `onWaiterJoined` fires on the OWNER's call each time another caller joins its\n\t * flight — ownership changes what the owner's abort signal means (a solo run\n\t * may cancel on its own client's disconnect, a shared one may not, or it would\n\t * 499 every waiter). Never fires for a joining caller, and never after `work`\n\t * settles.\n\t */\n\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T>;\n\t/** Number of keys currently in flight (observability / tests). */\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\t/**\n\t * Fired when a caller joins an already-in-flight key instead of running its\n\t * own work — the coalescing win this module exists for, otherwise invisible.\n\t */\n\tonJoin?: (key: string) => void;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\t// Each entry carries the shared promise plus the owner's join callback, so a\n\t// joining caller can notify the owner that its flight is now shared.\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// `.finally` releases the key on both settle paths. Start the work inside\n\t\t\t// the promise chain so a synchronous throw in `work` still rejects the\n\t\t\t// shared promise (and still frees the key) rather than escaping.\n\t\t\tconst flight: Flight = { notifyOwner: onWaiterJoined, promise: undefined! };\n\t\t\tconst p = (async () => work())().finally(() => {\n\t\t\t\t// Drop the callback so a late join can't fire it after the owner's\n\t\t\t\t// request has already been served.\n\t\t\t\tflight.notifyOwner = undefined;\n\t\t\t\t// Only delete if it's still OUR flight — a key freed and re-taken by a\n\t\t\t\t// later caller must not be clobbered.\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"],"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_CACHED_CLIENTS","createClientCache","config","maxCachedClients","cache","pending","debugLog","message","build","definitionGuid","rhinoTiming","solveMeta","clientConfig","t","decode","solve","encode","defCache","solveCache","client","scheduler","_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"]}
|
|
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"],"sourcesContent":["/**\n * Transport-agnostic solve pipeline: given an already-resolved solve context\n * (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the\n * framework-free half of a solve and returns a discriminated {@link SolveOutcome}\n * instead of throwing for expected failures. Nothing here touches auth, the\n * database, share tokens, rate limits, or metric sinks — those stay app policy\n * in the route that calls this.\n */\n\nimport {\n\tErrorCodes,\n\tRhinoComputeError,\n\tTreeBuilder,\n\ttype DataTree,\n\ttype GrasshopperComputeResponse,\n\ttype InputParam,\n\ttype SolveDefinition\n} from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\nimport { gzip, gunzipSync } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport { transformInputParameter } from './transform-input.js';\nimport type { CachedClient } from './client-cache.js';\nimport type { ByteRefOutcome } from './definition-byte-cache.js';\n\n/** Async gzip — off the event loop, unlike `gzipSync`, which blocks it for multi-MB bodies. */\nconst gzipAsync = promisify(gzip);\n\n// ============================================================================\n// Wire contract version\n// ============================================================================\n\n/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */\nexport const COMPUTE_CONTRACT_VERSION = 1 as const;\nexport const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';\n\n// ============================================================================\n// Inputs & outcomes\n// ============================================================================\n\nexport type PipelineInput = SchemaInput & {\n\tminimum?: number;\n\tmaximum?: number;\n\tstepSize?: number;\n};\n\nexport interface SolvePipelineArgs {\n\t/**\n\t * The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from\n\t * the definition-byte cache) whose bytes the scheduler materializes ONLY when\n\t * an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves\n\t * zero bytes.\n\t */\n\tdefinitionSource: SolveDefinition;\n\t/**\n\t * When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome\n\t * so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for\n\t * raw-bytes solves.\n\t */\n\tbyteRefOutcome?: ByteRefOutcome;\n\t/** Persisted input params; only those with a `paramType` are sent to the solve. */\n\tinputs: PipelineInput[];\n\t/**\n\t * A tree the caller already built with {@link buildSolveInputTree}. When present\n\t * the pipeline skips its own build and solves this exact object.\n\t *\n\t * Needed whenever the caller coalesces concurrent solves: a single-flight key\n\t * derived from raw `{inputs, values}` would split two requests that transform\n\t * to the same tree, which is the identity the scheduler actually caches on.\n\t */\n\tinputTree?: DataTree[];\n\t/** User-chosen values keyed by input id; missing keys fall back to the schema default. */\n\tvalues: Record<string, unknown>;\n\tclient: CachedClient;\n\tresponseMaxBytes: number;\n\t/** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */\n\tmaxSolveDurationMs: number;\n\t/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */\n\tacceptEncoding: string;\n\t/**\n\t * Request abort signal, forwarded to the scheduler so a client disconnect\n\t * cancels the upstream compute call. Its `aborted` flag also disambiguates a\n\t * client disconnect from the scheduler's own deadline firing.\n\t */\n\tsignal: AbortSignal;\n\t/**\n\t * Wall-clock origin (`performance.now()` captured at the top of the request)\n\t * so the pipeline's `load`/`total` phase timings line up with the caller's\n\t * pre-solve prep.\n\t */\n\tloadStartMs: number;\n\t/** Pre-solve \"load\" phase duration the caller already measured (auth + DB + fetch). */\n\tdefLoadMs: number;\n\t/**\n\t * Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`\n\t * Server-Timing entries.\n\t */\n\tprepMarks?: [string, number][];\n}\n\n/** Phase timings the pipeline measured; the caller uses them for debug logging. */\nexport interface SolvePhaseMetrics {\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\tserializedBytes: number;\n\t/** Null when compression was skipped. */\n\tcompressedBytes: number | null;\n}\n\n/** A ready-to-send response: body + headers + the solve result + phase metrics. */\nexport interface SolveEnvelope {\n\t/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */\n\tbody: string | Uint8Array;\n\tencoding?: 'gzip';\n\theaders: Record<string, string>;\n\tresult: GrasshopperComputeResponse;\n\tmetrics: SolvePhaseMetrics;\n}\n\n/**\n * Discriminated result. `ok` carries the envelope; every other variant names an\n * expected failure the transport maps to a status code (the app route maps\n * timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;\n * `compute_error` re-surfaces the original error for the generic 500/503 path).\n * `durationMs` on the error variants is the solve wall time up to the failure,\n * for the metric record.\n */\nexport type SolveOutcome =\n\t| {\n\t\t\tkind: 'ok';\n\t\t\tenvelope: SolveEnvelope;\n\t\t\tsolveMs: number;\n\t\t\terrorCount: number;\n\t\t\twarningCount: number;\n\t }\n\t| { kind: 'timeout'; durationMs: number; message: string }\n\t| { kind: 'client_abort'; durationMs: number }\n\t| { kind: 'too_large' }\n\t/**\n\t * Scheduler backpressure shed the solve before it executed — the per-server\n\t * queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline\n\t * (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.\n\t * `retryAfterSeconds` is a suggested backoff hint for the client.\n\t */\n\t| {\n\t\t\tkind: 'shed';\n\t\t\tdurationMs: number;\n\t\t\treason: 'queue_full' | 'queue_timeout';\n\t\t\tretryAfterSeconds: number;\n\t\t\tmessage: string;\n\t }\n\t| { kind: 'compute_error'; durationMs: number; error: unknown };\n\n// ============================================================================\n// Pipeline\n// ============================================================================\n\n/**\n * Build the transformed input tree — the exact object handed to the scheduler.\n * `runSolvePipeline` calls this itself; a caller only needs it directly to see\n * the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).\n */\nexport function buildSolveInputTree(\n\tinputs: PipelineInput[],\n\tvalues: Record<string, unknown>\n): DataTree[] {\n\treturn TreeBuilder.fromInputParams(\n\t\tinputs\n\t\t\t.filter((input) => input.paramType)\n\t\t\t.map((input): InputParam => transformInputParameter(input, values[input.id]))\n\t);\n}\n\nexport async function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome> {\n\tconst { client, signal } = args;\n\n\t// --- input tree build ---------------------------------------------------\n\tconst treeBuildStart = performance.now();\n\tconst inputTree = args.inputTree ?? buildSolveInputTree(args.inputs, args.values);\n\tconst treeBuildMs = performance.now() - treeBuildStart;\n\n\t// --- solve --------------------------------------------------------------\n\t// Snapshot the shared telemetry sequence counters. The holders live on the\n\t// warm client and are written by every request on this server; with\n\t// maxConcurrentSolves > 1 another request can write them mid-flight. After\n\t// the solve, a slot is attributed to THIS request only when exactly one\n\t// write happened since this snapshot — for solveMeta that single settle is\n\t// necessarily ours (onSettle fires before scheduler.solve() resolves).\n\t// Anything ambiguous is dropped from Server-Timing, never misattributed.\n\tconst settleSeqBefore = client.solveMeta.seq;\n\tconst rhinoSeqBefore = client.rhinoTiming.seq;\n\n\tlet result: GrasshopperComputeResponse;\n\tconst solveStart = performance.now();\n\ttry {\n\t\tresult = await client.scheduler.solve(args.definitionSource, inputTree, { signal });\n\t} catch (err) {\n\t\tconst durationMs = performance.now() - solveStart;\n\t\t// Distinguish timeout (scheduler deadline timer) from client disconnect\n\t\t// (request signal). AbortError with the request signal NOT aborted means\n\t\t// the scheduler's own timeout fired — the solve genuinely timed out.\n\t\tconst isAbort = err instanceof Error && err.name === 'AbortError';\n\t\tif (isAbort) {\n\t\t\tif (signal.aborted) return { kind: 'client_abort', durationMs };\n\t\t\treturn {\n\t\t\t\tkind: 'timeout',\n\t\t\t\tdurationMs,\n\t\t\t\tmessage: `Solve exceeded the ${Math.round(args.maxSolveDurationMs / 1000)}s deadline.`\n\t\t\t};\n\t\t}\n\t\t// A full queue or an over-deadline queue wait sheds the solve BEFORE compute\n\t\t// runs. Classify these as `shed` rather than the generic `compute_error` —\n\t\t// they're load signals, not failures, and the client should back off and retry.\n\t\tif (err instanceof RhinoComputeError) {\n\t\t\tif (err.code === ErrorCodes.QUEUE_FULL) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_full',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute server is at capacity. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (err.code === ErrorCodes.QUEUE_TIMEOUT) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: 'shed',\n\t\t\t\t\tdurationMs,\n\t\t\t\t\treason: 'queue_timeout',\n\t\t\t\t\tretryAfterSeconds: 1,\n\t\t\t\t\tmessage: 'Compute request waited too long in the queue. Retry shortly.'\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { kind: 'compute_error', durationMs, error: err };\n\t}\n\tconst solveMs = performance.now() - solveStart;\n\n\t// --- serialize (size-guarded) ------------------------------------------\n\tconst serializeStart = performance.now();\n\tlet serialized: string;\n\ttry {\n\t\tserialized = JSON.stringify(result);\n\t} catch (err) {\n\t\t// V8 caps a single string at ~512 MB; an oversized `file` output trips a\n\t\t// RangeError here. Surface it as the same too-large outcome as the byte cap.\n\t\tif (err instanceof RangeError) return { kind: 'too_large' };\n\t\tthrow err;\n\t}\n\tif (serialized.length > args.responseMaxBytes) return { kind: 'too_large' };\n\tconst serializeMs = performance.now() - serializeStart;\n\n\t// --- gzip ---------------------------------------------------------------\n\t// Compress BEFORE the timing snapshot so its cost is a measured phase (`gzip`\n\t// in Server-Timing, included in `total`). Buffered (not streamed) so\n\t// Content-Length is known and a connection cut mid-transfer fails hard\n\t// instead of truncating the JSON. Skip gzip for bodies under the 1 KB\n\t// break-even — not worth the CPU for the wire savings.\n\tconst clientWantsGzip = /\\bgzip\\b/i.test(args.acceptEncoding);\n\tconst worthSendingGzip = clientWantsGzip && serialized.length > 1024;\n\tlet compressed: Buffer | null = null;\n\tlet gzipMs = 0;\n\tif (worthSendingGzip) {\n\t\tconst gzipStart = performance.now();\n\t\tcompressed = await gzipAsync(Buffer.from(serialized));\n\t\tgzipMs = performance.now() - gzipStart;\n\t}\n\n\tconst serverTotalMs = performance.now() - args.loadStartMs;\n\n\t// --- per-request telemetry attribution (see snapshot above) -------------\n\t// settle: ours iff exactly one settle happened since the snapshot. rhino:\n\t// only meaningful when the settle is ours AND it was a real compute call\n\t// (a Selva-cache hit never reaches the server), and only unambiguous when\n\t// exactly one Server-Timing callback fired in the window.\n\tconst settle = client.solveMeta.seq === settleSeqBefore + 1 ? client.solveMeta.last : null;\n\tconst rhino =\n\t\tsettle && !settle.fromCache && client.rhinoTiming.seq === rhinoSeqBefore + 1\n\t\t\t? client.rhinoTiming.last\n\t\t\t: null;\n\n\t// --- Server-Timing envelope ----------------------------------------------\n\tconst serverTiming = buildServerTiming({\n\t\tdefLoadMs: args.defLoadMs,\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\trhino,\n\t\tsettle,\n\t\tbyteRefOutcome: args.byteRefOutcome,\n\t\tprepMarks: args.prepMarks ?? []\n\t});\n\n\tconst headers: Record<string, string> = {\n\t\t'Content-Type': 'application/json',\n\t\t'Server-Timing': serverTiming,\n\t\t[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),\n\t\tVary: 'Accept-Encoding'\n\t};\n\n\tconst metrics: SolvePhaseMetrics = {\n\t\ttreeBuildMs,\n\t\tsolveMs,\n\t\tserializeMs,\n\t\tgzipMs,\n\t\tserverTotalMs,\n\t\tserializedBytes: serialized.length,\n\t\tcompressedBytes: compressed?.byteLength ?? null\n\t};\n\n\tlet envelope: SolveEnvelope;\n\tif (compressed) {\n\t\t// A downstream proxy skips re-encoding an already-`Content-Encoding`d body,\n\t\t// so this never double-compresses.\n\t\theaders['Content-Encoding'] = 'gzip';\n\t\theaders['Content-Length'] = String(compressed.byteLength);\n\t\tenvelope = {\n\t\t\tbody: new Uint8Array(compressed),\n\t\t\tencoding: 'gzip',\n\t\t\theaders,\n\t\t\tresult,\n\t\t\tmetrics\n\t\t};\n\t} else {\n\t\theaders['Content-Length'] = String(Buffer.byteLength(serialized));\n\t\tenvelope = { body: serialized, headers, result, metrics };\n\t}\n\n\treturn {\n\t\tkind: 'ok',\n\t\tenvelope,\n\t\tsolveMs,\n\t\terrorCount: result.errors?.length ?? 0,\n\t\twarningCount: result.warnings?.length ?? 0\n\t};\n}\n\n// ============================================================================\n// Per-waiter encoding adaptation\n// ============================================================================\n\n/**\n * Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.\n *\n * The single-flight coalescer runs ONE pipeline execution for N identical\n * concurrent solves and hands every waiter the same {@link SolveEnvelope}. That\n * envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a\n * later waiter with a different `Accept-Encoding` would otherwise get a body\n * labelled with the wrong encoding (`Vary` can't help — this is one object\n * shared across waiters, not a cache lookup). Encoding is deliberately not part\n * of the coalesce key, so mixed clients still coalesce; this adapts the shared\n * result to each waiter instead.\n *\n * Only the correctness-critical direction is adapted: a gzip envelope served to\n * a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a\n * gzip-capable waiter) is left uncompressed — correct, just not maximally small.\n */\nexport function adaptEnvelopeToEncoding(\n\tenvelope: SolveEnvelope,\n\tacceptEncoding: string\n): { body: string | Uint8Array; headers: Record<string, string> } {\n\tconst waiterWantsGzip = /\\bgzip\\b/i.test(acceptEncoding);\n\n\t// The only mismatch that corrupts the response: a gzip body handed to a waiter\n\t// that did not advertise gzip. Gunzip it back to the JSON string and drop the\n\t// encoding headers. Every other combination is already wire-correct.\n\tif (envelope.encoding === 'gzip' && !waiterWantsGzip) {\n\t\tconst json = gunzipSync(\n\t\t\tenvelope.body instanceof Uint8Array ? envelope.body : Buffer.from(envelope.body)\n\t\t).toString('utf8');\n\t\tconst headers = { ...envelope.headers };\n\t\tdelete headers['Content-Encoding'];\n\t\theaders['Content-Length'] = String(Buffer.byteLength(json));\n\t\treturn { body: json, headers };\n\t}\n\n\treturn { body: envelope.body, headers: envelope.headers };\n}\n\n// ============================================================================\n// Server-Timing assembly\n// ============================================================================\n\ninterface ServerTimingParts {\n\tdefLoadMs: number;\n\ttreeBuildMs: number;\n\tsolveMs: number;\n\tserializeMs: number;\n\tgzipMs: number;\n\tserverTotalMs: number;\n\trhino: CachedClient['rhinoTiming']['last'];\n\tsettle: CachedClient['solveMeta']['last'];\n\tbyteRefOutcome?: ByteRefOutcome;\n\tprepMarks: [string, number][];\n}\n\n/**\n * Assemble the `Server-Timing` header. The browser reads these to attribute its\n * round-trip: `total` here == the server's headers-to-out wall time, so browser\n * `ttfb − total` ≈ network+send latency. When the compute server reported its\n * own decode/solve/encode (VektorNode fork), `rhino_*` is time ON the compute\n * server and `compute_link` is everything between (network + queue wait). Prep\n * sub-phases ride as `p_*`; cache verdicts as 0/1 `selva_cache`/`def_reupload`.\n */\nfunction buildServerTiming(parts: ServerTimingParts): string {\n\tlet header =\n\t\t`load;dur=${parts.defLoadMs.toFixed(1)}, ` +\n\t\t`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +\n\t\t`solve;dur=${parts.solveMs.toFixed(1)}, ` +\n\t\t`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +\n\t\t`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +\n\t\t`total;dur=${parts.serverTotalMs.toFixed(1)}`;\n\n\tconst rhino = parts.rhino;\n\tif (rhino) {\n\t\tconst onRhinoMs = rhino.decode + rhino.solve + rhino.encode;\n\t\tconst computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);\n\t\theader +=\n\t\t\t`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +\n\t\t\t`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +\n\t\t\t`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +\n\t\t\t`, compute_link;dur=${computeLinkMs.toFixed(1)}`;\n\t}\n\n\tfor (const [label, ms] of parts.prepMarks) {\n\t\theader += `, p_${label};dur=${ms.toFixed(1)}`;\n\t}\n\n\tconst settle = parts.settle;\n\tif (settle) {\n\t\theader += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;\n\t\tif (settle.definitionReuploaded !== undefined) {\n\t\t\theader += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;\n\t\t}\n\t}\n\n\t// Byte-cache verdict for `DefinitionRef` solves: skipped = the scheduler never\n\t// materialized bytes (pointer-known solve — the whole point of this path), hit\n\t// = warm byte cache, miss = fell through to storage. Absent for raw-bytes solves.\n\tconst byteRef = parts.byteRefOutcome;\n\tif (byteRef) {\n\t\tconst verdict = !byteRef.loaded ? 'skipped' : byteRef.fromCache ? 'hit' : 'miss';\n\t\theader += `, def_bytes;desc=${verdict}`;\n\t}\n\n\treturn header;\n}\n","import { type InputParam, type InputParamSchema, processInput } from '@selvajs/compute';\nimport type { SchemaInput } from '@selvajs/schemas';\n\n/**\n * Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`\n * and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`\n * are absent on `SchemaInput` (they live on the layout item's config), so\n * valueList/file inputs fall back to carrying the selected value with no option\n * list — the same value that reaches Grasshopper.\n */\nexport function transformInputParameter(\n\tinput: SchemaInput & { minimum?: number; maximum?: number; stepSize?: number },\n\tvalue: unknown\n): InputParam {\n\tconst raw: InputParamSchema = {\n\t\tid: input.id,\n\t\tname: input.nickname,\n\t\tnickname: input.nickname || null,\n\t\tdescription: input.description || '',\n\t\t// Dynamic value lists ride the ValueList contract on the wire — the plugin's\n\t\t// GetDynamicValueListParameter deliberately reuses TypeName \"ValueList\" so the\n\t\t// compute fork's existing ValueList input case assigns it. processInput has no\n\t\t// dynamicValueList handler; without this mapping it falls back to Geometry with\n\t\t// a null default and TreeBuilder DROPS the input, so the user's selection never\n\t\t// reaches the definition (the param then solves on its own stale fallback).\n\t\tparamType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,\n\t\ttreeAccess: input.inputStructure === 'tree',\n\t\tminimum: input.minimum ?? null,\n\t\tmaximum: input.maximum ?? null,\n\t\tatLeast: 1,\n\t\tatMost: 1,\n\t\tstepSize: input.stepSize,\n\t\tdefault: value ?? input.default\n\t};\n\n\tconst processed = processInput(raw);\n\t// Two cases where processInput drops a default we must carry anyway — a\n\t// dropped default means the input is omitted from the solve and the user's\n\t// selection never reaches Grasshopper:\n\t// - a plain array default (multi-select widgets, e.g. a dynamic value list\n\t// checklist) is dropped as \"malformed\".\n\t// - the ValueList fallback drops a scalar default it can't validate against\n\t// a `values` map — and `SchemaInput` never carries the option map (it\n\t// lives on the layout item's config), so every app value-list selection\n\t// lands here.\n\tconst effective = value ?? input.default;\n\tif (processed.default == null && effective != null) {\n\t\tif (Array.isArray(effective)) {\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t} else if (processed.paramType === 'ValueList' && typeof effective !== 'object') {\n\t\t\t// Scoped to ValueList: for other types a dropped scalar default means\n\t\t\t// the package rejected it as unparseable — restoring it would ship\n\t\t\t// garbage to Grasshopper.\n\t\t\t(processed as { default: unknown }).default = effective;\n\t\t}\n\t}\n\treturn processed;\n}\n","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 (ADR 0004) — a rotated\n// URL/apiKey keeps the same key with stale connection details, so the\n// config-write path MUST call `evict(id)` when a server's config changes.\n//\n// Deliberately a per-server LRU, not a single shared client: definitions can\n// pin different servers, so two definitions on two servers keep two warm\n// clients, with churn from one-off servers bounded by `maxCachedClients`.\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 `RhinoComputeKey`. */\n\tapiKey?: string;\n}\n\n/** Derive the opaque cache identity from a resolved server. Identity is the `id`. */\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.\n\t * The scheduler runs up to `maxConcurrentSolves` at once, so `last` alone\n\t * can't be trusted per-request: callers snapshot `seq` before their solve\n\t * and only attribute `last` to themselves if exactly one write happened\n\t * since (see the guard in `runSolvePipeline`), dropping it otherwise\n\t * rather than risk 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/** 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.maxSolveDurationMs`). */\n\tmaxSolveDurationMs: number;\n\t/** Ask Rhino.Compute to cache solve results and return them on identical repeats. */\n\tcachesolve: boolean;\n\t/** Also cache solves that reported GH errors (only meaningful with `cachesolve`). */\n\tcacheerroredsolves: boolean;\n\t/** Reference large definitions by server cache key (pointer) instead of re-uploading. */\n\treuseServerDefinitionCache: boolean;\n\t/**\n\t * Max in-flight solves per compute server (scheduler `maxConcurrent`; excess\n\t * FIFO-queues). Size to the server's `compute.geometry` child count\n\t * (`ComputeLimits.computeMaxConcurrentSolves`).\n\t */\n\tmaxConcurrentSolves: number;\n\t/**\n\t * Backpressure — max solves that may WAIT in the FIFO queue (excludes the\n\t * in-flight `maxConcurrentSolves`). `0` = unbounded (`ComputeLimits.\n\t * computeMaxQueueDepth`); a full queue sheds new solves with `QUEUE_FULL`.\n\t */\n\tmaxQueueDepth: number;\n\t/**\n\t * Backpressure — max ms a solve may sit queued before executing; `0` = no\n\t * deadline (`ComputeLimits.computeQueueWaitMs`). A too-long wait sheds with\n\t * `QUEUE_TIMEOUT`.\n\t */\n\tqueueWaitMs: number;\n\t/**\n\t * Byte budget for this client's in-process solve cache, evicted LRU alongside\n\t * its entry-count cap. Applies per warm client — total worst-case heap is this\n\t * × `maxCachedClients`. `0` disables the cache entirely\n\t * (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).\n\t */\n\tresponseCacheMaxBytes: number;\n\t/** Concise cache/timing logs. When false, `onDebugLog` is never invoked. */\n\tdebug: boolean;\n\t/** VERBOSE lib-level logging (full solve request/response incl. geometry). */\n\tdebugVerbose: boolean;\n\t/** Max distinct warm clients before the LRU evicts the oldest. Default 16. */\n\tmaxCachedClients?: number;\n\t/** Sink for the concise debug lines (the app wires `console.log`). */\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/**\n\t * Solve-cache counters summed across every warm client. `warmClients` is\n\t * reported alongside since each client owns its own cache. Counters die\n\t * with the client that owns them, so totals can fall over time.\n\t */\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\n/** Aggregate solve-cache counters across the warm clients (see `solveCacheStats`). */\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_CACHED_CLIENTS = 16;\n\nexport function createClientCache(config: ClientCacheConfig): ClientCache {\n\tconst maxCachedClients = config.maxCachedClients ?? DEFAULT_MAX_CACHED_CLIENTS;\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.debugVerbose) 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\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: config.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\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: config.maxConcurrentSolves,\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 shed EVERY queued solve).\n\t\t\tmaxQueueDepth: config.maxQueueDepth || undefined,\n\t\t\tqueueWaitMs: config.queueWaitMs || undefined,\n\t\t\ttimeoutMs: config.maxSolveDurationMs,\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\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 >= maxCachedClients) {\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 ${maxCachedClients})`\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\t/** When `loaded`, whether the bytes came from a warm cache entry. */\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\n/** Hit/miss/eviction counters for observability (Server-Timing, admin debug). */\nexport interface ByteCacheStats {\n\thits: number;\n\tmisses: number;\n\t/** Entries dropped by the byte-budget LRU. */\n\tevictions: number;\n\tentries: number;\n\tbytes: number;\n}\n\nexport interface DefinitionByteCache {\n\t/**\n\t * `DefinitionRef` whose `load()` serves `versionId`'s bytes from the cache\n\t * when warm, otherwise calls `load` and caches the result. Cheap to build\n\t * and moves no bytes until `load()` runs.\n\t */\n\tgetOrLoad(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;\n\tstats(): ByteCacheStats;\n\t/** Test seam / eviction on definition delete if ever needed. */\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 preserves insertion order → iteration order IS LRU order; re-inserting\n\t// on access moves an entry to the most-recently-used (last) position.\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 * Deliberately NOT conditional on a result cache being configured: the dogpile is\n * worst precisely when nothing else is caching (N concurrent identical solves each\n * paying a full Rhino round trip — the scheduler has no in-flight coalescing of its\n * own, and Rhino's `cachesolve` still costs a round trip per repeat). Gating this on\n * cache configuration, as an earlier version did, turned it off in exactly the\n * deployments most exposed to a cold-key stampede (hot public definition + a deploy).\n *\n * In-process only, sitting above `ISolveResultCache` (per app instance). A\n * cross-instance lease (Redis `SET NX`) is a later backend capability.\n *\n * Correctness note: the shared promise resolves to ONE value for all waiters, so\n * the wrapped work must return an immutable / independently-serializable result\n * (the solve pipeline's envelope is safe to share by reference here because the\n * app serializes it per response).\n */\n\nexport interface SolveCacheSingleFlight {\n\t/**\n\t * Run `work` under `key`, coalescing concurrent identical calls: the first\n\t * caller executes, overlapping callers for the same key await the same promise\n\t * and result, and the key is freed as soon as it settles.\n\t *\n\t * `onWaiterJoined` fires on the OWNER's call each time another caller joins its\n\t * flight — ownership changes what the owner's abort signal means (a solo run\n\t * may cancel on its own client's disconnect, a shared one may not, or it would\n\t * 499 every waiter). Never fires for a joining caller, and never after `work`\n\t * settles.\n\t */\n\trun<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T>;\n\t/** Number of keys currently in flight (observability / tests). */\n\tinFlight(): number;\n}\n\nexport interface SolveCacheSingleFlightOptions {\n\t/**\n\t * Fired when a caller joins an already-in-flight key instead of running its\n\t * own work — the coalescing win this module exists for, otherwise invisible.\n\t */\n\tonJoin?: (key: string) => void;\n}\n\nexport function createSolveCacheSingleFlight(\n\toptions: SolveCacheSingleFlightOptions = {}\n): SolveCacheSingleFlight {\n\t// Each entry carries the shared promise plus the owner's join callback, so a\n\t// joining caller can notify the owner that its flight is now shared.\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// `.finally` releases the key on both settle paths. Start the work inside\n\t\t\t// the promise chain so a synchronous throw in `work` still rejects the\n\t\t\t// shared promise (and still frees the key) rather than escaping.\n\t\t\tconst flight: Flight = { notifyOwner: onWaiterJoined, promise: undefined! };\n\t\t\tconst p = (async () => work())().finally(() => {\n\t\t\t\t// Drop the callback so a late join can't fire it after the owner's\n\t\t\t\t// request has already been served.\n\t\t\t\tflight.notifyOwner = undefined;\n\t\t\t\t// Only delete if it's still OUR flight — a key freed and re-taken by a\n\t\t\t\t// later caller must not be clobbered.\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"],"mappings":"AASA,OACC,cAAAA,EACA,qBAAAC,EACA,eAAAC,MAKM,mBAEP,OAAS,QAAAC,EAAM,cAAAC,MAAkB,OACjC,OAAS,aAAAC,MAAiB,OCpB1B,OAAiD,gBAAAC,MAAoB,mBAU9D,SAASC,EACfC,EACAC,EACa,CACb,IAAMC,EAAwB,CAC7B,GAAIF,EAAM,GACV,KAAMA,EAAM,SACZ,SAAUA,EAAM,UAAY,KAC5B,YAAaA,EAAM,aAAe,GAOlC,UAAWA,EAAM,YAAc,mBAAqB,YAAcA,EAAM,UACxE,WAAYA,EAAM,iBAAmB,OACrC,QAASA,EAAM,SAAW,KAC1B,QAASA,EAAM,SAAW,KAC1B,QAAS,EACT,OAAQ,EACR,SAAUA,EAAM,SAChB,QAASC,GAASD,EAAM,OACzB,EAEMG,EAAYL,EAAaI,CAAG,EAU5BE,EAAYH,GAASD,EAAM,QACjC,OAAIG,EAAU,SAAW,MAAQC,GAAa,OACzC,MAAM,QAAQA,CAAS,GAEhBD,EAAU,YAAc,aAAe,OAAOC,GAAc,YAIrED,EAAmC,QAAUC,GAGzCD,CACR,CD/BA,IAAME,EAAYC,EAAUC,CAAI,EAOnBC,EAA2B,EAC3BC,EAAyB,0BAmI/B,SAASC,EACfC,EACAC,EACa,CACb,OAAOC,EAAY,gBAClBF,EACE,OAAQG,GAAUA,EAAM,SAAS,EACjC,IAAKA,GAAsBC,EAAwBD,EAAOF,EAAOE,EAAM,EAAE,CAAC,CAAC,CAC9E,CACD,CAEA,eAAsBE,EAAiBC,EAAgD,CACtF,GAAM,CAAE,OAAAC,EAAQ,OAAAC,CAAO,EAAIF,EAGrBG,EAAiB,YAAY,IAAI,EACjCC,EAAYJ,EAAK,WAAaP,EAAoBO,EAAK,OAAQA,EAAK,MAAM,EAC1EK,EAAc,YAAY,IAAI,EAAIF,EAUlCG,EAAkBL,EAAO,UAAU,IACnCM,EAAiBN,EAAO,YAAY,IAEtCO,EACEC,EAAa,YAAY,IAAI,EACnC,GAAI,CACHD,EAAS,MAAMP,EAAO,UAAU,MAAMD,EAAK,iBAAkBI,EAAW,CAAE,OAAAF,CAAO,CAAC,CACnF,OAASQ,EAAK,CACb,IAAMC,EAAa,YAAY,IAAI,EAAIF,EAKvC,GADgBC,aAAe,OAASA,EAAI,OAAS,aAEpD,OAAIR,EAAO,QAAgB,CAAE,KAAM,eAAgB,WAAAS,CAAW,EACvD,CACN,KAAM,UACN,WAAAA,EACA,QAAS,sBAAsB,KAAK,MAAMX,EAAK,mBAAqB,GAAI,CAAC,aAC1E,EAKD,GAAIU,aAAeE,EAAmB,CACrC,GAAIF,EAAI,OAASG,EAAW,WAC3B,MAAO,CACN,KAAM,OACN,WAAAF,EACA,OAAQ,aACR,kBAAmB,EACnB,QAAS,+CACV,EAED,GAAID,EAAI,OAASG,EAAW,cAC3B,MAAO,CACN,KAAM,OACN,WAAAF,EACA,OAAQ,gBACR,kBAAmB,EACnB,QAAS,8DACV,CAEF,CACA,MAAO,CAAE,KAAM,gBAAiB,WAAAA,EAAY,MAAOD,CAAI,CACxD,CACA,IAAMI,EAAU,YAAY,IAAI,EAAIL,EAG9BM,EAAiB,YAAY,IAAI,EACnCC,EACJ,GAAI,CACHA,EAAa,KAAK,UAAUR,CAAM,CACnC,OAASE,EAAK,CAGb,GAAIA,aAAe,WAAY,MAAO,CAAE,KAAM,WAAY,EAC1D,MAAMA,CACP,CACA,GAAIM,EAAW,OAAShB,EAAK,iBAAkB,MAAO,CAAE,KAAM,WAAY,EAC1E,IAAMiB,EAAc,YAAY,IAAI,EAAIF,EASlCG,EADkB,YAAY,KAAKlB,EAAK,cAAc,GAChBgB,EAAW,OAAS,KAC5DG,EAA4B,KAC5BC,EAAS,EACb,GAAIF,EAAkB,CACrB,IAAMG,EAAY,YAAY,IAAI,EAClCF,EAAa,MAAM/B,EAAU,OAAO,KAAK4B,CAAU,CAAC,EACpDI,EAAS,YAAY,IAAI,EAAIC,CAC9B,CAEA,IAAMC,EAAgB,YAAY,IAAI,EAAItB,EAAK,YAOzCuB,EAAStB,EAAO,UAAU,MAAQK,EAAkB,EAAIL,EAAO,UAAU,KAAO,KAChFuB,EACLD,GAAU,CAACA,EAAO,WAAatB,EAAO,YAAY,MAAQM,EAAiB,EACxEN,EAAO,YAAY,KACnB,KAgBEwB,EAAkC,CACvC,eAAgB,mBAChB,gBAfoBC,EAAkB,CACtC,UAAW1B,EAAK,UAChB,YAAAK,EACA,QAAAS,EACA,YAAAG,EACA,OAAAG,EACA,cAAAE,EACA,MAAAE,EACA,OAAAD,EACA,eAAgBvB,EAAK,eACrB,UAAWA,EAAK,WAAa,CAAC,CAC/B,CAAC,EAKA,CAACR,CAAsB,EAAG,OAAOD,CAAwB,EACzD,KAAM,iBACP,EAEMoC,EAA6B,CAClC,YAAAtB,EACA,QAAAS,EACA,YAAAG,EACA,OAAAG,EACA,cAAAE,EACA,gBAAiBN,EAAW,OAC5B,gBAAiBG,GAAY,YAAc,IAC5C,EAEIS,EACJ,OAAIT,GAGHM,EAAQ,kBAAkB,EAAI,OAC9BA,EAAQ,gBAAgB,EAAI,OAAON,EAAW,UAAU,EACxDS,EAAW,CACV,KAAM,IAAI,WAAWT,CAAU,EAC/B,SAAU,OACV,QAAAM,EACA,OAAAjB,EACA,QAAAmB,CACD,IAEAF,EAAQ,gBAAgB,EAAI,OAAO,OAAO,WAAWT,CAAU,CAAC,EAChEY,EAAW,CAAE,KAAMZ,EAAY,QAAAS,EAAS,OAAAjB,EAAQ,QAAAmB,CAAQ,GAGlD,CACN,KAAM,KACN,SAAAC,EACA,QAAAd,EACA,WAAYN,EAAO,QAAQ,QAAU,EACrC,aAAcA,EAAO,UAAU,QAAU,CAC1C,CACD,CAsBO,SAASqB,EACfD,EACAE,EACiE,CACjE,IAAMC,EAAkB,YAAY,KAAKD,CAAc,EAKvD,GAAIF,EAAS,WAAa,QAAU,CAACG,EAAiB,CACrD,IAAMC,EAAOC,EACZL,EAAS,gBAAgB,WAAaA,EAAS,KAAO,OAAO,KAAKA,EAAS,IAAI,CAChF,EAAE,SAAS,MAAM,EACXH,EAAU,CAAE,GAAGG,EAAS,OAAQ,EACtC,cAAOH,EAAQ,kBAAkB,EACjCA,EAAQ,gBAAgB,EAAI,OAAO,OAAO,WAAWO,CAAI,CAAC,EACnD,CAAE,KAAMA,EAAM,QAAAP,CAAQ,CAC9B,CAEA,MAAO,CAAE,KAAMG,EAAS,KAAM,QAASA,EAAS,OAAQ,CACzD,CA2BA,SAASF,EAAkBQ,EAAkC,CAC5D,IAAIC,EACH,YAAYD,EAAM,UAAU,QAAQ,CAAC,CAAC,cAC1BA,EAAM,YAAY,QAAQ,CAAC,CAAC,eAC3BA,EAAM,QAAQ,QAAQ,CAAC,CAAC,mBACpBA,EAAM,YAAY,QAAQ,CAAC,CAAC,cACjCA,EAAM,OAAO,QAAQ,CAAC,CAAC,eACtBA,EAAM,cAAc,QAAQ,CAAC,CAAC,GAEtCV,EAAQU,EAAM,MACpB,GAAIV,EAAO,CACV,IAAMY,EAAYZ,EAAM,OAASA,EAAM,MAAQA,EAAM,OAC/Ca,EAAgB,KAAK,IAAI,EAAGH,EAAM,QAAUE,CAAS,EAC3DD,GACC,sBAAsBX,EAAM,OAAO,QAAQ,CAAC,CAAC,qBACxBA,EAAM,MAAM,QAAQ,CAAC,CAAC,sBACrBA,EAAM,OAAO,QAAQ,CAAC,CAAC,sBACvBa,EAAc,QAAQ,CAAC,CAAC,EAChD,CAEA,OAAW,CAACC,EAAOC,CAAE,IAAKL,EAAM,UAC/BC,GAAU,OAAOG,CAAK,QAAQC,EAAG,QAAQ,CAAC,CAAC,GAG5C,IAAMhB,EAASW,EAAM,OACjBX,IACHY,GAAU,qBAAqBZ,EAAO,UAAY,EAAI,CAAC,GACnDA,EAAO,uBAAyB,SACnCY,GAAU,sBAAsBZ,EAAO,qBAAuB,EAAI,CAAC,KAOrE,IAAMiB,EAAUN,EAAM,eACtB,GAAIM,EAAS,CACZ,IAAMC,EAAWD,EAAQ,OAAqBA,EAAQ,UAAY,MAAQ,OAAxC,UAClCL,GAAU,oBAAoBM,CAAO,EACtC,CAEA,OAAON,CACR,CElcA,OACC,qBAAAO,EACA,sBAAAC,MAGM,mBAmCA,SAASC,EAAeC,EAAoD,CAClF,OAAOA,EAAO,EACf,CA6GA,IAAMC,EAA6B,GAE5B,SAASC,EAAkBC,EAAwC,CACzE,IAAMC,EAAmBD,EAAO,kBAAoBF,EAC9CI,EAAQ,IAAI,IAIZC,EAAU,IAAI,IAEhBH,EAAO,cAAcL,EAAmB,EAE5C,IAAMS,EAAYC,GAAoB,CACjCL,EAAO,OAAOA,EAAO,aAAaK,CAAO,CAC9C,EAEA,eAAeC,EAAMT,EAAwBU,EAAgD,CAC5F,IAAMC,EAA2C,CAAE,KAAM,KAAM,IAAK,CAAE,EAChEC,EAAuC,CAAE,KAAM,KAAM,IAAK,CAAE,EAE5DC,EAAyC,CAC9C,UAAWb,EAAO,UAClB,OAAQA,EAAO,OAGf,MAAOG,EAAO,aACd,WAAYA,EAAO,WACnB,mBAAoBA,EAAO,mBAC3B,eAAiBW,GAAM,CACtB,IAAMC,EAASD,EAAE,QAAU,EACrBE,EAAQF,EAAE,OAAS,EACnBG,EAASH,EAAE,QAAU,EAG3B,GAFAH,EAAY,KAAO,EACnBA,EAAY,KAAO,CAAE,OAAAI,EAAQ,MAAAC,EAAO,OAAAC,CAAO,EACvC,CAACd,EAAO,MAAO,OAGnB,IAAMe,EACLH,GAAU,EAAI,8CAAgD,oBACzDI,EACLhB,EAAO,YAAca,GAAS,EAC3B,qCACAb,EAAO,WACN,sBACA,MACLI,EACC,gCAAgCQ,CAAM,YAAYC,CAAK,aAAaC,CAAM,0BAClDC,CAAQ,gCAAgCC,CAAU,EAC3E,CACD,CACD,EAIIT,IACHG,EAAa,QAAU,CAAE,qBAAsBH,CAAe,GAG/D,IAAMU,EAAS,MAAMvB,EAAkB,OAAOgB,CAAY,EACpDQ,EAAYD,EAAO,gBAAgB,CACxC,KAAM,QAIN,cAAejB,EAAO,oBAItB,cAAeA,EAAO,eAAiB,OACvC,YAAaA,EAAO,aAAe,OACnC,UAAWA,EAAO,mBAKlB,MAAOA,EAAO,sBAAwB,EAAI,CAAE,SAAUA,EAAO,qBAAsB,EAAI,GACvF,2BAA4BA,EAAO,2BACnC,SAAU,CAACmB,EAAMC,IAAW,CAE3B,GADAX,EAAU,KAAO,EACbW,EAAO,SAAW,YACtBX,EAAU,KAAO,CAChB,UAAWW,EAAO,YAAc,GAChC,qBAAsBA,EAAO,oBAC9B,EACI,EAACpB,EAAO,QACZI,EACC,yBAAyBgB,EAAO,UAAY,kDAA+C,mCAA8B,KAAK,KAAK,MAAMA,EAAO,UAAU,CAAC,KAC5J,EACKA,EAAO,WACXhB,EACCgB,EAAO,uBAAyB,GAC7B,kFACAA,EAAO,uBAAyB,GAC/B,kFACA,0EACL,EAEG,CAACA,EAAO,YAAW,CACtB,IAAMC,EAAQD,EAAO,UAAqC,OACpDE,EAAW,MAAM,QAAQD,CAAI,EAAIA,EAAK,OAAS,EACrD,GAAIC,EAAW,EAAG,CACjB,IAAMC,EAAYvB,EAAO,mBACtB,iCACA,gEACHI,EACC,yBAAyBkB,CAAQ,iBAAiBC,CAAS,YAAY,KAAK,UAAWF,EAAmB,CAAC,CAAC,CAAC,EAC9G,CACD,CACD,CACD,CACD,CAAC,EACD,MAAO,CAAE,OAAAJ,EAAQ,UAAAC,EAAW,YAAAV,EAAa,UAAAC,CAAU,CACpD,CAEA,MAAO,CACN,MAAM,UAAUZ,EAAQ2B,EAA6B,CACpD,IAAMC,EAAM5B,EAAO,GACb6B,EAAWxB,EAAM,IAAIuB,CAAG,EAC9B,GAAIC,EAEH,OAAAxB,EAAM,OAAOuB,CAAG,EAChBvB,EAAM,IAAIuB,EAAKC,CAAQ,EAChBA,EAGR,IAAMC,EAAWxB,EAAQ,IAAIsB,CAAG,EAChC,GAAIE,EAAU,OAAOA,EAIrBvB,EAAS,sEAAiEqB,CAAG,EAAE,EAC/E,IAAMG,GAAgB,SAAY,CACjC,IAAMC,EAAQ,MAAMvB,EAAMT,EAAQ2B,GAAM,cAAc,EAEtD,GAAItB,EAAM,MAAQD,EAAkB,CACnC,IAAM6B,EAAY5B,EAAM,KAAK,EAAE,KAAK,EAAE,MAClC4B,IAAc,SACjB1B,EACC,6DAA6D0B,CAAS,SAAS7B,CAAgB,GAChG,EACAC,EAAM,IAAI4B,CAAS,GAAG,UAAU,QAAQ,EACxC5B,EAAM,OAAO4B,CAAS,EAExB,CAEA,OAAA5B,EAAM,IAAIuB,EAAKI,CAAK,EACbA,CACR,GAAG,EAAE,QAAQ,IAAM1B,EAAQ,OAAOsB,CAAG,CAAC,EACtC,OAAAtB,EAAQ,IAAIsB,EAAKG,CAAY,EACtBA,CACR,EAEA,MAAMG,EAAU,CACf,IAAMN,EAAMM,EACNF,EAAQ3B,EAAM,IAAIuB,CAAG,EACvBI,IACHzB,EAAS,yDAAyDqB,CAAG,kBAAkB,EACvFI,EAAM,UAAU,QAAQ,EACxB3B,EAAM,OAAOuB,CAAG,EAElB,EAEA,iBAAmC,CAClC,IAAMO,EAAyB,CAC9B,YAAa9B,EAAM,KACnB,QAAS,EACT,MAAO,EACP,KAAM,EACN,OAAQ,EACR,UAAW,CACZ,EACA,QAAW2B,KAAS3B,EAAM,OAAO,EAAG,CACnC,IAAM+B,EAAIJ,EAAM,UAAU,WAAW,EACrCG,EAAM,SAAWC,EAAE,QACnBD,EAAM,OAASC,EAAE,MACjBD,EAAM,MAAQC,EAAE,KAChBD,EAAM,QAAUC,EAAE,OAClBD,EAAM,WAAaC,EAAE,SACtB,CACA,OAAOD,CACR,EAEA,kBAAyB,CACxB5B,EAAS,kDAAkDF,EAAM,IAAI,iBAAiB,EACtF,QAAW2B,KAAS3B,EAAM,OAAO,EAAG2B,EAAM,UAAU,WAAW,CAChE,EAEA,YAAmB,CAClBzB,EAAS,wCAAwCF,EAAM,IAAI,iBAAiB,EAC5E,QAAW2B,KAAS3B,EAAM,OAAO,EAAG2B,EAAM,UAAU,QAAQ,EAC5D3B,EAAM,MAAM,CACb,CACD,CACD,CCnRO,SAASgC,EAA0BC,EAAuC,CAChF,IAAMC,EAAS,KAAK,IAAI,EAAG,KAAK,MAAMD,CAAQ,CAAC,EAGzCE,EAAQ,IAAI,IACdC,EAAgB,EACdC,EAAQ,CAAE,KAAM,EAAG,OAAQ,EAAG,UAAW,CAAE,EAEjD,SAASC,EAAMC,EAAaC,EAAoB,CAC/CL,EAAM,OAAOI,CAAG,EAChBJ,EAAM,IAAII,EAAKC,CAAK,CACrB,CAEA,SAASC,GAAmB,CAC3B,KAAOL,EAAgBF,GAAUC,EAAM,KAAO,GAAG,CAChD,IAAMO,EAAYP,EAAM,KAAK,EAAE,KAAK,EAAE,MAChCQ,EAASR,EAAM,IAAIO,CAAS,EAClCP,EAAM,OAAOO,CAAS,EACtBN,GAAiBO,EAAO,MAAM,WAC9BN,EAAM,WAAa,CACpB,CACD,CAEA,SAASO,EAAML,EAAaM,EAAyB,CAIpD,GAHIX,IAAW,GAGXW,EAAM,WAAaX,EAAQ,OAC/B,IAAMY,EAAWX,EAAM,IAAII,CAAG,EAC1BO,IAAUV,GAAiBU,EAAS,MAAM,YAC9CX,EAAM,IAAII,EAAK,CAAE,MAAAM,CAAM,CAAC,EACxBT,GAAiBS,EAAM,WACvBJ,EAAW,CACZ,CAEA,MAAO,CACN,UAAUM,EAAWC,EAAM,CAC1B,IAAMC,EAA0B,CAAE,OAAQ,GAAO,UAAW,EAAM,EAClE,MAAO,CACN,IAAKF,EACL,QAAAE,EACA,KAAM,SAAY,CACjB,IAAMC,EAAMf,EAAM,IAAIY,CAAS,EAC/B,GAAIG,EACH,OAAAZ,EAAMS,EAAWG,CAAG,EACpBb,EAAM,MAAQ,EACdY,EAAQ,OAAS,GACjBA,EAAQ,UAAY,GACbC,EAAI,MAEZb,EAAM,QAAU,EAChBY,EAAQ,OAAS,GACjBA,EAAQ,UAAY,GACpB,IAAMJ,EAAQ,MAAMG,EAAK,EACzB,OAAAJ,EAAMG,EAAWF,CAAK,EACfA,CACR,CACD,CACD,EACA,OAAQ,CACP,MAAO,CACN,KAAMR,EAAM,KACZ,OAAQA,EAAM,OACd,UAAWA,EAAM,UACjB,QAASF,EAAM,KACf,MAAOC,CACR,CACD,EACA,OAAQ,CACPD,EAAM,MAAM,EACZC,EAAgB,CACjB,CACD,CACD,CClGO,SAASe,EACfC,EAAyC,CAAC,EACjB,CAOzB,IAAMC,EAAW,IAAI,IAErB,MAAO,CACN,IAAOC,EAAaC,EAAwBC,EAAyC,CACpF,IAAMC,EAAWJ,EAAS,IAAIC,CAAG,EACjC,GAAIG,EACH,OAAAL,EAAQ,SAASE,CAAG,EACpBG,EAAS,cAAc,EAChBA,EAAS,QAMjB,IAAMC,EAAiB,CAAE,YAAaF,EAAgB,QAAS,MAAW,EACpEG,GAAK,SAAYJ,EAAK,GAAG,EAAE,QAAQ,IAAM,CAG9CG,EAAO,YAAc,OAGjBL,EAAS,IAAIC,CAAG,IAAMI,GAAQL,EAAS,OAAOC,CAAG,CACtD,CAAC,EACD,OAAAI,EAAO,QAAUC,EACjBN,EAAS,IAAIC,EAAKI,CAAM,EACjBC,CACR,EACA,UAAmB,CAClB,OAAON,EAAS,IACjB,CACD,CACD","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_CACHED_CLIENTS","createClientCache","config","maxCachedClients","cache","pending","debugLog","message","build","definitionGuid","rhinoTiming","solveMeta","clientConfig","t","decode","solve","encode","defCache","solveCache","client","scheduler","_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"]}
|
package/dist/shared.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/selva/selva/packages/solve/dist/shared.cjs"],"names":[],"mappings":"AAAA","file":"/home/runner/work/selva/selva/packages/solve/dist/shared.cjs"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/solve",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
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/schemas": "4.7.0",
|
|
63
62
|
"@selvajs/platform": "^0.16.0",
|
|
64
|
-
"@selvajs/
|
|
63
|
+
"@selvajs/schemas": "4.7.0",
|
|
64
|
+
"@selvajs/compute": "^4.0.0-beta.1"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@eslint/js": "^10.0.1",
|