@selvajs/compute 5.0.0-beta.8 → 5.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,3052 @@
1
- import{C as e,D as t,O as n,S as r,T as i,_ as a,c as o,d as s,f as c,h as l,i as u,l as d,m as f,p,s as m,t as ee,u as te,v as ne,x as h,y as re}from"./core-BPYXUtfq.js";var g=class e{serverUrl;headers;disposed=!1;activeMonitors=/* @__PURE__ */ new Set;static DEFAULT_TIMEOUT_MS=5e3;static LIFECYCLE_TIMEOUT_MS=6e4;static MIN_MONITOR_INTERVAL_MS=100;static MAX_BODY_BYTES=1<<20;constructor(e,t){this.serverUrl=h(e),this.headers={...re(typeof t==`string`?{apiKey:t}:t??{}),"Content-Type":`application/json`}}fetchWithTimeout(t,n={},r=e.DEFAULT_TIMEOUT_MS){let i={...n,headers:this.headers,redirect:`manual`};return r>0&&!i.signal&&(i.signal=AbortSignal.timeout(r)),fetch(t,i)}async getText(t,n={},r=e.DEFAULT_TIMEOUT_MS){this.ensureNotDisposed();let a=`${n.method??`GET`} ${t}`;try{let o=await this.fetchWithTimeout(`${this.serverUrl}${t}`,n,r);if(!o.ok)return i().warn(`[ComputeServerStats] ${a} failed:`,o.status),await o.body?.cancel().catch(()=>{}),null;let{text:s,truncated:c}=await f(o,e.MAX_BODY_BYTES);return c?(i().warn(`[ComputeServerStats] ${a} answered with an oversized body`),null):s}catch(e){return i().warn(`[ComputeServerStats] ${a} errored:`,e),null}}async isServerOnline(t=e.DEFAULT_TIMEOUT_MS){return(await this.probeServer(t)).online}async probeServer(t=e.DEFAULT_TIMEOUT_MS){this.ensureNotDisposed();let n=`${this.serverUrl}/`;try{let e=await this.fetchWithTimeout(n,{method:`GET`},t);return await e.body?.cancel().catch(()=>{}),{online:e.ok||s(e),status:e.status}}catch(e){return i().debug(`[ComputeServerStats] Fetch error:`,e),{online:!1,error:e instanceof Error?`${e.name}: ${e.message}`:String(e)}}}async getActiveChildren(t={}){let{initialize:n=!0}=t,r=n?await this.getText(`/activechildren`,{},e.LIFECYCLE_TIMEOUT_MS):await this.getText(`/activechildren?initialize=false`);if(r===null)return null;let a=r.trim();return/^\d+$/.test(a)?parseInt(a,10):(i().warn(`[ComputeServerStats] /activechildren answered with a non-numeric body`),null)}async getVersion(){let e=await this.getText(`/version`);if(e===null)return null;let t=_(e);return t==null?{rhino:e,compute:``,git_sha:null}:{rhino:t.rhino??``,compute:t.compute??``,git_sha:t.git_sha??null}}async getInstalledPlugins(e=`gh`){let t=await this.getText(`/plugins/${e}/installed`),n=t===null?void 0:_(t);return n&&typeof n==`object`?n:null}async getServerStats(){if(this.ensureNotDisposed(),!await this.isServerOnline())return{isOnline:!1};let[e,t]=await Promise.all([this.getVersion(),this.getActiveChildren({initialize:!1})]);return{isOnline:!0,...e&&{version:e},...t!==null&&{activeChildren:t}}}async purgeCache(){let e=await this.getText(`/cache/purge`,{method:`POST`}),t=(e===null?void 0:_(e))?.purged;return typeof t==`number`?t:null}async purgeAllChildren(){this.ensureNotDisposed();let e=await this.getActiveChildren({initialize:!1});if(e===null)return i().warn(`[ComputeServerStats] purgeAllChildren: could not read child count`),null;if(e===0)return{totalPurged:0,calls:0,children:0,confident:!0};let t=e*2,n=0;for(let e=0;e<t;e++){let e=await this.purgeCache();e!==null&&(n+=e)}return{totalPurged:n,calls:t,children:e,confident:e===1}}async getServerTime(){let e=await this.getText(`/servertime`);if(e===null)return null;let t=new Date(e.trim().replace(/^"|"$/g,``));return isNaN(t.getTime())?null:t}async getIdleSpan(){let e=await this.getText(`/idlespan`);if(e===null)return null;let t=parseFloat(e.trim());return isNaN(t)?null:t}async launchChildren(){return this.postJson(`/launch-children`)}async launchChild(e){let t=e===void 0?`/launch-child`:`/launch-child?port=${e}`;return this.postJson(t)}async shutdownChildren(e){let t=e===void 0?`/shutdown-children`:`/shutdown-children?port=${e}`;return this.postJson(t)}async recycleChildren(e){let t=e===void 0?`/recycle-children`:`/recycle-children?port=${e}`;return this.postJson(t)}async postJson(t){let n=await this.getText(t,{method:`POST`},e.LIFECYCLE_TIMEOUT_MS);return n===null?null:_(n)??null}monitor(r,a=5e3){if(this.ensureNotDisposed(),!Number.isFinite(a)||a<e.MIN_MONITOR_INTERVAL_MS)throw new t(`monitor() intervalMs must be a finite number >= ${e.MIN_MONITOR_INTERVAL_MS}ms, got ${a}. Sub-${e.MIN_MONITOR_INTERVAL_MS}ms polling would hot-loop the server.`,n.INVALID_CONFIG,{context:{intervalMs:a}});let o=!0,s=null;i().info(`🔄 Starting server stats monitoring every ${a}ms`);let c=async()=>{if(s=null,o&&!this.disposed){try{let e=await this.getServerStats();if(!o||this.disposed)return;try{r(e)}catch(e){i().error(`[ComputeServerStats] Monitor callback threw:`,e)}}catch(e){!o||this.disposed?i().debug(`[ComputeServerStats] Monitor poll cancelled during shutdown:`,e):i().error(`[ComputeServerStats] Failed to fetch stats during monitor:`,e)}o&&!this.disposed&&(s=setTimeout(()=>void c(),a))}},l=()=>{o=!1,s!==null&&(clearTimeout(s),s=null),this.activeMonitors.delete(l)};return this.activeMonitors.add(l),c(),l}async dispose(){if(!this.disposed){this.disposed=!0;for(let e of this.activeMonitors)e();this.activeMonitors.clear()}}ensureNotDisposed(){if(this.disposed)throw new t(`ComputeServerStats has been disposed and cannot be used`,n.INVALID_STATE)}};function _(e){try{return JSON.parse(e)}catch{return}}const v=/* @__PURE__ */ new Set;function ie(){if(typeof window>`u`||globalThis.process?.versions?.node!=null)return!1;let e=globalThis.navigator,t=typeof e?.userAgent==`string`?e.userAgent:``;return!/jsdom/i.test(t)}function y(e,t){t||ie()&&!v.has(e)&&(v.add(e),i().warn(`Warning: ${e} is running on the client side. For better performance and security, consider running this on the server side.`))}const ae={definition_not_cached:n.DEFINITION_NOT_CACHED};function oe(e){return{...e,serverErrorCodes:{...ae,...e.serverErrorCodes}}}function se(e){return e instanceof t?e.code===n.DEFINITION_NOT_CACHED||e.message.includes(`Unable to load grasshopper definition`):!1}function ce(t,n){if(!n)return;let r=e(t,`values`);if(!Array.isArray(r)||r.length===0)return;let a=[];for(let t of r){let n=e(t,`innerTree`);n&&Object.keys(n).length!==0||a.push(e(t,`paramName`)??`<unnamed>`)}if(a.length===0)return;let o=a.length===r.length?`all`:`${a.length}/${r.length}`;i().warn(`Solve returned empty output(s) (${o}): ${a.join(`, `)}. These parameters produced no data — check the definition's inputs and the branch feeding each.`)}async function le(e,t,n){return y(`solveGrasshopperDefinition`,n.suppressBrowserWarning),(await de(e,t,n)).response}async function ue(e,t,n,r){if(y(`solveWithServerCacheKey`,r.suppressBrowserWarning),t!==null)try{return{...await pe({algo:null,pointer:t,values:e},r),missed:!1}}catch(e){if(!se(e))throw e}return{...await de(e,n,r),missed:!0}}async function de(e,t,n){return pe(b(await fe(t),e),n)}async function fe(e){if(!m(e))return e;try{return await e.load()}catch(r){throw new t(`Failed to load definition bytes for ref '${e.key}'`,n.INVALID_INPUT,{context:{definitionKey:e.key},originalError:r})}}async function pe(e,t){h(t.serverUrl),me(e,t);let{maxResponseBytes:n}=t,r=await d(`grasshopper`,e,oe({...t,maxResponseBytes:n!==void 0&&e.algo?n+e.algo.length:n})),{pointer:i,algo:a,...o}=r,s=o,l=c(r);if(l!==void 0){let e=typeof a==`string`?a.length:0;p(s,Math.max(0,l-e))}return ce(s,t.debug),{response:s,cacheKey:typeof i==`string`?i:null}}function b(e,t){let n={algo:null,pointer:null,values:t};return e instanceof Uint8Array?n.algo=l(e):o(e)?n.pointer=e:n.algo=a(e)??ne(e),n}function me(e,t){t.cachesolve!=null&&(e.cachesolve=t.cachesolve),t.modelunits!=null&&(e.modelunits=t.modelunits),t.angletolerance!=null&&(e.angletolerance=t.angletolerance),t.absolutetolerance!=null&&(e.absolutetolerance=t.absolutetolerance),t.dataversion!=null&&(e.dataversion=t.dataversion),t.selvaevents!=null&&(e.selvaevents=t.selvaevents)}function x(e,t,n){t===`__proto__`?Object.defineProperty(e,t,{value:n,writable:!0,enumerable:!0,configurable:!0}):e[t]=n}const S=/^\{(-?\d+(?:;-?\d+)*)?\}$/;function C(e){if(typeof e!=`object`||!e||Array.isArray(e))return!1;let t=Object.entries(e);return t.length>0&&t.every(([e,t])=>S.test(e)&&Array.isArray(t))}function w(e,t,n){if(e==null)return e;if(Array.isArray(e)){let n=e.map(t).filter(e=>e!==null);return n.length>0?n:void 0}let r=t(e);return r===null?n?void 0:e:r}const T=e=>{if(typeof e==`number`)return Number.isFinite(e)?e:null;if(typeof e==`string`){let t=e.trim();if(t===``||/^[+-]?0[xbo]/i.test(t))return null;let n=Number(t);return Number.isFinite(n)?n:null}return null},E=e=>{if(typeof e==`boolean`)return e;if(typeof e==`string`){let t=e.trim().toLowerCase();if(t===`true`)return!0;if(t===`false`)return!1}return null},he=e=>typeof e==`string`?e.length>=2&&e.startsWith(`"`)&&e.endsWith(`"`)?e.slice(1,-1):e.startsWith(`"`)?e.slice(1):e:null,ge=e=>{if(typeof e==`string`){let t=e.trim();return t.startsWith(`"`)&&t.endsWith(`"`)&&(t=t.slice(1,-1).trim()),t}return null};function D(e){return t=>{if(typeof t==`object`&&t)return t;if(typeof t==`string`&&t.trim()!==``)try{let n=JSON.parse(t);return typeof n==`object`&&n?n:(i().warn(`Parsed value for input ${e} is not an object`),null)}catch(n){return i().warn(`Failed to parse object value "${t}" for input ${e}`,n),null}return null}}const _e=/* @__PURE__ */ new Set([`System.Double`,`System.Single`,`System.Decimal`,`System.Int32`,`System.Int64`]),ve=/* @__PURE__ */ new Set([`System.Int32`,`System.Int64`]);function O(e,t){return i().warn(t),{schema:{...e,default:null},warning:{code:n.MALFORMED_DEFAULT,message:t}}}function ye(t,a){if(typeof t.default!=`object`||t.default===null||Array.isArray(t.default)||C(t.default))return{schema:t};if(!r(t.default,`innerTree`))return O(t,`Input "${a}" default had an unrecognized shape (no innerTree key); the default was dropped.`);let o=e(t.default,`innerTree`)??{};if(Object.keys(o).length===0)return{schema:{...t,default:void 0}};if(t.treeAccess||t.atMost&&t.atMost>1){let r={},s=[];for(let[n,i]of Object.entries(o)){if(!Array.isArray(i))return O(t,`Input "${a}" default had a non-array innerTree branch ("${n}"); the default was dropped.`);let o=[];for(let t of i){let n=e(t,`data`),r=e(t,`type`);if(r&&_e.has(r)){if(typeof n==`string`&&n.trim()===``)continue;let e=T(n);if(e===null){s.push(`"${String(n)}" (${r})`);continue}o.push(ve.has(r)?Math.round(e):e)}else if(r===`System.Boolean`){let e=E(n);if(e===null){s.push(`"${String(n)}" (${r})`);continue}o.push(e)}else if(typeof n==`string`&&r?.startsWith(`Rhino.Geometry`))try{o.push(JSON.parse(n))}catch{o.push(n)}else o.push(n)}x(r,n,o)}if(s.length>0){let e=`Input "${a}" default contained ${s.length} tree value(s) that could not be parsed and were dropped: ${s.join(`, `)}.`;return i().warn(e),{schema:{...t,default:r},warning:{code:n.MALFORMED_DEFAULT,message:e}}}return{schema:{...t,default:r}}}let s=[];for(let t of Object.values(o))if(Array.isArray(t))for(let n of t)n&&typeof n==`object`&&r(n,`data`)&&s.push(e(n,`data`));return{schema:{...t,default:s.length>1?s:s[0]}}}function be(e,t){let n=Number(e.toFixed(t));return Math.abs(e-n)<1e-8?n:e}function k(e){if(!Number.isFinite(e))return 0;let t=Number(Math.abs(e).toPrecision(15)),[n,r=`0`]=String(t).split(`e`),i=(n.split(`.`)[1]?.length??0)-Number(r);return Math.min(Math.max(i,0),12)}function xe(e){if(!Number.isFinite(e)||e===0)return .1;let t=k(e);return t===0?Math.abs(e)>=1?1:.1:Number((10**-t).toFixed(t))}function A(e){return typeof e.stepSize==`number`&&Number.isFinite(e.stepSize)&&e.stepSize>0?e.stepSize:void 0}const j=(e,t)=>(e.atMost??1)>1?[t]:t,M=e=>A(e)??(e.paramType===`Integer`?1:.1),N=(e,t)=>Array.isArray(e)?e.map(e=>typeof e==`number`?t(e):e):typeof e==`number`?t(e):e;function Se(e){if(C(e.default))return{default:e.default,stepSize:M(e)};if(typeof e.default==`string`&&e.default.trim()!==``&&T(e.default)===null)throw new t(`Invalid numeric default "${e.default}" for input "${e.name||`unknown`}"`,n.VALIDATION_ERROR,{context:{inputName:e.name,default:e.default}});let r=w(e.default,T,!0);if(e.paramType===`Integer`)return{default:N(r,Math.round),stepSize:M(e)};let i=Array.isArray(r)?r[0]:r,a=[i,e.minimum,e.maximum].find(e=>typeof e==`number`&&Number.isFinite(e)&&e!==0),o=A(e)??(a===void 0?.1:xe(a)),s=k(o);if(s===0&&typeof i==`number`&&i!==0&&Math.abs(i)<1){let e=Math.ceil(-Math.log10(Math.abs(i)));Number.isFinite(e)&&e>0&&(s=e)}return s=Math.min(Math.max(s,0),12),{default:N(r,e=>be(e,s)),stepSize:o}}const P={parse(e,t){let{default:n,stepSize:r}=Se(e);return{...t,paramType:e.paramType,minimum:e.minimum,maximum:e.maximum,atLeast:e.atLeast,atMost:e.atMost,stepSize:r,default:n}},fallback(e,t){let n=typeof e.minimum==`number`&&Number.isFinite(e.minimum)&&e.minimum>0?e.minimum:0;return{...t,paramType:e.paramType,minimum:e.minimum,maximum:e.maximum,atLeast:e.atLeast,atMost:e.atMost,stepSize:M(e),default:j(e,n)}}},Ce={parse(e,r){if(C(e.default))return{...r,paramType:`Boolean`,default:e.default};let i=w(e.default,E,!1);if(i!=null&&typeof i!=`boolean`&&!Array.isArray(i))throw new t(`Invalid boolean default "${String(i)}" for input "${e.name||`unknown`}"`,n.VALIDATION_ERROR,{context:{inputName:e.name,default:e.default}});return{...r,paramType:`Boolean`,default:i}},fallback(e,t){return{...t,paramType:`Boolean`,default:j(e,!1)}}},we={parse(e,t){let n=w(e.default,he,!1);return{...t,paramType:`Text`,default:n}}},Te={parse(e,r,a){if(!e.values||typeof e.values!=`object`||Object.keys(e.values).length===0){let r=e.nickname||`unnamed`;throw new t(`Input "${r}" has no values defined (expected ValueList)`,n.INVALID_INPUT,{context:{inputName:r,expectedType:`ValueList`}})}let o=e.default;if(e.default!==void 0&&e.default!==null){if(typeof e.default==`object`)throw new t(`ValueList input "${e.nickname||`unnamed`}" default is not a string-able value`,n.VALIDATION_ERROR,{context:{inputName:e.name,default:e.default}});let r=String(e.default).toLowerCase(),s=Object.keys(e.values).find(e=>e.toLowerCase()===r);if(s!==void 0)o=s;else{let t=`ValueList input "${e.nickname||`unnamed`}" default value "${e.default}" is not in available values`;i().warn(t),a?.({code:n.VALIDATION_ERROR,message:t})}}return{...r,paramType:`ValueList`,values:e.values,default:o}},fallback(e,t){return{...t,paramType:`ValueList`,values:e.values&&typeof e.values==`object`?e.values:{},default:void 0}}};function F(e,t){return{...t,paramType:`Geometry`,default:j(e,null)}}const I=/* @__PURE__ */ new Map([[`Number`,P],[`Integer`,P],[`Boolean`,Ce],[`Text`,we],[`ValueList`,Te],[`Geometry`,{parse(e,t){let n=w(e.default,D(e.nickname||`unnamed`),!0);return{...t,paramType:`Geometry`,default:n}},fallback:F}],[`File`,{parse(e,t){let n=w(e.default,D(e.nickname||`unnamed`),!0);return{...t,paramType:`File`,acceptedFormats:e.acceptedFormats,default:n}}}],[`Color`,{parse(e,t){let n=w(e.default,ge,!1);return{...t,paramType:`Color`,default:n}}}]]),Ee=F,De=new Map([...I.keys()].map(e=>[e.toLowerCase(),e]));function Oe(e){return De.get(e?.toLowerCase())??e}function ke(e){return L(e).input}function L(e){let r=e.name||`unknown`,a=[],o=({code:t,message:n})=>{a.push({inputName:r,paramType:e.paramType,message:n,code:t})},s={description:e.description,name:e.name,nickname:e.nickname,treeAccess:e.treeAccess,groupName:e.groupName??void 0,id:e.id},c=Oe(e.paramType),l={...e,paramType:c};try{let{schema:e,warning:t}=ye(l,r);l=e,t&&o(t)}catch(e){l={...l,default:null},o({code:n.MALFORMED_DEFAULT,message:`Input "${r}" default could not be normalized and was dropped: ${e instanceof Error?e.message:String(e)}`})}let u=I.get(c);try{if(!u)throw new t(`Unknown paramType: ${c}`,n.VALIDATION_ERROR,{context:{receivedParamType:c,paramName:e.name}});return{input:u.parse(l,s,o),errors:a}}catch(d){if(!(d instanceof t))throw new t(d instanceof Error?d.message:String(d),n.VALIDATION_ERROR,{context:{paramName:e.name,paramType:c},originalError:d});return i().error(`Validation error for input ${r}:`,d.message),o(d),{input:(u?.fallback??Ee)(l,s),errors:a}}}function Ae(e){let t=[],n=[];for(let r of e){let{input:e,errors:i}=L(r);t.push(e),n.push(...i)}return{inputs:t,parseErrors:n}}function je(t){let n=e(t,`stepSize`);return{id:e(t,`id`)??``,name:e(t,`name`)??``,nickname:e(t,`nickname`)??null,description:e(t,`description`)??``,paramType:e(t,`paramType`)??``,treeAccess:e(t,`treeAccess`)??!1,minimum:e(t,`minimum`)??null,maximum:e(t,`maximum`)??null,atLeast:e(t,`atLeast`)??1,atMost:e(t,`atMost`)??1,stepSize:typeof n==`number`&&Number.isFinite(n)?n:void 0,default:e(t,`default`),values:e(t,`values`),acceptedFormats:e(t,`acceptedFormats`),groupName:e(t,`groupName`)??null}}function Me(t){return{name:e(t,`name`)??``,nickname:e(t,`nickname`)??null,paramType:e(t,`paramType`)??``,id:e(t,`id`)??``}}async function R(r,i){y(`fetchDefinitionIO`,i.suppressBrowserWarning),h(i.serverUrl);let{algo:a,pointer:o}=b(r,[]);if(!a&&!o)throw new t(`Definition must resolve to either a URL pointer or base64 algo`,n.INVALID_INPUT);let s=await d(`io`,a?{algo:a}:{pointer:o},oe(i));if(!s||typeof s!=`object`)throw new t(`Invalid IO response structure`,n.INVALID_INPUT,{context:{response:s}});let c=z(e(s,`warnings`)),l=z(e(s,`errors`)),u=e(s,`inputs`),f=e(s,`outputs`);return{inputs:Array.isArray(u)?u.map(je):[],outputs:Array.isArray(f)?f.map(Me):[],...c&&{loadWarnings:c},...l&&{loadErrors:l}}}function z(e){if(!Array.isArray(e))return;let t=e.map(Ne).filter(e=>e!==void 0);return t.length>0?t:void 0}function Ne(t){if(t==null)return;let n;if(typeof t==`string`)n=t;else if(typeof t==`object`){let r=e(t,`message`);if(typeof r==`string`&&r.trim().length>0)n=r;else try{n=JSON.stringify(t)??String(t)}catch{n=String(t)}}else n=String(t);return n.trim().length>0?n:void 0}async function B(e,t){let{inputs:n,outputs:r,loadWarnings:i,loadErrors:a}=await R(e,t),{inputs:o,parseErrors:s}=Ae(n);return{inputs:o,outputs:r,...s.length>0&&{parseErrors:s},...i&&{loadWarnings:i},...a&&{loadErrors:a}}}const V=globalThis.process?.getBuiltinModule?.(`node:crypto`);function H(e){if(V){let t=V.createHash(`sha256`);return typeof e==`string`?t.update(e,`utf16le`):t.update(e),t.digest(`hex`)}return Fe(e)}const Pe=new Int32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]);function Fe(e){let t=new Ie;return typeof e==`string`?t.updateString(e):t.update(e),t.digestHex()}var Ie=class{h=new Int32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]);w=/* @__PURE__ */ new Int32Array(64);pending=/* @__PURE__ */ new Uint8Array(64);pendingLength=0;totalBytes=0;update(e){this.totalBytes+=e.length;let t=0;if(this.pendingLength>0){let n=Math.min(64-this.pendingLength,e.length);if(this.pending.set(e.subarray(0,n),this.pendingLength),this.pendingLength+=n,t=n,this.pendingLength<64)return;this.compressBytes(this.pending,0),this.pendingLength=0}for(;t+64<=e.length;t+=64)this.compressBytes(e,t);this.pending.set(e.subarray(t),0),this.pendingLength=e.length-t}updateString(e){let t=this.w,n=e.length-e.length%32;for(let r=0;r<n;r+=32){for(let n=0;n<16;n++){let i=e.charCodeAt(r+2*n),a=e.charCodeAt(r+2*n+1);t[n]=(i&255)<<24|i>>>8<<16|(a&255)<<8|a>>>8}this.compress()}this.totalBytes+=n*2;let r=new Uint8Array((e.length-n)*2);for(let t=n,i=0;t<e.length;t++){let n=e.charCodeAt(t);r[i++]=n&255,r[i++]=n>>>8}this.update(r)}digestHex(){let e=this.totalBytes*8,t=new Uint8Array(this.pendingLength<56?64:128);t.set(this.pending.subarray(0,this.pendingLength)),t[this.pendingLength]=128;let n=new DataView(t.buffer);n.setUint32(t.length-8,Math.floor(e/4294967296)),n.setUint32(t.length-4,e>>>0);for(let e=0;e<t.length;e+=64)this.compressBytes(t,e);let r=``;for(let e of this.h)r+=(e>>>0).toString(16).padStart(8,`0`);return r}compressBytes(e,t){let n=this.w;for(let r=0;r<16;r++){let i=t+r*4;n[r]=e[i]<<24|e[i+1]<<16|e[i+2]<<8|e[i+3]}this.compress()}compress(){let e=this.w;for(let t=16;t<64;t++){let n=e[t-15],r=e[t-2],i=(n>>>7|n<<25)^(n>>>18|n<<14)^n>>>3,a=(r>>>17|r<<15)^(r>>>19|r<<13)^r>>>10;e[t]=e[t-16]+i+e[t-7]+a|0}let t=this.h,n=t[0],r=t[1],i=t[2],a=t[3],o=t[4],s=t[5],c=t[6],l=t[7];for(let t=0;t<64;t++){let u=(o>>>6|o<<26)^(o>>>11|o<<21)^(o>>>25|o<<7),d=o&s^~o&c,f=l+u+d+Pe[t]+e[t]|0,p=((n>>>2|n<<30)^(n>>>13|n<<19)^(n>>>22|n<<10))+(n&r^n&i^r&i)|0;l=c,c=s,s=o,o=a+f|0,a=i,i=r,r=n,n=f+p|0}t[0]=t[0]+n|0,t[1]=t[1]+r|0,t[2]=t[2]+i|0,t[3]=t[3]+a|0,t[4]=t[4]+o|0,t[5]=t[5]+s|0,t[6]=t[6]+c|0,t[7]=t[7]+l|0}};function U(e){let t=/* @__PURE__ */ new WeakSet,n=e=>{if(e===null)return`null`;if(typeof e==`number`)return Number.isFinite(e)?String(e):`null`;if(typeof e==`string`||typeof e==`boolean`)return JSON.stringify(e);if(typeof e==`bigint`)return`${e}n`;if(typeof e!=`object`)return`undefined`;if(e instanceof Uint8Array)return`{u8:${H(e)}}`;if(t.has(e))return`[Circular]`;t.add(e);let r;if(Array.isArray(e)){let t=[];for(let r=0;r<e.length;r++)t.push(n(e[r]));r=`[${t.join(`,`)}]`}else r=typeof e.toJSON==`function`?n(e.toJSON()):e instanceof Map?`{map:[${[...e.entries()].map(([e,t])=>`[${n(e)},${n(t)}]`).sort().join(`,`)}]}`:e instanceof Set?`{set:[${[...e.values()].map(n).sort().join(`,`)}]}`:`{${Object.keys(e).sort().map(t=>`${JSON.stringify(t)}:${n(e[t])}`).join(`,`)}}`;return t.delete(e),r};return n(e)}function Le(e,t){return W(G(e),t)}function W(e,t){return`${e}|t:${H(U(t))}`}function G(e){return m(e)?`r:${e.key}`:typeof e==`string`?`s:${H(e)}`:`u8:${H(e)}`}const K=/* @__PURE__ */ new WeakMap;function Re(e){if(!(e instanceof Uint8Array))return G(e);let t=K.get(e);return t===void 0&&(t=G(e),K.set(e,t)),t}var ze=class{hashDefinition;dataTree;definitionMemo;keyMemo;constructor(e,t){this.hashDefinition=e,this.dataTree=t}get definitionHash(){return this.definitionMemo??=this.hashDefinition()}get key(){return this.keyMemo??=W(this.definitionHash,this.dataTree)}},Be=class{items=[];head=0;live=/* @__PURE__ */ new Set;get size(){return this.live.size}push(e){this.items.push(e),this.live.add(e)}remove(e){this.live.delete(e)&&this.compact()}shift(){for(;this.head<this.items.length;){let e=this.items[this.head];if(this.items[this.head++]=void 0,e&&this.live.delete(e))return this.compact(),e}}compact(){if(this.live.size===0){this.items=[],this.head=0;return}let e=this.items.length-this.live.size;e<=64||e<=this.live.size||(this.items=this.items.filter(e=>e!==void 0&&this.live.has(e)),this.head=0)}};function Ve(e){try{return JSON.stringify(e)?.length??0}catch{return 0}}var He=class{executor;baseConfig;mode;maxConcurrent;maxQueueDepth;queueWaitMs;timeoutMs;retry;cacheEnabled;cacheMaxBytes;cacheTtl;cacheErroredSolves;cache=/* @__PURE__ */ new Map;cacheBytes=0;cacheHits=0;cacheMisses=0;cacheEvictions=0;reuseServerDefinitionCache;serverCacheKeys=/* @__PURE__ */ new Map;lastStringDefinition=null;onStart;onSettle;onSuperseded;subscribers=/* @__PURE__ */ new Set;inFlight=/* @__PURE__ */ new Set;pendingForLatestWins=null;fifo=new Be;_lastResult=null;_lastError=null;_lastDurationMs=null;solveSeq=0;lastStateSeq=0;disposed=!1;constructor(e,r,i={}){if(this.executor=e,this.baseConfig=r,this.mode=i.mode??`latest-wins`,this.mode!==`latest-wins`&&this.mode!==`queue`)throw new t(`Unknown scheduler mode '${String(this.mode)}'. For concurrent solves use { mode: 'queue', maxConcurrent: 4 }.`,n.INVALID_CONFIG,{context:{mode:this.mode}});this.maxConcurrent=Math.max(1,i.maxConcurrent??1),this.maxQueueDepth=i.maxQueueDepth!==void 0&&i.maxQueueDepth>0?Math.floor(i.maxQueueDepth):void 0,this.queueWaitMs=i.queueWaitMs!==void 0&&i.queueWaitMs>0?i.queueWaitMs:void 0,this.timeoutMs=i.timeoutMs,this.retry=i.retry;let a=i.cache,o=typeof a==`object`?a:null;this.cacheMaxBytes=Math.max(0,o?.maxBytes??0),this.cacheEnabled=this.cacheMaxBytes>0,this.cacheTtl=o?.ttlMs??0,this.cacheErroredSolves=o?.cacheErroredSolves??!0,this.reuseServerDefinitionCache=i.reuseServerDefinitionCache??!0,this.onStart=i.onStart,this.onSettle=i.onSettle,this.onSuperseded=i.onSuperseded}get isSolving(){return this.inFlight.size>0}get hasPending(){return this.pendingForLatestWins!==null||this.fifo.size>0}get inFlightCount(){return this.inFlight.size}get queueDepth(){return this.fifo.size+ +!!this.pendingForLatestWins}get lastResult(){return this._lastResult}get lastError(){return this._lastError}get lastDurationMs(){return this._lastDurationMs}setMaxConcurrent(e){let t=Math.max(1,Math.floor(e));if(t===this.maxConcurrent)return;let n=t>this.maxConcurrent;this.maxConcurrent=t,n&&this.drainNext()}getMaxConcurrent(){return this.maxConcurrent}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(){for(let e of this.subscribers)try{e()}catch(e){i().error(`[SolveScheduler] subscriber threw:`,e)}}solve(e,t,n){return this.solveDetailed(e,t,n).then(e=>e.response)}solveDetailed(e,r,i){if(this.disposed)return Promise.reject(new t(`SolveScheduler has been disposed and cannot be used`,n.INVALID_STATE));let a=new ze(()=>this.hashDefinitionMemo(e),r),o=``;if(this.cacheEnabled||this.onStart||this.onSettle||this.onSuperseded)try{o=a.key}catch(e){return Promise.reject(new t(`Solve input could not be hashed`,n.INVALID_INPUT,{originalError:e}))}let s=++this.solveSeq,c={key:o,enqueuedAt:Date.now(),startedAt:null};if(i?.signal?.aborted)return Promise.reject(this.makeAbortError(c));let l=this.readCache(o);if(l){this.mode===`latest-wins`&&this.supersedeCurrent();let e={status:`success`,response:l,durationMs:0,fromCache:!0};return this.writeLastState(s,{result:l,durationMs:0}),this.runHook(this.onStart,c),this.runHook(this.onSettle,c,e),this.notify(),Promise.resolve({response:l,fromCache:!0,durationMs:0})}return new Promise((t,n)=>{let o={definition:e,dataTree:r,hashes:a,ctx:c,seq:s,resolve:t,reject:n,externalSignal:i?.signal,selvaevents:i?.selvaevents,onServerTiming:i?.onServerTiming};o.externalSignal&&(o.queuedAbortHandler=()=>this.abortQueuedItem(o),o.externalSignal.addEventListener(`abort`,o.queuedAbortHandler,{once:!0})),this.enqueue(o)})}writeLastState(e,t){e<this.lastStateSeq||(this.lastStateSeq=e,`result`in t?(this._lastResult=t.result,this._lastError=null):this._lastError=t.error,this._lastDurationMs=t.durationMs)}supersedeCurrent(){this.pendingForLatestWins&&=(this.supersede(this.pendingForLatestWins),null);for(let e of this.inFlight)this.supersede(e),e.controller.abort()}dequeue(e){this.pendingForLatestWins===e&&(this.pendingForLatestWins=null),this.fifo.remove(e)}abortQueuedItem(e){this.dequeue(e),this.fail(e,this.makeAbortError(e.ctx)),this.notify()}shedAsQueueFull(e){let r=new t(`Solve queue is full; request rejected (backpressure)`,n.QUEUE_FULL,{statusCode:503,context:{queueDepth:this.fifo.size,maxQueueDepth:this.maxQueueDepth}});this.fail(e,r),this.notify()}armQueueWaitTimer(e){if(this.queueWaitMs===void 0)return;let r=this.queueWaitMs;e.queueWaitTimer=setTimeout(()=>{if(e.settled)return;this.dequeue(e);let i=new t(`Solve waited longer than ${r}ms in queue; rejected (backpressure)`,n.QUEUE_TIMEOUT,{statusCode:503,context:{waitedMs:Date.now()-e.ctx.enqueuedAt,queueWaitMs:r}});this.fail(e,i),this.notify()},r)}clearQueueWaitTimer(e){e.queueWaitTimer!==void 0&&(clearTimeout(e.queueWaitTimer),e.queueWaitTimer=void 0)}enqueue(e){switch(this.mode){case`latest-wins`:this.supersedeCurrent(),this.inFlight.size===0?this.execute(e):this.pendingForLatestWins=e;break;case`queue`:this.inFlight.size<this.maxConcurrent?this.execute(e):this.maxQueueDepth!==void 0&&this.fifo.size>=this.maxQueueDepth?this.shedAsQueueFull(e):(this.armQueueWaitTimer(e),this.fifo.push(e))}this.notify()}async execute(e){let t=new AbortController,n=e;n.controller=t,this.inFlight.add(n),e.ctx.startedAt=Date.now(),this.clearQueueWaitTimer(e),this.removeQueuedAbortHandler(e);let r=()=>t.abort();e.externalSignal?.addEventListener(`abort`,r,{once:!0}),this.runHook(this.onStart,e.ctx),this.notify();let i=performance.now();try{let n={...this.baseConfig,signal:t.signal,...this.timeoutMs!==void 0&&{timeoutMs:this.timeoutMs},...this.retry!==void 0&&{retry:this.retry},...e.selvaevents!==void 0&&{selvaevents:e.selvaevents}},r=e.onServerTiming;if(r){let e=this.baseConfig.onServerTiming;n.onServerTiming=(t,n)=>{try{e?.(t,n)}finally{r(t,n)}}}let{response:a,definitionReuploaded:o}=await this.runExecutor(e,n),s=performance.now()-i;if(this.writeCache(e.ctx.key,a),!this.settleSuccess(e,{response:a,fromCache:!1,definitionReuploaded:o,durationMs:s}))return;this.writeLastState(e.seq,{result:a,durationMs:s}),this.runHook(this.onSettle,e.ctx,{status:`success`,response:a,durationMs:s,fromCache:!1,definitionReuploaded:o})}catch(t){let r=performance.now()-i,a=this.normalizeExecutionError(t,n);this.writeLastState(e.seq,{error:a,durationMs:r}),this.fail(e,a,r)}finally{e.externalSignal?.removeEventListener(`abort`,r),this.inFlight.delete(n),this.drainNext(),this.notify()}}async runExecutor(e,t){let{definition:n,dataTree:r}=e;if(!this.reuseServerDefinitionCache||o(n)){let{response:e}=await this.executor(n,r,null,t);return{response:e}}let i=e.hashes.definitionHash,a=this.serverCacheKeys.get(i)??null,s=await this.executor(n,r,a,t);return this.serverCacheKeys.delete(i),s.cacheKey&&(this.serverCacheKeys.set(i,s.cacheKey),this.serverCacheKeys.size>100&&this.serverCacheKeys.delete(this.serverCacheKeys.keys().next().value)),{response:s.response,definitionReuploaded:s.missed}}hashDefinitionMemo(e){return typeof e==`string`?(this.lastStringDefinition?.value!==e&&(this.lastStringDefinition={value:e,hash:G(e)}),this.lastStringDefinition.hash):Re(e)}drainNext(){if(!this.disposed){if(this.mode===`latest-wins`){if(this.pendingForLatestWins&&this.inFlight.size===0){let e=this.pendingForLatestWins;this.pendingForLatestWins=null,this.execute(e)}}else for(;this.fifo.size>0&&this.inFlight.size<this.maxConcurrent;)this.execute(this.fifo.shift())}}supersede(e){let r=new t(`Superseded by newer solve`,n.SUPERSEDED,{context:{enqueuedAt:e.ctx.enqueuedAt}});this.settleError(e,r)&&this.runHook(this.onSuperseded,e.ctx)}makeAbortError(e){return new t(`Request aborted by caller`,n.ABORTED,{context:{enqueuedAt:e.enqueuedAt}})}settleError(e,t){return!e.settled&&(e.settled={error:t},this.clearQueueWaitTimer(e),this.removeQueuedAbortHandler(e),e.reject(t),!0)}fail(e,t,n=0){this.settleError(e,t)&&this.runHook(this.onSettle,e.ctx,{status:`error`,error:t,durationMs:n})}settleSuccess(e,t){return!e.settled&&(e.settled={ok:!0},this.clearQueueWaitTimer(e),this.removeQueuedAbortHandler(e),e.resolve(t),!0)}removeQueuedAbortHandler(e){e.queuedAbortHandler&&e.externalSignal&&e.externalSignal.removeEventListener(`abort`,e.queuedAbortHandler),e.queuedAbortHandler=void 0}normalizeExecutionError(e,r){return r.settled&&`error`in r.settled?r.settled.error:e instanceof t?e:e instanceof Error&&e.name===`AbortError`?this.makeAbortError(r.ctx):new t(e instanceof Error?e.message:String(e),n.UNKNOWN_ERROR,{originalError:e})}cancelAll(){this.pendingForLatestWins&&=(this.fail(this.pendingForLatestWins,this.makeAbortError(this.pendingForLatestWins.ctx)),null);for(let e=this.fifo.shift();e;e=this.fifo.shift())this.fail(e,this.makeAbortError(e.ctx));for(let e of this.inFlight){let t=e.ctx.startedAt?Date.now()-e.ctx.startedAt:0;this.fail(e,this.makeAbortError(e.ctx),t),e.controller.abort()}this.notify()}readCache(e){if(!this.cacheEnabled)return null;let t=this.cache.get(e);return t?this.cacheTtl>0&&Date.now()-t.insertedAt>this.cacheTtl?(this.dropCacheEntry(e),this.cacheMisses+=1,null):(this.cache.delete(e),this.cache.set(e,t),this.cacheHits+=1,t.response):(this.cacheMisses+=1,null)}writeCache(t,n){if(!this.cacheEnabled)return;let r=e(n,`errors`);if(!this.cacheErroredSolves&&Array.isArray(r)&&r.length>0)return;let i=e(e(n,`selva`),`outcome`);if(e(i,`aborted`)===!0)return;let a=c(n)??Ve(n);if(!(a>this.cacheMaxBytes))for(this.dropCacheEntry(t),this.cache.set(t,{response:n,insertedAt:Date.now(),sizeBytes:a}),this.cacheBytes+=a;this.cacheBytes>this.cacheMaxBytes&&this.cache.size>0;){let e=this.cache.keys().next().value;if(e===void 0)break;this.dropCacheEntry(e),this.cacheEvictions+=1}}dropCacheEntry(e){let t=this.cache.get(e);t&&(this.cache.delete(e),this.cacheBytes-=t.sizeBytes)}clearCache(){this.cache.clear(),this.cacheBytes=0}cacheStats(){return{entries:this.cache.size,bytes:this.cacheBytes,hits:this.cacheHits,misses:this.cacheMisses,evictions:this.cacheEvictions}}dispose(){this.disposed||(this.disposed=!0,this.cancelAll(),this.subscribers.clear(),this.clearCache())}runHook(e,...t){if(e)try{e(...t)}catch(e){i().error(`[SolveScheduler] hook threw:`,e)}}};function Ue(e){return m(e)?`ref:${e.key}`:typeof e==`string`&&e.length<200?e:`...content...`}function We(t){return Array.isArray(t)?t.map(t=>{let n=0,r=0,i=e(t,`innerTree`);if(i&&typeof i==`object`){for(let e of Object.values(i))if(Array.isArray(e)){n+=e.length;for(let t of e){let e=t?.data;typeof e==`string`&&(r+=e.length)}}}return{param:e(t,`paramName`)??`<unnamed>`,items:n,bytes:r}}):[]}var Ge=class r{config;serverStats;disposed=!1;static CREATE_PROBE_TIMEOUT_MS=2e3;constructor(e){this.config={...e,serverUrl:h(e.serverUrl),debug:e.debug??!1},this.serverStats=new g(this.config.serverUrl,this.config)}static async create(e,i={}){let a=new r(e),o=Math.max(1,(i.probe?.attempts??2)+1),s=i.probe?.baseDelayMs??250,c=i.probe?.maxDelayMs??1e3,l,u=null,d=0;for(let e=0;e<o;e++){if(d=e+1,l=await a.serverStats.probeServer(r.CREATE_PROBE_TIMEOUT_MS),l.online)return a;if(u=te(l),u&&!u.retryable)break;if(e<o-1){let t=Math.min(s*2**e,c);await new Promise(e=>setTimeout(e,t))}}await a.dispose();let f=l?.status,p=u?`Rhino Compute server is not available. ${u.summary}`:`Rhino Compute server is not online`;throw new t(p,n.NETWORK_ERROR,{...f!==void 0&&{statusCode:f},context:{serverUrl:a.config.serverUrl,attempts:d,maxAttempts:o,...u&&{probeVerdict:u.verdict,probeRetryable:u.retryable},...f!==void 0&&{lastProbeStatus:f},...l?.error!==void 0&&{lastProbeError:l.error}}})}getConfig(){return this.ensureNotDisposed(),{...this.config}}async getIO(e){return this.ensureNotDisposed(),B(e,this.config)}async getRawIO(e){return this.ensureNotDisposed(),R(e,this.config)}async solve(r,a,o){this.ensureNotDisposed();try{if(typeof r==`string`&&!r.trim())throw new t(`Definition URL/content is required`,n.INVALID_INPUT,{context:{receivedUrl:r}});if(r instanceof Uint8Array&&r.length===0)throw new t(`Definition content is empty`,n.INVALID_INPUT);if(m(r)&&!r.key.trim())throw new t(`DefinitionRef key is empty`,n.INVALID_INPUT);let i=await le(a,r,{...this.config,...o?.signal!==void 0&&{signal:o.signal},...o?.timeoutMs!==void 0&&{timeoutMs:o.timeoutMs},...o?.retry!==void 0&&{retry:o.retry},...o?.selvaevents!==void 0&&{selvaevents:o.selvaevents}}),s=e(i,`errors`);if(Array.isArray(s)&&s.length>0)throw new t(s.map(String).join(`; `)||`Computation failed`,n.COMPUTATION_ERROR,{context:{definition:Ue(r),inputSummary:We(a),errors:s,warnings:e(i,`warnings`),values:i.values}});return i}catch(e){throw this.config.debug&&i().error(`Compute failed:`,e),e instanceof t?e:new t(e instanceof Error?e.message:String(e),n.COMPUTATION_ERROR,{context:{definition:Ue(r),inputSummary:We(a)},originalError:e})}}createScheduler(e){return this.ensureNotDisposed(),new He((e,t,n,r)=>ue(t,n,e,r),this.config,e)}async dispose(){this.disposed||(this.disposed=!0,await this.serverStats.dispose())}ensureNotDisposed(){if(this.disposed)throw new t(`GrasshopperClient has been disposed and cannot be used`,n.INVALID_STATE)}};function Ke(e){return typeof e==`object`&&!!e&&e.__decodeError===!0}function qe(e,t){return{__decodeError:!0,type:e,raw:t}}function Je(e){let t=/* @__PURE__ */ new WeakSet,n=e=>{if(!e||typeof e!=`object`||t.has(e))return;t.add(e);let r=e.delete;if(typeof r==`function`){let t=e.isDeleted;typeof t==`function`&&t.call(e)||r.call(e);return}if(Array.isArray(e)){for(let t of e)n(t);return}let i=Object.getPrototypeOf(e);if(i===Object.prototype||i===null)for(let t of Object.values(e))n(t)};n(e)}const q=/* @__PURE__ */ new Map([[`Rhino.Geometry.Point3d`,(e,t)=>{let n=t;return!n||typeof n.X!=`number`?null:new e.Point([n.X,n.Y,n.Z])}],[`Rhino.Geometry.Line`,(e,t)=>{let n=t;return!n||!n.From||!n.To?null:new e.Line([n.From.X,n.From.Y,n.From.Z],[n.To.X,n.To.Y,n.To.Z])}]]);function Ye(e){if(q.has(e))return q.get(e);for(let[t,n]of q)if(e.startsWith(t))return n}function Xe(e){return!!(e&&typeof e==`object`&&typeof e.data==`string`)}function Ze(e,t,n){let r=Ye(t),a=!1;if(r)try{let t=r(n,e);if(t!=null)return t}catch(e){i().warn(`Failed to decode Rhino type ${t}:`,e),a=!0}try{if(Xe(e))return n.CommonObject.decode(e)}catch(n){return i().warn(`Failed to decode ${t} with CommonObject:`,n),qe(t,e)}return a?qe(t,e):e}const J={STRING:`System.String`,INT:`System.Int32`,DOUBLE:`System.Double`,BOOL:`System.Boolean`},Qe=`FileData`;function $e(e,t){return e.includes(`WebDisplay`)||t&&e!==J.STRING}function et(e){if(typeof e!=`string`)return e;let t=e.trim();if(!(t.startsWith(`{`)||t.startsWith(`[`)||t.startsWith(`"`)))return e;try{let e=JSON.parse(t);if(typeof e==`string`)try{return JSON.parse(e)}catch{return e}return e}catch{return e}}function tt(e,t,n){switch(t){case J.STRING:return typeof e==`string`?e.replace(/^"(.*)"$/,`$1`):e;case J.INT:return Number.parseInt(String(e),10);case J.DOUBLE:return Number.parseFloat(String(e));case J.BOOL:return String(e).toLowerCase()===`true`;default:return n&&t.startsWith(`Rhino.Geometry.`)?Ze(e,t,n):e}}function Y(e,t,n,r){if(typeof e.data!=`string`)return e.data;let i=n?et(e.data):e.data;return n&&t.includes(Qe)?nt(i)??i:tt(i,t,r)}function nt(t){if(!t||typeof t!=`object`)return null;let n=e(t,`fileName`),i=e(t,`fileType`),a=e(t,`subFolder`),o=e(t,`isBase64Encoded`);if(typeof n!=`string`||typeof i!=`string`||typeof a!=`string`||!r(t,`data`)||typeof o!=`boolean`&&typeof o!=`string`)return null;let s=e(t,`metadata`);return{fileName:n,fileType:i,data:e(t,`data`),isBase64Encoded:u(o),subFolder:a,...s?{metadata:s}:{}}}function X(t){let n=e(t,`values`);return Array.isArray(n)?n:[]}function Z(e){return typeof e.type==`string`?e.type:``}function Q(e,t){if(e&&typeof e==`object`){for(let n of Object.values(e))if(Array.isArray(n))for(let e of n)e&&typeof e==`object`&&t(e)}}function rt(t,n=!1,r={}){let{parseValues:i=!0,rhino:a,stringOnly:o=!1}=r,s={},c=/* @__PURE__ */ new Set;for(let r of X(t)){let t=e(r,`paramName`);Q(e(r,`innerTree`),e=>{let r=Z(e);if($e(r,o))return;let l=n?e.id:t;if(!l)return;let u=Y(e,r,i,a);Object.prototype.hasOwnProperty.call(s,l)?c.has(l)?s[l].push(u):(s[l]=[s[l],u],c.add(l)):x(s,l,u)})}return{values:s,dispose:()=>Je(s)}}function it(t){let n=[];for(let r of X(t))Q(e(r,`innerTree`),e=>{if(!Z(e).includes(Qe))return;let t=nt(et(e.data));t&&n.push(t)});return n}function at(t,n,r={}){let{parseValues:i=!0,rhino:a,stringOnly:o=!1}=r;for(let r of X(t)){let t=e(r,`innerTree`),s=!1;if(`byName`in n){if(e(r,`paramName`)!==n.byName)continue;s=!0}let c=[];if(Q(t,e=>{if(`byId`in n){if(e.id!==n.byId)return;s=!0}let t=Z(e);$e(t,o)||c.push(Y(e,t,i,a))}),s)return c.length===0?void 0:c.length===1?c[0]:c}}var ot=class{response;constructor(e){this.response=e}getValues(e=!1,t={}){return rt(this.response,e,t)}getValue(e,t){return at(this.response,e,t)}async getAndDownloadFiles(e,t){await ee(it(this.response),e,t)}};function st(t){return(Array.isArray(t)?t:[t]).map(t=>({schemas:e(t,`schemas`),error:e(t,`error`)}))}const ct=/* @__PURE__ */ new Set([`options`,`defaultOptions`,`values`]),lt=/* @__PURE__ */ new Set([`optionMedia`,`assets`]),ut=e=>e>=`A`&&e<=`Z`;function dt(e){if(e.length===0||!ut(e[0]))return e;let t=0;for(;t<e.length&&ut(e[t]);)t++;return t>1&&t<e.length&&t--,e.slice(0,t).toLowerCase()+e.slice(t)}function ft(e){return $(e)}function $(e){if(Array.isArray(e))return e.map($);if(typeof e!=`object`||!e)return e;let t={};for(let[n,r]of Object.entries(e)){let e=dt(n);x(t,e,ct.has(e)?r:lt.has(e)?pt(r):$(r))}return t}function pt(e){if(typeof e!=`object`||!e||Array.isArray(e))return e;let t={};for(let[n,r]of Object.entries(e))x(t,n,$(r));return t}var mt=class r{innerTree;paramName;constructor(e){this.paramName=e,this.innerTree={}}append(e,t){let n=this.innerTree[r.formatPathString(e)]??=[];for(let e of t)n.push({data:r.serializeValue(e)});return this}fromDataTreeDefault(e){this.innerTree={};for(let[t,n]of Object.entries(e)){if(!Array.isArray(n))continue;let e=r.parsePathString(t);this.append(e,n)}return this}appendFlat(e){let t=Array.isArray(e)?e:[e];return this.append([0],t)}flatten(){let e=[];for(let t of Object.values(this.innerTree))if(Array.isArray(t))for(let n of t)e.push(r.deserializeValue(n.data));return e}toComputeFormat(){return{ParamName:this.paramName,InnerTree:this.innerTree}}getParamName(){return this.paramName}static fromInputParams(e){return r.fromValues(e.map(e=>({paramName:e.nickname||`unnamed`,paramType:e.paramType,value:e.default,...`minimum`in e&&{minimum:e.minimum,maximum:e.maximum}})))}static fromValues(e){let t=[];for(let n of e){let e=new r(n.paramName),a=r.itemCoercer(n);for(let[t,o]of r.branchesOf(n.value)){let r=[];for(let e of o){if(e==null)continue;let t=a(e);t===null?i().warn(`${n.paramName}: dropped an item that is not a valid ${n.paramType}`):r.push(t)}r.length>0&&e.append(t,r)}Object.keys(e.innerTree).length>0&&t.push(e.toComputeFormat())}return t}static branchesOf(e){return r.hasValidValue(e)?C(e)?Object.entries(e).map(([e,t])=>[r.parsePathString(e),t]):[[[0],Array.isArray(e)?e:[e]]]:[]}static itemCoercer(e){let{paramName:t,minimum:n,maximum:i}=e,a=e.paramType.toLowerCase();return a===`number`||a===`integer`?e=>{let o=T(e);if(o===null)return null;let s=a===`integer`?Math.round(o):o;return r.clampValue(s,n,i,t)}:a===`boolean`?E:e=>e}static replaceTreeValue(e,t,n){let i=r.buildFromValue(t,n).toComputeFormat(),a=e.slice(),o=a.findIndex(e=>r.paramNameOf(e)===t);return o===-1?a.push(i):a[o]=i,a}static buildFromValue(e,t){let n=new r(e);return C(t)?n.fromDataTreeDefault(t):n.appendFlat(t),n}static getTree(t,n){let i=t.find(e=>r.paramNameOf(e)===n);if(!i)return null;let a=/* @__PURE__ */ new Map,o=e(i,`InnerTree`);if(!o||typeof o!=`object`)return a;for(let[e,t]of Object.entries(o))Array.isArray(t)&&a.set(e,t.map(e=>e?.data===void 0?null:r.deserializeValue(e.data)));return a}static getTreeValue(e,i){let a=r.getTree(e,i);if(!a||a.size===0)return null;if(a.size>1)throw new t(`"${i}" has ${a.size} branches; read it with TreeBuilder.getTree`,n.INVALID_INPUT,{context:{paramName:i,branchCount:a.size}});let[o]=a.values();return o.length===0?null:o.length===1?o[0]:o}static paramNameOf(t){return e(t,`ParamName`)}static parsePathString(e){let r=e.match(S);if(!r)throw new t(`Invalid Grasshopper tree path: "${e}". Expected "{}", "{0}", or "{0;1;2}" (negative indices allowed, no empty segments).`,n.INVALID_INPUT,{context:{pathStr:e}});return r[1]?r[1].split(`;`).map(Number):[]}static formatPathString(e){return`{${e.join(`;`)}}`}static serializeValue(e){return typeof e==`boolean`||typeof e==`number`||typeof e==`string`?e:typeof e==`object`&&e?JSON.stringify(e):String(e)}static deserializeValue(e){if(typeof e!=`string`)return e;if(e.startsWith(`{`)||e.startsWith(`[`))try{return JSON.parse(e)}catch{return e}let t=Number(e);return Number.isFinite(t)&&String(t)===e?t:e===`true`||e!==`false`&&e}static hasValidValue(e){return e==null?!1:typeof e==`string`||!(Array.isArray(e)&&e.length===0||typeof e==`object`&&!Array.isArray(e)&&Object.keys(e).length===0)}static clampValue(e,t,n,r){let a=e;return t!=null&&a<t&&(i().warn(`${r}: ${e} below min ${t}, clamping`),a=t),n!=null&&a>n&&(i().warn(`${r}: ${e} above max ${n}, clamping`),a=n),a}};export{g as ComputeServerStats,Ge as GrasshopperClient,ot as GrasshopperResponseProcessor,He as SolveScheduler,mt as TreeBuilder,Je as disposeRhinoObjects,R as fetchDefinitionIO,B as fetchParsedDefinitionIO,G as hashDefinition,Le as hashSolveInput,Ke as isRhinoDecodeError,ft as normalizeUISchemaCasing,ke as processInput,st as readSchemaResults,le as solveGrasshopperDefinition,U as stableStringify};
1
+ import { A as readField, C as withJsonContentType, D as tryParseJson, E as validateServerUrl, F as ErrorCodes, I as abortedByCaller, L as sleep, M as getLogger, O as redactUrl, P as ComputeError, S as buildAuthHeaders, _ as readBodyText, b as detectBase64Payload, c as isDefinitionUrl, d as classifyProbeFailure, f as isRedirect, g as raceSignal, h as composeSignal, i as isBase64Flag, k as hasField, l as fetchCompute, m as setResponseWireSize, p as getResponseWireSize, s as isDefinitionRef, t as downloadFileData, u as validateNumbers, v as base64ByteArray, w as warnIfClientSide, x as encodeStringToBase64 } from "./handle-files-fKtSlWT2.js";
2
+ // src/grasshopper/server/compute-server-stats.ts
3
+ /**
4
+ * Server health and control: liveness, version, child processes, cache purges.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const stats = new ComputeServerStats('http://localhost:6500', 'your-api-key');
9
+ *
10
+ * try {
11
+ * const isOnline = await stats.isServerOnline();
12
+ * const children = await stats.getActiveChildren();
13
+ * const version = await stats.getVersion();
14
+ *
15
+ * // Or get everything at once
16
+ * const allStats = await stats.getServerStats();
17
+ * } finally {
18
+ * await stats.dispose(); // Clean up resources
19
+ * }
20
+ * ```
21
+ */
22
+ var ComputeServerStats = class ComputeServerStats {
23
+ serverUrl;
24
+ headers;
25
+ disposed = false;
26
+ activeMonitors = /* @__PURE__ */ new Set();
27
+ /** Timeout (ms) for the fast read/monitoring endpoints. */
28
+ static DEFAULT_TIMEOUT_MS = 5e3;
29
+ /** Timeout (ms) for child-lifecycle POSTs — a cold Windows child can take ~30s to spawn. */
30
+ static LIFECYCLE_TIMEOUT_MS = 6e4;
31
+ /** Floor for `monitor()`'s `intervalMs` — anything lower hot-loops the server. */
32
+ static MIN_MONITOR_INTERVAL_MS = 100;
33
+ /** Every control-plane answer is a count, a version or a short map. */
34
+ static MAX_BODY_BYTES = 1 << 20;
35
+ /**
36
+ * @param serverUrl - Base URL of the Rhino Compute server with http:// or https:// scheme (e.g., 'http://localhost:6500')
37
+ * @param auth - An API key, or the same auth fields a solve's `ComputeConfig` carries
38
+ * (`apiKey`, `apiKeyHeader`, `authToken`, `headers`).
39
+ */
40
+ constructor(serverUrl, auth) {
41
+ this.serverUrl = validateServerUrl(serverUrl);
42
+ this.headers = buildAuthHeaders(typeof auth === "string" ? { apiKey: auth } : auth ?? {});
43
+ }
44
+ /**
45
+ * Aborts after `timeoutMs` so a hung connection can't stall a probe or `monitor()`; `0` disables
46
+ * it. Redirects are not followed: fetch would carry the API key header to the target.
47
+ */
48
+ async fetchWithTimeout(url, init = {}, timeoutMs = ComputeServerStats.DEFAULT_TIMEOUT_MS) {
49
+ const { signal, cleanup } = composeSignal(init.signal ?? void 0, timeoutMs);
50
+ try {
51
+ return await fetch(url, {
52
+ ...init,
53
+ headers: init.method === "POST" ? withJsonContentType(this.headers) : this.headers,
54
+ redirect: "manual",
55
+ signal
56
+ });
57
+ } finally {
58
+ cleanup();
59
+ }
60
+ }
61
+ /**
62
+ * The response body of `path`, or `null` (logged) on a non-2xx or a network
63
+ * failure. Throws only when disposed.
64
+ */
65
+ async getText(path, init = {}, timeoutMs = ComputeServerStats.DEFAULT_TIMEOUT_MS) {
66
+ this.ensureNotDisposed();
67
+ const label = `${init.method ?? "GET"} ${path}`;
68
+ try {
69
+ const response = await this.fetchWithTimeout(`${this.serverUrl}${path}`, init, timeoutMs);
70
+ if (!response.ok) {
71
+ getLogger().warn(`[ComputeServerStats] ${label} failed:`, response.status);
72
+ await response.body?.cancel().catch(() => {});
73
+ return null;
74
+ }
75
+ const { text, truncated } = await readBodyText(response, ComputeServerStats.MAX_BODY_BYTES);
76
+ if (truncated) {
77
+ getLogger().warn(`[ComputeServerStats] ${label} answered with an oversized body`);
78
+ return null;
79
+ }
80
+ return text;
81
+ } catch (err) {
82
+ getLogger().warn(`[ComputeServerStats] ${label} errored:`, err);
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Check if the server is online.
88
+ *
89
+ * This is a single-sample probe: it returns `true` only on a 2xx or a redirect
90
+ * from the proxy liveness root `/`, and `false` for every other outcome (4xx/5xx,
91
+ * network error, or timeout). A cold or briefly-busy-but-up server can therefore
92
+ * read as offline — callers that gate on this (e.g. client construction)
93
+ * should retry rather than treat a single `false` as authoritative.
94
+ *
95
+ * @param timeoutMs - Abort the probe after this many ms (default: 5000).
96
+ * Pass `0` to disable the timeout. Prevents a hung connection from
97
+ * stalling the caller indefinitely.
98
+ */
99
+ async isServerOnline(timeoutMs = ComputeServerStats.DEFAULT_TIMEOUT_MS) {
100
+ return (await this.probeServer(timeoutMs)).online;
101
+ }
102
+ /**
103
+ * Detailed liveness probe backing {@link isServerOnline}.
104
+ *
105
+ * Reports what the probe actually saw so callers can distinguish "connection
106
+ * failed" (`error` set) from "server answered non-2xx" (`status` set) — e.g.
107
+ * a 401 from a proxy that requires an API key is a misconfiguration, not an
108
+ * offline server. Same single-sample caveat as {@link isServerOnline}.
109
+ *
110
+ * @param timeoutMs - Abort the probe after this many ms (default: 5000).
111
+ * Pass `0` to disable the timeout.
112
+ * @param signal - Cancels the probe, which then reports the abort as its `error`.
113
+ * @returns `online` plus either the HTTP `status` the server answered with,
114
+ * or the `error` the connection attempt failed with.
115
+ */
116
+ async probeServer(timeoutMs = ComputeServerStats.DEFAULT_TIMEOUT_MS, signal) {
117
+ this.ensureNotDisposed();
118
+ const url = `${this.serverUrl}/`;
119
+ try {
120
+ const response = await this.fetchWithTimeout(url, {
121
+ method: "GET",
122
+ signal
123
+ }, timeoutMs);
124
+ await response.body?.cancel().catch(() => {});
125
+ return {
126
+ online: response.ok || isRedirect(response),
127
+ status: response.status
128
+ };
129
+ } catch (err) {
130
+ const error = describeProbeError(err);
131
+ getLogger().debug(`[ComputeServerStats] Fetch error: ${error}`);
132
+ return {
133
+ online: false,
134
+ error
135
+ };
136
+ }
137
+ }
138
+ /**
139
+ * Get the number of active child processes on the server.
140
+ *
141
+ * By default the proxy's `/activechildren` endpoint will *spawn* children up
142
+ * to the configured count if none are running, then report the count — which
143
+ * wakes (and bills) an idle server. Pass `{ initialize: false }` for a passive
144
+ * read that reports the current count without spawning; use this for
145
+ * monitoring or before a purge/probe where you must not wake the server.
146
+ *
147
+ * @param options.initialize - When `false`, append `?initialize=false` so the
148
+ * server reports without spawning. Defaults to `true` (the server's default).
149
+ * @returns Number of active children, or null if unavailable
150
+ */
151
+ async getActiveChildren(options = {}) {
152
+ const { initialize = true } = options;
153
+ const body = initialize ? await this.getText("/activechildren", {}, ComputeServerStats.LIFECYCLE_TIMEOUT_MS) : await this.getText("/activechildren?initialize=false");
154
+ if (body === null) return null;
155
+ const text = body.trim();
156
+ if (!/^\d+$/.test(text)) {
157
+ getLogger().warn("[ComputeServerStats] /activechildren answered with a non-numeric body");
158
+ return null;
159
+ }
160
+ return parseInt(text, 10);
161
+ }
162
+ /**
163
+ * Get the server version information.
164
+ *
165
+ * @returns Version object with rhino, compute, and git_sha, or null if unavailable
166
+ */
167
+ async getVersion() {
168
+ const text = await this.getText("/version");
169
+ if (text === null) return null;
170
+ const json = tryParseJson(text);
171
+ if (json == null) return {
172
+ rhino: text,
173
+ compute: "",
174
+ git_sha: null
175
+ };
176
+ return {
177
+ rhino: json.rhino ?? "",
178
+ compute: json.compute ?? "",
179
+ git_sha: json.git_sha ?? null
180
+ };
181
+ }
182
+ /**
183
+ * Get the plugins installed on the server.
184
+ *
185
+ * Returns a `name → version` map of non-core plugins the server has loaded,
186
+ * or `null` if the request failed. Pass `kind` to choose which inventory:
187
+ * `'gh'` (default) lists Grasshopper add-on assemblies via
188
+ * `/plugins/gh/installed`; `'rhino'` lists Rhino plugins via
189
+ * `/plugins/rhino/installed`. Plugins that ship with Rhino / are core
190
+ * libraries are excluded by the server.
191
+ *
192
+ * @param kind - `'gh'` for Grasshopper add-ons (default) or `'rhino'` for Rhino plugins.
193
+ * @returns Map of plugin name to version, or `null` on failure.
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * const gh = await stats.getInstalledPlugins(); // Grasshopper add-ons
198
+ * const selvaVersion = gh?.['Selva'] ?? null;
199
+ * ```
200
+ */
201
+ async getInstalledPlugins(kind = "gh") {
202
+ const text = await this.getText(`/plugins/${kind}/installed`);
203
+ const json = text === null ? void 0 : tryParseJson(text);
204
+ return json && typeof json === "object" ? json : null;
205
+ }
206
+ /**
207
+ * Get comprehensive server statistics.
208
+ * Fetches all available server information in parallel.
209
+ *
210
+ * @returns Object containing server status and available stats
211
+ */
212
+ async getServerStats() {
213
+ this.ensureNotDisposed();
214
+ if (!await this.isServerOnline()) return { isOnline: false };
215
+ const [version, activeChildren] = await Promise.all([this.getVersion(), this.getActiveChildren({ initialize: false })]);
216
+ return {
217
+ isOnline: true,
218
+ ...version && { version },
219
+ ...activeChildren !== null && { activeChildren }
220
+ };
221
+ }
222
+ /**
223
+ * Purge the server's solve-results / URL-data cache.
224
+ *
225
+ * POSTs to `cache/purge` and returns the number of entries removed, or `null`
226
+ * if the request failed. This clears cached solve responses and fetched
227
+ * definition-URL data; it does NOT evict the definition cache (active
228
+ * `pointer` references stay valid).
229
+ *
230
+ * **Caveat:** `cache/purge` is forwarded by the rhino.compute proxy to a
231
+ * single round-robin-selected child, so in a multi-child deployment one call
232
+ * purges one child's cache. Call repeatedly (or size the pool to 1) if you
233
+ * need a fleet-wide purge.
234
+ *
235
+ * @returns Number of entries removed, or `null` on failure.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * const removed = await stats.purgeCache();
240
+ * if (removed !== null) console.log(`Purged ${removed} cached solves`);
241
+ * ```
242
+ */
243
+ async purgeCache() {
244
+ const text = await this.getText("/cache/purge", { method: "POST" });
245
+ const purged = (text === null ? void 0 : tryParseJson(text))?.purged;
246
+ return typeof purged === "number" ? purged : null;
247
+ }
248
+ /**
249
+ * Best-effort fleet-wide cache purge across a multi-child deployment.
250
+ *
251
+ * A single {@link purgeCache} POST is forwarded by the rhino.compute proxy to
252
+ * just ONE round-robin-selected child, so the other children keep serving
253
+ * stale cached solves. There is no proxy endpoint that addresses children
254
+ * individually, so this method reads the active child count (passively, never
255
+ * spawning) and fires `2 × count` sequential purges, relying on the proxy's
256
+ * round-robin to spread the hits across the pool.
257
+ *
258
+ * **This is best-effort, not a guarantee.** Round-robin can revisit one child
259
+ * and skip another; under concurrent traffic the rotation drifts. The result's
260
+ * `confident` flag is `true` only when the server reports a single child (where
261
+ * one purge is exact) — surface it so callers don't over-promise. For a hard
262
+ * fleet-wide guarantee, run the deployment at `--childcount 1` or add a
263
+ * server-side fan-out endpoint.
264
+ *
265
+ * @returns `{ totalPurged, calls, children, confident }`, or `null` if the
266
+ * child count couldn't be read (server unreachable). `totalPurged` sums the
267
+ * per-call counts; `calls` is how many purges were issued; `children` is the
268
+ * reported pool size; `confident` is `true` only at a single-child pool.
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * const r = await stats.purgeAllChildren();
273
+ * if (r && !r.confident) {
274
+ * console.warn(`Purged ~${r.totalPurged} across ${r.children} children (best-effort)`);
275
+ * }
276
+ * ```
277
+ */
278
+ async purgeAllChildren() {
279
+ this.ensureNotDisposed();
280
+ const children = await this.getActiveChildren({ initialize: false });
281
+ if (children === null) {
282
+ getLogger().warn("[ComputeServerStats] purgeAllChildren: could not read child count");
283
+ return null;
284
+ }
285
+ if (children === 0) return {
286
+ totalPurged: 0,
287
+ calls: 0,
288
+ children: 0,
289
+ confident: true
290
+ };
291
+ const calls = children * 2;
292
+ let totalPurged = 0;
293
+ for (let i = 0; i < calls; i++) {
294
+ const purged = await this.purgeCache();
295
+ if (purged !== null) totalPurged += purged;
296
+ }
297
+ return {
298
+ totalPurged,
299
+ calls,
300
+ children,
301
+ confident: children === 1
302
+ };
303
+ }
304
+ /**
305
+ * Get the server's current UTC clock.
306
+ *
307
+ * GETs `/servertime`, which the server emits as a JSON-encoded ISO-8601
308
+ * timestamp (e.g. `"2026-06-18T08:30:00Z"`). Useful for detecting clock skew
309
+ * between caller and server. Returns `null` if the request failed or the body
310
+ * isn't a parseable date.
311
+ *
312
+ * @returns A `Date` for the server's UTC time, or `null` on failure.
313
+ */
314
+ async getServerTime() {
315
+ const text = await this.getText("/servertime");
316
+ if (text === null) return null;
317
+ const date = new Date(text.trim().replace(/^"|"$/g, ""));
318
+ return isNaN(date.getTime()) ? null : date;
319
+ }
320
+ /**
321
+ * Get how long the rhino.compute proxy has been idle.
322
+ *
323
+ * GETs `/idlespan` on the proxy, which returns the seconds elapsed since the
324
+ * last request was forwarded to a compute child. This is a proxy-level metric
325
+ * (not proxied to a child) used by autoscalers to decide when a node can be
326
+ * drained. Returns `null` if unavailable.
327
+ *
328
+ * @returns Idle time in seconds, or `null` on failure.
329
+ */
330
+ async getIdleSpan() {
331
+ const text = await this.getText("/idlespan");
332
+ if (text === null) return null;
333
+ const seconds = parseFloat(text.trim());
334
+ return isNaN(seconds) ? null : seconds;
335
+ }
336
+ /**
337
+ * Fill the compute child pool up to the server's configured baseline.
338
+ *
339
+ * POSTs `/launch-children`. No-op when the pool is already at or above the
340
+ * configured `--childcount`. Returns `{ spawned, active }` — how many children
341
+ * were started and the resulting child count — or `null` on failure.
342
+ *
343
+ * To raise capacity above the baseline use {@link launchChild}; the baseline
344
+ * itself can only be changed by restarting rhino.compute.
345
+ *
346
+ * @returns `{ spawned, active }`, or `null` on failure.
347
+ */
348
+ async launchChildren() {
349
+ return this.postJson("/launch-children");
350
+ }
351
+ /**
352
+ * Add a single compute child to the pool, optionally on a specific port.
353
+ *
354
+ * POSTs `/launch-child` (with `?port=N` when `port` is given). Unlike
355
+ * {@link launchChildren}, this can push the pool above the baseline, up to the
356
+ * server's `MaxChildren` cap. Returns `{ spawned: [port] }` on success, or
357
+ * `null` on failure (server replies 400 for a bad port, 409 if the port is in
358
+ * use, 503 at the max-children cap).
359
+ *
360
+ * @param port - Optional specific port to launch on; otherwise the next free one.
361
+ * @returns `{ spawned }` listing the launched port, or `null` on failure.
362
+ */
363
+ async launchChild(port) {
364
+ const path = port === void 0 ? "/launch-child" : `/launch-child?port=${port}`;
365
+ return this.postJson(path);
366
+ }
367
+ /**
368
+ * Gracefully shut down compute children without respawning them.
369
+ *
370
+ * POSTs `/shutdown-children`. With no `port` it shuts down every child; with
371
+ * `port` it targets just that one. Children do not respawn, but the next
372
+ * `/grasshopper` request auto-spawns the pool back to the baseline. Returns
373
+ * `{ shutdown, active }` — how many were stopped and the remaining count — or
374
+ * `null` on failure.
375
+ *
376
+ * @param port - Optional port to target; omit to shut down all children.
377
+ * @returns `{ shutdown, active }`, or `null` on failure.
378
+ */
379
+ async shutdownChildren(port) {
380
+ const path = port === void 0 ? "/shutdown-children" : `/shutdown-children?port=${port}`;
381
+ return this.postJson(path);
382
+ }
383
+ /**
384
+ * Shut down compute children and respawn replacements (rolling restart).
385
+ *
386
+ * POSTs `/recycle-children`. With no `port` it recycles every child; with
387
+ * `port` it recycles just that one. The server recycles sequentially (each
388
+ * replacement is serving before the next child is stopped) so the pool never
389
+ * drops to zero mid-recycle. Returns `{ shutdown, spawned, active }`, or
390
+ * `null` on failure.
391
+ *
392
+ * @param port - Optional port to target; omit to recycle all children.
393
+ * @returns `{ shutdown, spawned, active }`, or `null` on failure.
394
+ */
395
+ async recycleChildren(port) {
396
+ const path = port === void 0 ? "/recycle-children" : `/recycle-children?port=${port}`;
397
+ return this.postJson(path);
398
+ }
399
+ /** A child-lifecycle POST: a spawn or recycle can take ~30 s. */
400
+ async postJson(path) {
401
+ const text = await this.getText(path, { method: "POST" }, ComputeServerStats.LIFECYCLE_TIMEOUT_MS);
402
+ return text === null ? null : tryParseJson(text) ?? null;
403
+ }
404
+ /**
405
+ * Continuously monitor server stats at specified interval.
406
+ *
407
+ * @param callback - Function called with stats on each interval
408
+ * @param intervalMs - Milliseconds between checks (default: 5000). Must be a
409
+ * finite number of at least 100 ms — lower values would hot-loop the server.
410
+ * @returns Function to stop monitoring
411
+ * @throws {ComputeError} `INVALID_CONFIG` if `intervalMs` is not a finite number >= 100.
412
+ *
413
+ * @example
414
+ * ```typescript
415
+ * const stopMonitoring = stats.monitor((data) => {
416
+ * console.log('Server stats:', data);
417
+ * }, 3000);
418
+ *
419
+ * // Later...
420
+ * stopMonitoring();
421
+ * ```
422
+ */
423
+ monitor(callback, intervalMs = 5e3) {
424
+ this.ensureNotDisposed();
425
+ if (!Number.isFinite(intervalMs) || intervalMs < ComputeServerStats.MIN_MONITOR_INTERVAL_MS) throw new ComputeError(`monitor() intervalMs must be a finite number >= ${ComputeServerStats.MIN_MONITOR_INTERVAL_MS}ms, got ${intervalMs}. Sub-${ComputeServerStats.MIN_MONITOR_INTERVAL_MS}ms polling would hot-loop the server.`, ErrorCodes.INVALID_CONFIG, { context: { intervalMs } });
426
+ let active = true;
427
+ let currentTimeoutId = null;
428
+ getLogger().info(`🔄 Starting server stats monitoring every ${intervalMs}ms`);
429
+ const check = async () => {
430
+ currentTimeoutId = null;
431
+ if (!active || this.disposed) return;
432
+ try {
433
+ const _stats = await this.getServerStats();
434
+ if (!active || this.disposed) return;
435
+ try {
436
+ callback(_stats);
437
+ } catch (err) {
438
+ getLogger().error("[ComputeServerStats] Monitor callback threw:", err);
439
+ }
440
+ } catch (err) {
441
+ if (!active || this.disposed) getLogger().debug("[ComputeServerStats] Monitor poll cancelled during shutdown:", err);
442
+ else getLogger().error("[ComputeServerStats] Failed to fetch stats during monitor:", err);
443
+ }
444
+ if (active && !this.disposed) currentTimeoutId = setTimeout(() => void check(), intervalMs);
445
+ };
446
+ const stopMonitoring = () => {
447
+ active = false;
448
+ if (currentTimeoutId !== null) {
449
+ clearTimeout(currentTimeoutId);
450
+ currentTimeoutId = null;
451
+ }
452
+ this.activeMonitors.delete(stopMonitoring);
453
+ };
454
+ this.activeMonitors.add(stopMonitoring);
455
+ check();
456
+ return stopMonitoring;
457
+ }
458
+ /**
459
+ * Disposes of all resources and stops all active monitors.
460
+ * Call this when you're done using the stats instance.
461
+ */
462
+ async dispose() {
463
+ if (this.disposed) return;
464
+ this.disposed = true;
465
+ for (const stopMonitor of this.activeMonitors) stopMonitor();
466
+ this.activeMonitors.clear();
467
+ }
468
+ ensureNotDisposed() {
469
+ if (this.disposed) throw new ComputeError("ComputeServerStats has been disposed and cannot be used", ErrorCodes.INVALID_STATE);
470
+ }
471
+ };
472
+ /**
473
+ * undici reports every connection failure as `TypeError: fetch failed` and keeps what happened on
474
+ * `cause`, whose message is empty when several addresses failed: its `code` is the one sure field.
475
+ */
476
+ function describeProbeError(err) {
477
+ if (!(err instanceof Error)) return String(err);
478
+ const summary = `${err.name}: ${err.message}`;
479
+ if (!(err.cause instanceof Error)) return summary;
480
+ const { code } = err.cause;
481
+ return `${summary}: ${typeof code === "string" ? `${code} ` : ""}${err.cause.message}`.trim();
482
+ }
483
+ // src/grasshopper/solve.ts
484
+ /**
485
+ * Server message for a `pointer` it can't resolve (evicted, or a different child in the pool).
486
+ * Production servers scrub it, so the `definition_not_cached` code is the reliable signal; this
487
+ * substring covers debug-mode servers and forks that emit no code.
488
+ */
489
+ const DEFINITION_LOAD_FAILED = "Unable to load grasshopper definition";
490
+ /** `core/` knows no backend's wire codes, so every Grasshopper request passes these in. */
491
+ const GRASSHOPPER_SERVER_ERROR_CODES = { definition_not_cached: ErrorCodes.DEFINITION_NOT_CACHED };
492
+ /** Attach the Grasshopper wire-code table without clobbering a caller's own entries. */
493
+ function withGrasshopperErrorCodes(config) {
494
+ return {
495
+ ...config,
496
+ serverErrorCodes: {
497
+ ...GRASSHOPPER_SERVER_ERROR_CODES,
498
+ ...config.serverErrorCodes
499
+ }
500
+ };
501
+ }
502
+ function isDefinitionLoadMiss(error) {
503
+ if (!(error instanceof ComputeError)) return false;
504
+ return error.code === ErrorCodes.DEFINITION_NOT_CACHED || error.message.includes(DEFINITION_LOAD_FAILED);
505
+ }
506
+ /**
507
+ * Debug-only: names the outputs that produced nothing, usually a definition that didn't compute
508
+ * (missing inputs, a guarded branch). An empty output can be legitimate, so this only warns.
509
+ */
510
+ function warnOnEmptyInnerTrees(response, debug) {
511
+ if (!debug) return;
512
+ const values = readField(response, "values");
513
+ if (!Array.isArray(values) || values.length === 0) return;
514
+ const empty = [];
515
+ for (const param of values) {
516
+ const innerTree = readField(param, "innerTree");
517
+ if (!innerTree || Object.keys(innerTree).length === 0) empty.push(readField(param, "paramName") ?? "<unnamed>");
518
+ }
519
+ if (empty.length === 0) return;
520
+ const scope = empty.length === values.length ? "all" : `${empty.length}/${values.length}`;
521
+ getLogger().warn(`Solve returned empty output(s) (${scope}): ${empty.join(", ")}. These parameters produced no data — check the definition's inputs and the branch feeding each.`);
522
+ }
523
+ /**
524
+ * Run one solve. `GrasshopperClient.solve()` adds input validation and throws on solver errors.
525
+ *
526
+ * @param definition - A URL (sent as a pointer), base64 `.gh` content, raw `.gh` bytes, or a
527
+ * `DefinitionRef` (loaded for the upload). Any other string is encoded as the definition text.
528
+ * @returns The compute response, without the server's `pointer` and echoed `algo`.
529
+ *
530
+ * @example
531
+ * await solveGrasshopperDefinition(trees, 'https://example.com/definition.gh', config);
532
+ * await solveGrasshopperDefinition(trees, new Uint8Array(await readFile('model.gh')), config);
533
+ */
534
+ async function solveGrasshopperDefinition(dataTree, definition, config) {
535
+ warnIfClientSide("solveGrasshopperDefinition", config.suppressBrowserWarning);
536
+ validateServerUrl(config.serverUrl);
537
+ return (await uploadAndSolve(dataTree, definition, config)).response;
538
+ }
539
+ /**
540
+ * Solve by `pointer: cacheKey` when a key is known, skipping the upload. With no key, or when the
541
+ * server has evicted it, upload the full definition and report `missed: true`. A `DefinitionRef`
542
+ * is loaded only on that upload path.
543
+ */
544
+ async function solveWithServerCacheKey(dataTree, cacheKey, definition, config) {
545
+ warnIfClientSide("solveWithServerCacheKey", config.suppressBrowserWarning);
546
+ if (cacheKey !== null) try {
547
+ return {
548
+ ...await runSolve({
549
+ algo: null,
550
+ pointer: cacheKey,
551
+ values: dataTree
552
+ }, config),
553
+ missed: false
554
+ };
555
+ } catch (error) {
556
+ if (!isDefinitionLoadMiss(error)) throw error;
557
+ }
558
+ return {
559
+ ...await uploadAndSolve(dataTree, definition, config),
560
+ missed: true
561
+ };
562
+ }
563
+ async function uploadAndSolve(dataTree, definition, config) {
564
+ return runSolve(prepareGrasshopperArgs(await materializeDefinition(definition, config), dataTree), config);
565
+ }
566
+ async function materializeDefinition(definition, config) {
567
+ if (!isDefinitionRef(definition)) return definition;
568
+ validateNumbers(config);
569
+ const { signal, cleanup } = composeSignal(config.signal, config.timeoutMs);
570
+ try {
571
+ return await raceSignal(definition.load(signal), signal);
572
+ } catch (error) {
573
+ if (config.signal?.aborted) throw abortedByCaller({ definitionKey: definition.key }, error);
574
+ if (signal?.aborted) throw new ComputeError(`Loading definition '${definition.key}' timed out after ${config.timeoutMs}ms`, ErrorCodes.TIMEOUT_ERROR, { context: {
575
+ definitionKey: definition.key,
576
+ timeoutMs: config.timeoutMs
577
+ } });
578
+ throw new ComputeError(`Failed to load definition bytes for ref '${definition.key}'`, ErrorCodes.INVALID_INPUT, {
579
+ context: { definitionKey: definition.key },
580
+ originalError: error
581
+ });
582
+ } finally {
583
+ cleanup();
584
+ }
585
+ }
586
+ /**
587
+ * POST a solve and split the server's `pointer` off the response. The server echoes the request's
588
+ * base64 definition back as `algo`; it is dropped so a cached response doesn't pin a multi-MB copy
589
+ * of the definition against the scheduler's byte budget.
590
+ */
591
+ async function runSolve(args, config) {
592
+ applyOptionalComputeSettings(args, config);
593
+ const { maxResponseBytes } = config;
594
+ const result = await fetchCompute("grasshopper", args, withGrasshopperErrorCodes({
595
+ ...config,
596
+ maxResponseBytes: maxResponseBytes !== void 0 && args.algo ? maxResponseBytes + args.algo.length : maxResponseBytes
597
+ }));
598
+ assertResponseObject(result);
599
+ const { pointer, algo: _algo, ...rest } = result;
600
+ const response = readField(rest, "values") ? rest : {
601
+ ...rest,
602
+ values: []
603
+ };
604
+ const wireSize = getResponseWireSize(result);
605
+ if (wireSize !== void 0) {
606
+ const algoLength = typeof _algo === "string" ? _algo.length : 0;
607
+ setResponseWireSize(response, Math.max(0, wireSize - algoLength));
608
+ }
609
+ warnOnEmptyInnerTrees(response, config.debug);
610
+ return {
611
+ response,
612
+ cacheKey: typeof pointer === "string" ? pointer : null
613
+ };
614
+ }
615
+ /** A 200 can carry valid JSON that isn't an object: `null` from a proxy, or a bare string. */
616
+ function assertResponseObject(response) {
617
+ if (!response || typeof response !== "object") throw new ComputeError("Invalid response structure", ErrorCodes.INVALID_RESPONSE, { context: { responseType: response === null ? "null" : typeof response } });
618
+ }
619
+ function prepareGrasshopperArgs(definition, dataTree) {
620
+ const args = {
621
+ algo: null,
622
+ pointer: null,
623
+ values: dataTree
624
+ };
625
+ if (definition instanceof Uint8Array) args.algo = base64ByteArray(definition);
626
+ else if (isDefinitionUrl(definition)) args.pointer = definition;
627
+ else args.algo = detectBase64Payload(definition) ?? encodeStringToBase64(definition);
628
+ return args;
629
+ }
630
+ function applyOptionalComputeSettings(arglist, options) {
631
+ if (options.cachesolve != null) arglist.cachesolve = options.cachesolve;
632
+ if (options.modelunits != null) arglist.modelunits = options.modelunits;
633
+ if (options.angletolerance != null) arglist.angletolerance = options.angletolerance;
634
+ if (options.absolutetolerance != null) arglist.absolutetolerance = options.absolutetolerance;
635
+ if (options.dataversion != null) arglist.dataversion = options.dataversion;
636
+ if (options.selvaevents != null) arglist.selvaevents = options.selvaevents;
637
+ }
638
+ // src/core/utils/set-own-field.ts
639
+ /**
640
+ * `obj[key] = value` for a key taken from a wire payload. Assigning `__proto__` would call the
641
+ * prototype setter and swap the object's prototype instead of storing the value.
642
+ */
643
+ function setOwnField(obj, key, value) {
644
+ if (key === "__proto__") Object.defineProperty(obj, key, {
645
+ value,
646
+ writable: true,
647
+ enumerable: true,
648
+ configurable: true
649
+ });
650
+ else obj[key] = value;
651
+ }
652
+ // src/grasshopper/data-tree/tree-path.ts
653
+ /**
654
+ * Canonical matcher for a Grasshopper branch path key like `{0}`, `{0;1}`, or
655
+ * the root path `{}`. This is the single source of truth for the branch-path
656
+ * shape (`DataTreePath`); anything testing "does this string name a branch?"
657
+ * should use this rather than re-inlining the regex.
658
+ *
659
+ * Matches `{}`, `{0}`, `{0;1;2}`, and negative indices like `{-1;2}` (GH_Path
660
+ * allows them) — but NOT empty segments like `{0;}` / `{;}` / `{0;;1}`, which
661
+ * would otherwise split to phantom 0s (`Number('') === 0`).
662
+ */
663
+ const TREE_PATH_RE = /^\{(-?\d+(?:;-?\d+)*)?\}$/;
664
+ /**
665
+ * Membership test for a {@link DataTreeDefault}: an object keyed entirely by
666
+ * branch paths, each mapping to an array of values. This is the one predicate
667
+ * both the input-type parsers (to pass a tree-access default through untouched)
668
+ * and `TreeBuilder` (to dispatch it to `fromDataTreeDefault`) ask — so the two
669
+ * agree by construction on exactly which values are trees.
670
+ */
671
+ function isDataTreeDefault(value) {
672
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
673
+ const entries = Object.entries(value);
674
+ return entries.length > 0 && entries.every(([key, val]) => TREE_PATH_RE.test(key) && Array.isArray(val));
675
+ }
676
+ /** Every numeric CLR type Grasshopper serializes, so no numeric item stays a string. */
677
+ const NUMERIC_ITEM_TYPES = /* @__PURE__ */ new Set([
678
+ "System.Double",
679
+ "System.Single",
680
+ "System.Decimal",
681
+ "System.Int32",
682
+ "System.Int64"
683
+ ]);
684
+ /** Integral wire item types: rounded like the scalar Integer path. */
685
+ const INTEGER_ITEM_TYPES = /* @__PURE__ */ new Set(["System.Int32", "System.Int64"]);
686
+ // src/grasshopper/io/input/transformers.ts
687
+ /**
688
+ * Coerce a schema's `default`: array items are transformed and the rejected ones
689
+ * filtered (none left → `undefined`); a rejected scalar becomes `undefined` when
690
+ * `dropInvalidScalar`, else is kept verbatim for the parser to report.
691
+ */
692
+ function coerceDefault(value, transform, dropInvalidScalar) {
693
+ if (value === void 0 || value === null) return value;
694
+ if (Array.isArray(value)) {
695
+ const processed = value.map(transform).filter((v) => v !== null);
696
+ return processed.length > 0 ? processed : void 0;
697
+ }
698
+ const transformed = transform(value);
699
+ if (transformed !== null) return transformed;
700
+ return dropInvalidScalar ? void 0 : value;
701
+ }
702
+ const numericTransformer = (value) => {
703
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
704
+ if (typeof value === "string") {
705
+ const trimmed = value.trim();
706
+ if (trimmed === "") return null;
707
+ if (/^[+-]?0[xbo]/i.test(trimmed)) return null;
708
+ const parsed = Number(trimmed);
709
+ return Number.isFinite(parsed) ? parsed : null;
710
+ }
711
+ return null;
712
+ };
713
+ const booleanTransformer = (value) => {
714
+ if (typeof value === "boolean") return value;
715
+ if (typeof value === "string") {
716
+ const lower = value.trim().toLowerCase();
717
+ if (lower === "true") return true;
718
+ if (lower === "false") return false;
719
+ }
720
+ return null;
721
+ };
722
+ const textTransformer = (value) => {
723
+ if (typeof value === "string") {
724
+ if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
725
+ if (value.startsWith("\"")) return value.slice(1);
726
+ return value;
727
+ }
728
+ return null;
729
+ };
730
+ const colorTransformer = (value) => {
731
+ if (typeof value === "string") {
732
+ let cleaned = value.trim();
733
+ if (cleaned.startsWith("\"") && cleaned.endsWith("\"")) cleaned = cleaned.slice(1, -1).trim();
734
+ return cleaned;
735
+ }
736
+ return null;
737
+ };
738
+ function objectTransformer(inputName) {
739
+ return (value) => {
740
+ if (typeof value === "object" && value !== null) return value;
741
+ if (typeof value === "string" && value.trim() !== "") try {
742
+ const parsed = JSON.parse(value);
743
+ if (typeof parsed === "object" && parsed !== null) return parsed;
744
+ getLogger().warn(`Parsed value for input ${inputName} is not an object`);
745
+ return null;
746
+ } catch {
747
+ getLogger().warn(`Failed to parse the object value for input ${inputName}`);
748
+ return null;
749
+ }
750
+ return null;
751
+ };
752
+ }
753
+ // src/grasshopper/io/input/normalize-default.ts
754
+ function dropDefault(input, message) {
755
+ getLogger().warn(message);
756
+ return {
757
+ schema: {
758
+ ...input,
759
+ default: null
760
+ },
761
+ warning: {
762
+ code: ErrorCodes.MALFORMED_DEFAULT,
763
+ message
764
+ }
765
+ };
766
+ }
767
+ /**
768
+ * Flatten a raw input's `default` DataTree into the shape the type parsers take.
769
+ * Never mutates `input`. A default it can't interpret is dropped with a `warning`.
770
+ */
771
+ function normalizeDefault(input, label) {
772
+ if (typeof input.default !== "object" || input.default === null) return { schema: input };
773
+ if (Array.isArray(input.default) || isDataTreeDefault(input.default)) return { schema: input };
774
+ if (!hasField(input.default, "innerTree")) return dropDefault(input, `Input "${label}" default had an unrecognized shape (no innerTree key); the default was dropped.`);
775
+ const innerTree = readField(input.default, "innerTree") ?? {};
776
+ if (Object.keys(innerTree).length === 0) return { schema: {
777
+ ...input,
778
+ default: void 0
779
+ } };
780
+ if (input.treeAccess || input.atMost && input.atMost > 1) {
781
+ const tree = {};
782
+ const invalidItems = [];
783
+ for (const [branch, items] of Object.entries(innerTree)) {
784
+ if (!Array.isArray(items)) return dropDefault(input, `Input "${label}" default had a non-array innerTree branch ("${branch}"); the default was dropped.`);
785
+ const parsed = [];
786
+ for (const item of items) {
787
+ const data = readField(item, "data");
788
+ const type = readField(item, "type");
789
+ if (type && NUMERIC_ITEM_TYPES.has(type)) {
790
+ if (typeof data === "string" && data.trim() === "") continue;
791
+ const num = numericTransformer(data);
792
+ if (num === null) {
793
+ invalidItems.push(`"${String(data)}" (${type})`);
794
+ continue;
795
+ }
796
+ parsed.push(INTEGER_ITEM_TYPES.has(type) ? Math.round(num) : num);
797
+ continue;
798
+ }
799
+ if (type === "System.Boolean") {
800
+ const bool = booleanTransformer(data);
801
+ if (bool === null) {
802
+ invalidItems.push(`"${String(data)}" (${type})`);
803
+ continue;
804
+ }
805
+ parsed.push(bool);
806
+ continue;
807
+ }
808
+ if (type === "System.String" && typeof data === "string") {
809
+ parsed.push(textTransformer(data));
810
+ continue;
811
+ }
812
+ if (typeof data === "string" && type?.startsWith("Rhino.Geometry")) {
813
+ try {
814
+ parsed.push(JSON.parse(data));
815
+ } catch {
816
+ parsed.push(data);
817
+ }
818
+ continue;
819
+ }
820
+ parsed.push(data);
821
+ }
822
+ setOwnField(tree, branch, parsed);
823
+ }
824
+ if (invalidItems.length > 0) {
825
+ const message = `Input "${label}" default contained ${invalidItems.length} tree value(s) that could not be parsed and were dropped: ${invalidItems.join(", ")}.`;
826
+ getLogger().warn(`Input "${label}" default: dropped ${invalidItems.length} tree value(s)`);
827
+ return {
828
+ schema: {
829
+ ...input,
830
+ default: tree
831
+ },
832
+ warning: {
833
+ code: ErrorCodes.MALFORMED_DEFAULT,
834
+ message
835
+ }
836
+ };
837
+ }
838
+ return { schema: {
839
+ ...input,
840
+ default: tree
841
+ } };
842
+ }
843
+ const allValues = [];
844
+ for (const items of Object.values(innerTree)) {
845
+ if (!Array.isArray(items)) continue;
846
+ for (const item of items) if (item && typeof item === "object" && hasField(item, "data")) allValues.push(readField(item, "data"));
847
+ }
848
+ return { schema: {
849
+ ...input,
850
+ default: allValues.length > 1 ? allValues : allValues[0]
851
+ } };
852
+ }
853
+ // src/grasshopper/io/input/numeric-rounding.ts
854
+ /** Snap float noise (`0.30000000000000004`) to `decimalPlaces`; a real difference is kept. */
855
+ function applyRounding(value, decimalPlaces) {
856
+ const rounded = Number(value.toFixed(decimalPlaces));
857
+ return Math.abs(value - rounded) < 1e-8 ? rounded : value;
858
+ }
859
+ const MAX_DECIMALS = 12;
860
+ const SIGNIFICANT_DIGITS = 15;
861
+ /**
862
+ * Decimal places `x` is written with, ignoring float noise, at most 12:
863
+ * `0.30000000000000004` → 1, `16384.2` → 1, `1e-7` → 7, `1.5e21` → 0.
864
+ */
865
+ function decimalsOf(x) {
866
+ if (!Number.isFinite(x)) return 0;
867
+ const clean = Number(Math.abs(x).toPrecision(SIGNIFICANT_DIGITS));
868
+ const [mantissa, exponent = "0"] = String(clean).split("e");
869
+ const decimals = (mantissa.split(".")[1]?.length ?? 0) - Number(exponent);
870
+ return Math.min(Math.max(decimals, 0), MAX_DECIMALS);
871
+ }
872
+ function getInputStepSize(value) {
873
+ if (!Number.isFinite(value) || value === 0) return .1;
874
+ const decimals = decimalsOf(value);
875
+ if (decimals === 0) return Math.abs(value) >= 1 ? 1 : .1;
876
+ return Number((10 ** -decimals).toFixed(decimals));
877
+ }
878
+ /** A server-authored `stepSize`, or `undefined` when absent or unusable. */
879
+ function serverStepSize(schema) {
880
+ return typeof schema.stepSize === "number" && Number.isFinite(schema.stepSize) && schema.stepSize > 0 ? schema.stepSize : void 0;
881
+ }
882
+ // src/grasshopper/io/input/input-type-parsers.ts
883
+ const labelOf = (schema) => schema.nickname || "unnamed";
884
+ const listOr = (schema, value) => (schema.atMost ?? 1) > 1 ? [value] : value;
885
+ const defaultStep = (schema) => serverStepSize(schema) ?? (schema.paramType === "Integer" ? 1 : .1);
886
+ const mapNumbers = (value, fn) => Array.isArray(value) ? value.map((v) => typeof v === "number" ? fn(v) : v) : typeof value === "number" ? fn(value) : value;
887
+ function computeNumeric(schema) {
888
+ if (isDataTreeDefault(schema.default)) return {
889
+ default: schema.default,
890
+ stepSize: defaultStep(schema)
891
+ };
892
+ if (typeof schema.default === "string" && schema.default.trim() !== "" && numericTransformer(schema.default) === null) throw new ComputeError(`Invalid numeric default "${schema.default}" for input "${schema.name || "unknown"}"`, ErrorCodes.VALIDATION_ERROR, { context: {
893
+ inputName: schema.name,
894
+ default: schema.default
895
+ } });
896
+ const value = coerceDefault(schema.default, numericTransformer, true);
897
+ if (schema.paramType === "Integer") return {
898
+ default: mapNumbers(value, Math.round),
899
+ stepSize: defaultStep(schema)
900
+ };
901
+ const firstValue = Array.isArray(value) ? value[0] : value;
902
+ const stepSource = [
903
+ firstValue,
904
+ schema.minimum,
905
+ schema.maximum
906
+ ].find((v) => typeof v === "number" && Number.isFinite(v) && v !== 0);
907
+ const stepSize = serverStepSize(schema) ?? (stepSource !== void 0 ? getInputStepSize(stepSource) : .1);
908
+ let decimalPlaces = decimalsOf(stepSize);
909
+ if (decimalPlaces === 0 && typeof firstValue === "number" && firstValue !== 0 && Math.abs(firstValue) < 1) {
910
+ const inferred = Math.ceil(-Math.log10(Math.abs(firstValue)));
911
+ if (Number.isFinite(inferred) && inferred > 0) decimalPlaces = inferred;
912
+ }
913
+ decimalPlaces = Math.min(Math.max(decimalPlaces, 0), 12);
914
+ return {
915
+ default: mapNumbers(value, (n) => applyRounding(n, decimalPlaces)),
916
+ stepSize
917
+ };
918
+ }
919
+ const numericParam = (schema, base, computed) => ({
920
+ ...base,
921
+ paramType: schema.paramType,
922
+ minimum: schema.minimum,
923
+ maximum: schema.maximum,
924
+ atLeast: schema.atLeast,
925
+ atMost: schema.atMost,
926
+ ...computed
927
+ });
928
+ const numericParser = {
929
+ parse(schema, base) {
930
+ return numericParam(schema, base, computeNumeric(schema));
931
+ },
932
+ fallback(schema, base) {
933
+ const safeValue = typeof schema.minimum === "number" && Number.isFinite(schema.minimum) && schema.minimum > 0 ? schema.minimum : 0;
934
+ return numericParam(schema, base, {
935
+ stepSize: defaultStep(schema),
936
+ default: listOr(schema, safeValue)
937
+ });
938
+ }
939
+ };
940
+ const booleanParser = {
941
+ parse(schema, base) {
942
+ if (isDataTreeDefault(schema.default)) return {
943
+ ...base,
944
+ paramType: "Boolean",
945
+ default: schema.default
946
+ };
947
+ const value = coerceDefault(schema.default, booleanTransformer, false);
948
+ if (value !== void 0 && value !== null && typeof value !== "boolean" && !Array.isArray(value)) throw new ComputeError(`Invalid boolean default "${String(value)}" for input "${schema.name || "unknown"}"`, ErrorCodes.VALIDATION_ERROR, { context: {
949
+ inputName: schema.name,
950
+ default: schema.default
951
+ } });
952
+ return {
953
+ ...base,
954
+ paramType: "Boolean",
955
+ default: value
956
+ };
957
+ },
958
+ fallback(schema, base) {
959
+ return {
960
+ ...base,
961
+ paramType: "Boolean",
962
+ default: listOr(schema, false)
963
+ };
964
+ }
965
+ };
966
+ const textParser = { parse(schema, base) {
967
+ const value = coerceDefault(schema.default, textTransformer, false);
968
+ return {
969
+ ...base,
970
+ paramType: "Text",
971
+ default: value
972
+ };
973
+ } };
974
+ const valueListParser = {
975
+ parse(schema, base, warn) {
976
+ if (!schema.values || typeof schema.values !== "object" || Object.keys(schema.values).length === 0) {
977
+ const inputName = labelOf(schema);
978
+ throw new ComputeError(`Input "${inputName}" has no values defined (expected ValueList)`, ErrorCodes.INVALID_INPUT, { context: {
979
+ inputName,
980
+ expectedType: "ValueList"
981
+ } });
982
+ }
983
+ let defaultValue;
984
+ if (schema.default !== void 0 && schema.default !== null) {
985
+ if (typeof schema.default === "object") throw new ComputeError(`ValueList input "${labelOf(schema)}" default is not a string-able value`, ErrorCodes.VALIDATION_ERROR, { context: {
986
+ inputName: schema.name,
987
+ default: schema.default
988
+ } });
989
+ const defaultLower = String(schema.default).toLowerCase();
990
+ const match = Object.keys(schema.values).find((key) => key.toLowerCase() === defaultLower);
991
+ defaultValue = match ?? String(schema.default);
992
+ if (match === void 0) {
993
+ const inputName = labelOf(schema);
994
+ getLogger().warn(`ValueList input "${inputName}" default is not in available values`);
995
+ warn?.({
996
+ code: ErrorCodes.VALIDATION_ERROR,
997
+ message: `ValueList input "${inputName}" default value "${schema.default}" is not in available values`
998
+ });
999
+ }
1000
+ }
1001
+ return {
1002
+ ...base,
1003
+ paramType: "ValueList",
1004
+ values: schema.values,
1005
+ default: defaultValue
1006
+ };
1007
+ },
1008
+ fallback(schema, base) {
1009
+ return {
1010
+ ...base,
1011
+ paramType: "ValueList",
1012
+ values: schema.values && typeof schema.values === "object" ? schema.values : {},
1013
+ default: void 0
1014
+ };
1015
+ }
1016
+ };
1017
+ const objectDefault = (schema) => coerceDefault(schema.default, objectTransformer(labelOf(schema)), true);
1018
+ function geometryFallback(schema, base) {
1019
+ return {
1020
+ ...base,
1021
+ paramType: "Geometry",
1022
+ default: listOr(schema, null)
1023
+ };
1024
+ }
1025
+ /** Keyed by canonical paramType. */
1026
+ const INPUT_TYPE_PARSERS = /* @__PURE__ */ new Map([
1027
+ ["Number", numericParser],
1028
+ ["Integer", numericParser],
1029
+ ["Boolean", booleanParser],
1030
+ ["Text", textParser],
1031
+ ["ValueList", valueListParser],
1032
+ ["Geometry", {
1033
+ parse(schema, base) {
1034
+ return {
1035
+ ...base,
1036
+ paramType: "Geometry",
1037
+ default: objectDefault(schema)
1038
+ };
1039
+ },
1040
+ fallback: geometryFallback
1041
+ }],
1042
+ ["File", { parse(schema, base) {
1043
+ return {
1044
+ ...base,
1045
+ paramType: "File",
1046
+ acceptedFormats: schema.acceptedFormats,
1047
+ default: objectDefault(schema)
1048
+ };
1049
+ } }],
1050
+ ["Color", { parse(schema, base) {
1051
+ const value = coerceDefault(schema.default, colorTransformer, false);
1052
+ return {
1053
+ ...base,
1054
+ paramType: "Color",
1055
+ default: value
1056
+ };
1057
+ } }]
1058
+ ]);
1059
+ const unknownTypeFallback = geometryFallback;
1060
+ // src/grasshopper/io/input/input-processors.ts
1061
+ const CANONICAL_PARAM_TYPES = new Map([...INPUT_TYPE_PARSERS.keys()].map((key) => [key.toLowerCase(), key]));
1062
+ /** An unknown paramType comes back unchanged, so its unknown-type error still surfaces. */
1063
+ function canonicalizeParamType(paramType) {
1064
+ return CANONICAL_PARAM_TYPES.get(paramType?.toLowerCase()) ?? paramType;
1065
+ }
1066
+ /**
1067
+ * Parse one raw Grasshopper input schema into a typed {@link InputParam}.
1068
+ * A validation failure is swallowed and replaced with a safe default.
1069
+ */
1070
+ function processInput(rawInput) {
1071
+ return processInputWithError(rawInput).input;
1072
+ }
1073
+ /**
1074
+ * Like {@link processInput}, but also returns every problem found on the way, in
1075
+ * order: a dropped default, warnings the parser recovered from, and the parser
1076
+ * error that forced the safe default. Entries carry the RAW declared `paramType`
1077
+ * so clients match on the casing they sent.
1078
+ *
1079
+ * A non-`ComputeError` failure still throws: it is a bug, not bad input.
1080
+ */
1081
+ function processInputWithError(rawInput) {
1082
+ const label = rawInput.name || "unknown";
1083
+ const errors = [];
1084
+ const report = ({ code, message }) => {
1085
+ errors.push({
1086
+ inputName: label,
1087
+ paramType: rawInput.paramType,
1088
+ message,
1089
+ code
1090
+ });
1091
+ };
1092
+ const baseInput = {
1093
+ description: rawInput.description,
1094
+ name: rawInput.name,
1095
+ nickname: rawInput.nickname,
1096
+ treeAccess: rawInput.treeAccess,
1097
+ groupName: rawInput.groupName ?? void 0,
1098
+ id: rawInput.id
1099
+ };
1100
+ const paramType = canonicalizeParamType(rawInput.paramType);
1101
+ let schema = {
1102
+ ...rawInput,
1103
+ paramType
1104
+ };
1105
+ try {
1106
+ const { schema: normalized, warning } = normalizeDefault(schema, label);
1107
+ schema = normalized;
1108
+ if (warning) report(warning);
1109
+ } catch (error) {
1110
+ schema = {
1111
+ ...schema,
1112
+ default: null
1113
+ };
1114
+ report({
1115
+ code: ErrorCodes.MALFORMED_DEFAULT,
1116
+ message: `Input "${label}" default could not be normalized and was dropped: ${error instanceof Error ? error.message : String(error)}`
1117
+ });
1118
+ }
1119
+ const parser = INPUT_TYPE_PARSERS.get(paramType);
1120
+ try {
1121
+ if (!parser) throw new ComputeError(`Unknown paramType: ${paramType}`, ErrorCodes.VALIDATION_ERROR, { context: {
1122
+ receivedParamType: paramType,
1123
+ paramName: rawInput.name
1124
+ } });
1125
+ return {
1126
+ input: parser.parse(schema, baseInput, report),
1127
+ errors
1128
+ };
1129
+ } catch (error) {
1130
+ if (!(error instanceof ComputeError)) throw new ComputeError(error instanceof Error ? error.message : String(error), ErrorCodes.VALIDATION_ERROR, {
1131
+ context: {
1132
+ paramName: rawInput.name,
1133
+ paramType
1134
+ },
1135
+ originalError: error
1136
+ });
1137
+ getLogger().error(`Validation error for input ${label}: ${error.code}`);
1138
+ report(error);
1139
+ return {
1140
+ input: (parser?.fallback ?? unknownTypeFallback)(schema, baseInput),
1141
+ errors
1142
+ };
1143
+ }
1144
+ }
1145
+ /** {@link processInputWithError} over a list, with every input's errors collected. */
1146
+ function processInputsWithErrors(rawInputs) {
1147
+ const inputs = [];
1148
+ const parseErrors = [];
1149
+ for (const raw of rawInputs) {
1150
+ const { input, errors } = processInputWithError(raw);
1151
+ inputs.push(input);
1152
+ parseErrors.push(...errors);
1153
+ }
1154
+ return {
1155
+ inputs,
1156
+ parseErrors
1157
+ };
1158
+ }
1159
+ // src/grasshopper/io/normalize-schema.ts
1160
+ /**
1161
+ * Canonicalize a raw `/io` param record's field keys to camelCase, so the
1162
+ * parsers can read `schema.paramType` whichever casing the server sent. Values,
1163
+ * including `default` and the user-authored `values` map, pass through verbatim.
1164
+ *
1165
+ * A missing required field gets a fallback that keeps the type true: `paramType`
1166
+ * `''` surfaces as an unknown-type parse error, and `groupName` stays `null`
1167
+ * (not `''`) so "no group" differs from an empty group name.
1168
+ */
1169
+ function normalizeInputSchema(raw) {
1170
+ const stepSize = readField(raw, "stepSize");
1171
+ return {
1172
+ id: readField(raw, "id") ?? "",
1173
+ name: readField(raw, "name") ?? "",
1174
+ nickname: readField(raw, "nickname") ?? null,
1175
+ description: readField(raw, "description") ?? "",
1176
+ paramType: readField(raw, "paramType") ?? "",
1177
+ treeAccess: readField(raw, "treeAccess") ?? false,
1178
+ minimum: readField(raw, "minimum") ?? null,
1179
+ maximum: readField(raw, "maximum") ?? null,
1180
+ atLeast: readField(raw, "atLeast") ?? 1,
1181
+ atMost: readField(raw, "atMost") ?? 1,
1182
+ stepSize: typeof stepSize === "number" && Number.isFinite(stepSize) ? stepSize : void 0,
1183
+ default: readField(raw, "default"),
1184
+ values: readField(raw, "values"),
1185
+ acceptedFormats: readField(raw, "acceptedFormats"),
1186
+ groupName: readField(raw, "groupName") ?? null
1187
+ };
1188
+ }
1189
+ function normalizeOutputSchema(raw) {
1190
+ return {
1191
+ name: readField(raw, "name") ?? "",
1192
+ nickname: readField(raw, "nickname") ?? null,
1193
+ paramType: readField(raw, "paramType") ?? "",
1194
+ id: readField(raw, "id") ?? ""
1195
+ };
1196
+ }
1197
+ // src/grasshopper/io/definition-io.ts
1198
+ /**
1199
+ * Fetches a definition's input/output schemas without per-type parsing.
1200
+ *
1201
+ * Field KEYS are canonicalized to camelCase and missing required fields get a fallback; a missing
1202
+ * or non-array `inputs` / `outputs` becomes `[]`. Field VALUES, notably `default` and the `values`
1203
+ * label map, pass through verbatim.
1204
+ *
1205
+ * @throws {ComputeError} If fetch fails or response is invalid
1206
+ */
1207
+ async function fetchDefinitionIO(definition, config) {
1208
+ warnIfClientSide("fetchDefinitionIO", config.suppressBrowserWarning);
1209
+ validateServerUrl(config.serverUrl);
1210
+ const { algo, pointer } = prepareGrasshopperArgs(definition, []);
1211
+ if (!algo && !pointer) throw new ComputeError("Definition must resolve to either a URL pointer or base64 algo", ErrorCodes.INVALID_INPUT);
1212
+ const response = await fetchCompute("io", algo ? { algo } : { pointer }, withGrasshopperErrorCodes(config));
1213
+ assertResponseObject(response);
1214
+ const loadWarnings = nonEmptyStrings(readField(response, "warnings"));
1215
+ const loadErrors = nonEmptyStrings(readField(response, "errors"));
1216
+ const rawInputs = readField(response, "inputs");
1217
+ const rawOutputs = readField(response, "outputs");
1218
+ return {
1219
+ inputs: Array.isArray(rawInputs) ? rawInputs.map(normalizeInputSchema) : [],
1220
+ outputs: Array.isArray(rawOutputs) ? rawOutputs.map(normalizeOutputSchema) : [],
1221
+ ...loadWarnings && { loadWarnings },
1222
+ ...loadErrors && { loadErrors }
1223
+ };
1224
+ }
1225
+ /**
1226
+ * Server diagnostics as non-blank strings, or `undefined` when there are none.
1227
+ * Non-string entries are kept, not dropped: a fork reporting `{ message }`
1228
+ * objects must not yield an empty input list with no explanation.
1229
+ */
1230
+ function nonEmptyStrings(value) {
1231
+ if (!Array.isArray(value)) return void 0;
1232
+ const cleaned = value.map(coerceDiagnostic).filter((v) => v !== void 0);
1233
+ return cleaned.length > 0 ? cleaned : void 0;
1234
+ }
1235
+ function coerceDiagnostic(value) {
1236
+ if (value === null || value === void 0) return void 0;
1237
+ let text;
1238
+ if (typeof value === "string") text = value;
1239
+ else if (typeof value === "object") {
1240
+ const message = readField(value, "message");
1241
+ if (typeof message === "string" && message.trim().length > 0) text = message;
1242
+ else try {
1243
+ text = JSON.stringify(value) ?? String(value);
1244
+ } catch {
1245
+ text = String(value);
1246
+ }
1247
+ } else text = String(value);
1248
+ return text.trim().length > 0 ? text : void 0;
1249
+ }
1250
+ /**
1251
+ * Fetches a definition's input/output schemas and parses each input into its
1252
+ * typed `InputParam`. An input that fails to parse gets a safe default
1253
+ * and an entry in `parseErrors`.
1254
+ *
1255
+ * @throws {ComputeError} If fetch fails or response is invalid
1256
+ *
1257
+ * @example
1258
+ * ```typescript
1259
+ * const { inputs, outputs } = await fetchParsedDefinitionIO(
1260
+ * 'https://example.com/definition.gh',
1261
+ * { serverUrl: 'http://localhost:6500', apiKey: 'YOUR_KEY' }
1262
+ * );
1263
+ *
1264
+ * // Inputs are now strongly typed
1265
+ * inputs.forEach(input => {
1266
+ * if (input.paramType === 'Number') {
1267
+ * console.log(input.minimum, input.maximum); // TypeScript knows these exist
1268
+ * }
1269
+ * });
1270
+ * ```
1271
+ */
1272
+ async function fetchParsedDefinitionIO(definition, config) {
1273
+ const { inputs: rawInputs, outputs, loadWarnings, loadErrors } = await fetchDefinitionIO(definition, config);
1274
+ const { inputs, parseErrors } = processInputsWithErrors(rawInputs);
1275
+ return {
1276
+ inputs,
1277
+ outputs,
1278
+ ...parseErrors.length > 0 && { parseErrors },
1279
+ ...loadWarnings && { loadWarnings },
1280
+ ...loadErrors && { loadErrors }
1281
+ };
1282
+ }
1283
+ // src/grasshopper/scheduler/response-cache.ts
1284
+ /**
1285
+ * For a response that never crossed the fetch boundary (custom executor), so carries no
1286
+ * wire-size hint. An unserializable response is sized 0 rather than failing the solve.
1287
+ */
1288
+ function estimateResponseSize(response) {
1289
+ try {
1290
+ return JSON.stringify(response)?.length ?? 0;
1291
+ } catch {
1292
+ return 0;
1293
+ }
1294
+ }
1295
+ /** The scheduler's response cache: LRU by bytes, with an optional TTL. */
1296
+ var ResponseCache = class {
1297
+ maxBytes;
1298
+ ttlMs;
1299
+ cacheErroredSolves;
1300
+ entries = /* @__PURE__ */ new Map();
1301
+ bytes = 0;
1302
+ hits = 0;
1303
+ misses = 0;
1304
+ evictions = 0;
1305
+ constructor(options) {
1306
+ const config = options || null;
1307
+ this.maxBytes = Math.max(0, config?.maxBytes ?? 0);
1308
+ this.ttlMs = config?.ttlMs ?? 0;
1309
+ this.cacheErroredSolves = config?.cacheErroredSolves ?? true;
1310
+ }
1311
+ /** A zero budget retains nothing, so it is off rather than a cache that can only ever miss. */
1312
+ get enabled() {
1313
+ return this.maxBytes > 0;
1314
+ }
1315
+ get(key) {
1316
+ if (!this.enabled) return null;
1317
+ const entry = this.entries.get(key);
1318
+ if (!entry) {
1319
+ this.misses += 1;
1320
+ return null;
1321
+ }
1322
+ if (this.ttlMs > 0 && Date.now() - entry.insertedAt > this.ttlMs) {
1323
+ this.drop(key);
1324
+ this.misses += 1;
1325
+ return null;
1326
+ }
1327
+ this.entries.delete(key);
1328
+ this.entries.set(key, entry);
1329
+ this.hits += 1;
1330
+ return entry.response;
1331
+ }
1332
+ set(key, response) {
1333
+ if (!this.enabled) return;
1334
+ const solveErrors = readField(response, "errors");
1335
+ if (!this.cacheErroredSolves && Array.isArray(solveErrors) && solveErrors.length > 0) return;
1336
+ const outcome = readField(readField(response, "selva"), "outcome");
1337
+ if (readField(outcome, "aborted") === true) return;
1338
+ const sizeBytes = getResponseWireSize(response) ?? estimateResponseSize(response);
1339
+ if (sizeBytes > this.maxBytes) return;
1340
+ this.drop(key);
1341
+ this.entries.set(key, {
1342
+ response,
1343
+ insertedAt: Date.now(),
1344
+ sizeBytes
1345
+ });
1346
+ this.bytes += sizeBytes;
1347
+ while (this.bytes > this.maxBytes && this.entries.size > 0) {
1348
+ const oldest = this.entries.keys().next().value;
1349
+ if (oldest === void 0) break;
1350
+ this.drop(oldest);
1351
+ this.evictions += 1;
1352
+ }
1353
+ }
1354
+ clear() {
1355
+ this.entries.clear();
1356
+ this.bytes = 0;
1357
+ }
1358
+ stats() {
1359
+ return {
1360
+ entries: this.entries.size,
1361
+ bytes: this.bytes,
1362
+ hits: this.hits,
1363
+ misses: this.misses,
1364
+ evictions: this.evictions
1365
+ };
1366
+ }
1367
+ drop(key) {
1368
+ const entry = this.entries.get(key);
1369
+ if (!entry) return;
1370
+ this.entries.delete(key);
1371
+ this.bytes -= entry.sizeBytes;
1372
+ }
1373
+ };
1374
+ // src/core/utils/sha256.ts
1375
+ const nodeCrypto = globalThis.process?.getBuiltinModule?.("node:crypto");
1376
+ /** Lowercase hex SHA-256 of `input`. */
1377
+ function sha256Hex(input) {
1378
+ if (nodeCrypto) {
1379
+ const hash = nodeCrypto.createHash("sha256");
1380
+ if (typeof input === "string") hash.update(input, "utf16le");
1381
+ else hash.update(input);
1382
+ return hash.digest("hex");
1383
+ }
1384
+ return sha256HexPortable(input);
1385
+ }
1386
+ const K = new Int32Array([
1387
+ 1116352408,
1388
+ 1899447441,
1389
+ 3049323471,
1390
+ 3921009573,
1391
+ 961987163,
1392
+ 1508970993,
1393
+ 2453635748,
1394
+ 2870763221,
1395
+ 3624381080,
1396
+ 310598401,
1397
+ 607225278,
1398
+ 1426881987,
1399
+ 1925078388,
1400
+ 2162078206,
1401
+ 2614888103,
1402
+ 3248222580,
1403
+ 3835390401,
1404
+ 4022224774,
1405
+ 264347078,
1406
+ 604807628,
1407
+ 770255983,
1408
+ 1249150122,
1409
+ 1555081692,
1410
+ 1996064986,
1411
+ 2554220882,
1412
+ 2821834349,
1413
+ 2952996808,
1414
+ 3210313671,
1415
+ 3336571891,
1416
+ 3584528711,
1417
+ 113926993,
1418
+ 338241895,
1419
+ 666307205,
1420
+ 773529912,
1421
+ 1294757372,
1422
+ 1396182291,
1423
+ 1695183700,
1424
+ 1986661051,
1425
+ 2177026350,
1426
+ 2456956037,
1427
+ 2730485921,
1428
+ 2820302411,
1429
+ 3259730800,
1430
+ 3345764771,
1431
+ 3516065817,
1432
+ 3600352804,
1433
+ 4094571909,
1434
+ 275423344,
1435
+ 430227734,
1436
+ 506948616,
1437
+ 659060556,
1438
+ 883997877,
1439
+ 958139571,
1440
+ 1322822218,
1441
+ 1537002063,
1442
+ 1747873779,
1443
+ 1955562222,
1444
+ 2024104815,
1445
+ 2227730452,
1446
+ 2361852424,
1447
+ 2428436474,
1448
+ 2756734187,
1449
+ 3204031479,
1450
+ 3329325298
1451
+ ]);
1452
+ /** Exported so tests can check it against Node's native hash. */
1453
+ function sha256HexPortable(input) {
1454
+ const state = new Sha256();
1455
+ if (typeof input === "string") state.updateString(input);
1456
+ else state.update(input);
1457
+ return state.digestHex();
1458
+ }
1459
+ var Sha256 = class {
1460
+ h = new Int32Array([
1461
+ 1779033703,
1462
+ 3144134277,
1463
+ 1013904242,
1464
+ 2773480762,
1465
+ 1359893119,
1466
+ 2600822924,
1467
+ 528734635,
1468
+ 1541459225
1469
+ ]);
1470
+ w = /* @__PURE__ */ new Int32Array(64);
1471
+ pending = /* @__PURE__ */ new Uint8Array(64);
1472
+ pendingLength = 0;
1473
+ totalBytes = 0;
1474
+ update(bytes) {
1475
+ this.totalBytes += bytes.length;
1476
+ let offset = 0;
1477
+ if (this.pendingLength > 0) {
1478
+ const take = Math.min(64 - this.pendingLength, bytes.length);
1479
+ this.pending.set(bytes.subarray(0, take), this.pendingLength);
1480
+ this.pendingLength += take;
1481
+ offset = take;
1482
+ if (this.pendingLength < 64) return;
1483
+ this.compressBytes(this.pending, 0);
1484
+ this.pendingLength = 0;
1485
+ }
1486
+ for (; offset + 64 <= bytes.length; offset += 64) this.compressBytes(bytes, offset);
1487
+ this.pending.set(bytes.subarray(offset), 0);
1488
+ this.pendingLength = bytes.length - offset;
1489
+ }
1490
+ /**
1491
+ * Feed a string's UTF-16LE bytes straight into the message words, skipping a byte buffer.
1492
+ * Only on a fresh state: 32 code units fill a block exactly, so nothing may be pending.
1493
+ */
1494
+ updateString(s) {
1495
+ const w = this.w;
1496
+ const whole = s.length - s.length % 32;
1497
+ for (let offset = 0; offset < whole; offset += 32) {
1498
+ for (let i = 0; i < 16; i++) {
1499
+ const c0 = s.charCodeAt(offset + 2 * i);
1500
+ const c1 = s.charCodeAt(offset + 2 * i + 1);
1501
+ w[i] = (c0 & 255) << 24 | c0 >>> 8 << 16 | (c1 & 255) << 8 | c1 >>> 8;
1502
+ }
1503
+ this.compress();
1504
+ }
1505
+ this.totalBytes += whole * 2;
1506
+ const rest = new Uint8Array((s.length - whole) * 2);
1507
+ for (let i = whole, j = 0; i < s.length; i++) {
1508
+ const code = s.charCodeAt(i);
1509
+ rest[j++] = code & 255;
1510
+ rest[j++] = code >>> 8;
1511
+ }
1512
+ this.update(rest);
1513
+ }
1514
+ digestHex() {
1515
+ const bitLength = this.totalBytes * 8;
1516
+ const tail = new Uint8Array(this.pendingLength < 56 ? 64 : 128);
1517
+ tail.set(this.pending.subarray(0, this.pendingLength));
1518
+ tail[this.pendingLength] = 128;
1519
+ const view = new DataView(tail.buffer);
1520
+ view.setUint32(tail.length - 8, Math.floor(bitLength / 4294967296));
1521
+ view.setUint32(tail.length - 4, bitLength >>> 0);
1522
+ for (let offset = 0; offset < tail.length; offset += 64) this.compressBytes(tail, offset);
1523
+ let hex = "";
1524
+ for (const word of this.h) hex += (word >>> 0).toString(16).padStart(8, "0");
1525
+ return hex;
1526
+ }
1527
+ compressBytes(bytes, offset) {
1528
+ const w = this.w;
1529
+ for (let i = 0; i < 16; i++) {
1530
+ const j = offset + i * 4;
1531
+ w[i] = bytes[j] << 24 | bytes[j + 1] << 16 | bytes[j + 2] << 8 | bytes[j + 3];
1532
+ }
1533
+ this.compress();
1534
+ }
1535
+ /** One block, from the 16 message words already in `w`. */
1536
+ compress() {
1537
+ const w = this.w;
1538
+ for (let i = 16; i < 64; i++) {
1539
+ const x = w[i - 15];
1540
+ const y = w[i - 2];
1541
+ const s0 = (x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ x >>> 3;
1542
+ const s1 = (y >>> 17 | y << 15) ^ (y >>> 19 | y << 13) ^ y >>> 10;
1543
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
1544
+ }
1545
+ const h = this.h;
1546
+ let a = h[0];
1547
+ let b = h[1];
1548
+ let c = h[2];
1549
+ let d = h[3];
1550
+ let e = h[4];
1551
+ let f = h[5];
1552
+ let g = h[6];
1553
+ let k = h[7];
1554
+ for (let i = 0; i < 64; i++) {
1555
+ const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
1556
+ const ch = e & f ^ ~e & g;
1557
+ const t1 = k + S1 + ch + K[i] + w[i] | 0;
1558
+ const t2 = ((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c) | 0;
1559
+ k = g;
1560
+ g = f;
1561
+ f = e;
1562
+ e = d + t1 | 0;
1563
+ d = c;
1564
+ c = b;
1565
+ b = a;
1566
+ a = t1 + t2 | 0;
1567
+ }
1568
+ h[0] = h[0] + a | 0;
1569
+ h[1] = h[1] + b | 0;
1570
+ h[2] = h[2] + c | 0;
1571
+ h[3] = h[3] + d | 0;
1572
+ h[4] = h[4] + e | 0;
1573
+ h[5] = h[5] + f | 0;
1574
+ h[6] = h[6] + g | 0;
1575
+ h[7] = h[7] + k | 0;
1576
+ }
1577
+ };
1578
+ // src/grasshopper/scheduler/stable-hash.ts
1579
+ /**
1580
+ * Solve-key hashing, public so an app-layer durable cache or a pre-solved bundle viewer can
1581
+ * reproduce the scheduler's exact key. A key that canonicalizes even slightly differently
1582
+ * misses every entry the scheduler wrote, or worse, collides differently.
1583
+ *
1584
+ * Keys are SHA-256: one scheduler's cache serves every user of a server, so a key a user can
1585
+ * collide on purpose lets them choose what another user's solve returns.
1586
+ */
1587
+ /**
1588
+ * Deterministic stringify with sorted keys. {a:1,b:2} and {b:2,a:1} produce
1589
+ * the same string. Safely handles circular references and non-finite numbers.
1590
+ *
1591
+ * The output is a cache key, so the invariant that matters is: two payloads
1592
+ * that serialize differently on the wire must stringify differently here
1593
+ * (false misses are harmless; false hits serve the wrong cached solve).
1594
+ * Non-JSON values get unquoted markers (`{u8:…}`, `[Circular]`) that no JSON
1595
+ * value stringifies to, so a plain object can't pose as one.
1596
+ */
1597
+ function stableStringify(value) {
1598
+ const path = /* @__PURE__ */ new WeakSet();
1599
+ const stringify = (v) => {
1600
+ if (v === null) return "null";
1601
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
1602
+ if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
1603
+ if (typeof v === "bigint") return `${v}n`;
1604
+ if (typeof v !== "object") return "undefined";
1605
+ if (v instanceof Uint8Array) return `{u8:${sha256Hex(v)}}`;
1606
+ if (path.has(v)) return "[Circular]";
1607
+ path.add(v);
1608
+ let out;
1609
+ if (Array.isArray(v)) {
1610
+ const parts = [];
1611
+ for (let i = 0; i < v.length; i++) parts.push(stringify(v[i]));
1612
+ out = `[${parts.join(",")}]`;
1613
+ } else if (typeof v.toJSON === "function") out = stringify(v.toJSON());
1614
+ else if (v instanceof Map) out = `{map:[${[...v.entries()].map(([k, val]) => `[${stringify(k)},${stringify(val)}]`).sort().join(",")}]}`;
1615
+ else if (v instanceof Set) out = `{set:[${[...v.values()].map(stringify).sort().join(",")}]}`;
1616
+ else out = `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${stringify(v[k])}`).join(",")}}`;
1617
+ path.delete(v);
1618
+ return out;
1619
+ };
1620
+ return stringify(value);
1621
+ }
1622
+ /** The scheduler's solve-cache key for a definition and its input tree. */
1623
+ function hashSolveInput(definition, dataTree) {
1624
+ return hashSolveInputForDefinition(hashDefinition(definition), dataTree);
1625
+ }
1626
+ /**
1627
+ * `hashSolveInput` from a precomputed {@link hashDefinition}, so the scheduler, which also keys its
1628
+ * server-cache-key map by that hash, passes over a multi-MB definition once per solve.
1629
+ */
1630
+ function hashSolveInputForDefinition(definitionHash, dataTree) {
1631
+ return `${definitionHash}|t:${sha256Hex(stableStringify(dataTree))}`;
1632
+ }
1633
+ /**
1634
+ * A definition's identity, without its inputs. Strings and bytes are hashed over their full
1635
+ * content; a `DefinitionRef` uses its `key`.
1636
+ */
1637
+ function hashDefinition(definition) {
1638
+ if (isDefinitionRef(definition)) return `r:${definition.key}`;
1639
+ return typeof definition === "string" ? `s:${sha256Hex(definition)}` : `u8:${sha256Hex(definition)}`;
1640
+ }
1641
+ // src/grasshopper/scheduler/solve-scheduler.ts
1642
+ const SERVER_CACHE_KEYS_MAX = 100;
1643
+ /** `NaN` passes `Math.max` and fails every `inFlight < limit`, so nothing would ever dispatch. */
1644
+ function concurrencyLimit(value) {
1645
+ if (Number.isNaN(value)) throw new ComputeError("maxConcurrent must be a number", ErrorCodes.INVALID_CONFIG, { context: { maxConcurrent: value } });
1646
+ return Math.max(1, Math.floor(value));
1647
+ }
1648
+ const u8DefinitionHashes = /* @__PURE__ */ new WeakMap();
1649
+ /**
1650
+ * Remembers a buffer's hash by identity: a buffer mutated after its first solve keeps its old
1651
+ * hash and is served the old content's cached solves.
1652
+ */
1653
+ function hashDefinitionOnce(definition) {
1654
+ if (!(definition instanceof Uint8Array)) return hashDefinition(definition);
1655
+ let hash = u8DefinitionHashes.get(definition);
1656
+ if (hash === void 0) {
1657
+ hash = hashDefinition(definition);
1658
+ u8DefinitionHashes.set(definition, hash);
1659
+ }
1660
+ return hash;
1661
+ }
1662
+ /** One solve's input hashes, each taken on first use. */
1663
+ var SolveInputHashes = class {
1664
+ hashDefinition;
1665
+ dataTree;
1666
+ definitionMemo;
1667
+ keyMemo;
1668
+ constructor(hashDefinition, dataTree) {
1669
+ this.hashDefinition = hashDefinition;
1670
+ this.dataTree = dataTree;
1671
+ }
1672
+ get definitionHash() {
1673
+ return this.definitionMemo ??= this.hashDefinition();
1674
+ }
1675
+ get key() {
1676
+ return this.keyMemo ??= hashSolveInputForDefinition(this.definitionHash, this.dataTree);
1677
+ }
1678
+ };
1679
+ /**
1680
+ * The FIFO behind `queue` mode. Removing an item only drops it from `live`; the array is swept once
1681
+ * the dead entries outnumber the live ones, so aborting n queued solves costs O(n), not O(n²).
1682
+ */
1683
+ var SolveQueue = class {
1684
+ items = [];
1685
+ head = 0;
1686
+ live = /* @__PURE__ */ new Set();
1687
+ get size() {
1688
+ return this.live.size;
1689
+ }
1690
+ push(item) {
1691
+ this.items.push(item);
1692
+ this.live.add(item);
1693
+ }
1694
+ remove(item) {
1695
+ if (this.live.delete(item)) this.compact();
1696
+ }
1697
+ shift() {
1698
+ while (this.head < this.items.length) {
1699
+ const item = this.items[this.head];
1700
+ this.items[this.head++] = void 0;
1701
+ if (item && this.live.delete(item)) {
1702
+ this.compact();
1703
+ return item;
1704
+ }
1705
+ }
1706
+ }
1707
+ compact() {
1708
+ if (this.live.size === 0) {
1709
+ this.items = [];
1710
+ this.head = 0;
1711
+ return;
1712
+ }
1713
+ const dead = this.items.length - this.live.size;
1714
+ if (dead <= 64 || dead <= this.live.size) return;
1715
+ this.items = this.items.filter((item) => item !== void 0 && this.live.has(item));
1716
+ this.head = 0;
1717
+ }
1718
+ };
1719
+ /**
1720
+ * Adds to a plain solve call:
1721
+ * - Scheduling (latest-wins for sliders, queue for jobs)
1722
+ * - Backpressure (bounded queue depth + queue-wait deadline)
1723
+ * - Cancellation (per-call signal + cancelAll)
1724
+ * - Optional response caching for repeated inputs
1725
+ * - Lifecycle hooks (start / settle / superseded) and state via subscribe()
1726
+ *
1727
+ * @example
1728
+ * ```ts
1729
+ * const scheduler = client.createScheduler({ mode: 'latest-wins', timeoutMs: 30_000 });
1730
+ *
1731
+ * // From a slider handler:
1732
+ * scheduler.solve(definition, tree).then((result) => {
1733
+ * updateMeshes(result);
1734
+ * }).catch((err) => {
1735
+ * if (err.code !== 'SUPERSEDED') showError(err);
1736
+ * });
1737
+ *
1738
+ * // From a UI binding:
1739
+ * scheduler.subscribe(() => {
1740
+ * showSpinner = scheduler.isSolving;
1741
+ * });
1742
+ * ```
1743
+ */
1744
+ var SolveScheduler = class {
1745
+ executor;
1746
+ baseConfig;
1747
+ mode;
1748
+ maxConcurrent;
1749
+ maxQueueDepth;
1750
+ queueWaitMs;
1751
+ timeoutMs;
1752
+ retry;
1753
+ cache;
1754
+ reuseServerDefinitionCache;
1755
+ /** Definition hash → the server's cache key (`pointer`) for it. */
1756
+ serverCacheKeys = /* @__PURE__ */ new Map();
1757
+ /** A string definition re-sent unchanged is compared with this, far cheaper than hashing it. */
1758
+ lastStringDefinition = null;
1759
+ onStart;
1760
+ onSettle;
1761
+ onSuperseded;
1762
+ subscribers = /* @__PURE__ */ new Set();
1763
+ inFlight = /* @__PURE__ */ new Set();
1764
+ pendingForLatestWins = null;
1765
+ fifo = new SolveQueue();
1766
+ _lastResult = null;
1767
+ _lastError = null;
1768
+ _lastDurationMs = null;
1769
+ solveSeq = 0;
1770
+ /** seq of the solve that last wrote `_lastResult`/`_lastError`. */
1771
+ lastStateSeq = 0;
1772
+ disposed = false;
1773
+ constructor(executor, baseConfig, options = {}) {
1774
+ this.executor = executor;
1775
+ this.baseConfig = baseConfig;
1776
+ this.mode = options.mode ?? "latest-wins";
1777
+ if (this.mode !== "latest-wins" && this.mode !== "queue") throw new ComputeError(`Unknown scheduler mode '${String(this.mode)}'. For concurrent solves use { mode: 'queue', maxConcurrent: 4 }.`, ErrorCodes.INVALID_CONFIG, { context: { mode: this.mode } });
1778
+ this.maxConcurrent = concurrencyLimit(options.maxConcurrent ?? 1);
1779
+ this.maxQueueDepth = options.maxQueueDepth !== void 0 && options.maxQueueDepth > 0 ? Math.floor(options.maxQueueDepth) : void 0;
1780
+ this.queueWaitMs = options.queueWaitMs !== void 0 && options.queueWaitMs > 0 ? options.queueWaitMs : void 0;
1781
+ this.timeoutMs = options.timeoutMs;
1782
+ this.retry = options.retry;
1783
+ this.cache = new ResponseCache(options.cache);
1784
+ this.reuseServerDefinitionCache = options.reuseServerDefinitionCache ?? true;
1785
+ this.onStart = options.onStart;
1786
+ this.onSettle = options.onSettle;
1787
+ this.onSuperseded = options.onSuperseded;
1788
+ }
1789
+ get isSolving() {
1790
+ return this.inFlight.size > 0;
1791
+ }
1792
+ get hasPending() {
1793
+ return this.pendingForLatestWins !== null || this.fifo.size > 0;
1794
+ }
1795
+ get inFlightCount() {
1796
+ return this.inFlight.size;
1797
+ }
1798
+ get queueDepth() {
1799
+ return this.fifo.size + (this.pendingForLatestWins ? 1 : 0);
1800
+ }
1801
+ get lastResult() {
1802
+ return this._lastResult;
1803
+ }
1804
+ get lastError() {
1805
+ return this._lastError;
1806
+ }
1807
+ get lastDurationMs() {
1808
+ return this._lastDurationMs;
1809
+ }
1810
+ /**
1811
+ * Adjust how many solves may run at once, for when the compute server's worker
1812
+ * pool changes size after this scheduler was built.
1813
+ *
1814
+ * Raising it drains queued work immediately. Lowering it never interrupts work
1815
+ * already in flight — those finish above the new limit, and the cap applies from
1816
+ * the next dispatch. Values below 1 are clamped, since 0 would wedge the queue.
1817
+ *
1818
+ * @throws {ComputeError} `INVALID_CONFIG` for `NaN`, which would wedge it too.
1819
+ */
1820
+ setMaxConcurrent(value) {
1821
+ const next = concurrencyLimit(value);
1822
+ if (next === this.maxConcurrent) return;
1823
+ const raised = next > this.maxConcurrent;
1824
+ this.maxConcurrent = next;
1825
+ if (raised) this.drainNext();
1826
+ }
1827
+ /** Subscribe to state changes. */
1828
+ subscribe(listener) {
1829
+ this.subscribers.add(listener);
1830
+ return () => this.subscribers.delete(listener);
1831
+ }
1832
+ notify() {
1833
+ for (const listener of this.subscribers) try {
1834
+ listener();
1835
+ } catch (err) {
1836
+ getLogger().error("[SolveScheduler] subscriber threw:", err);
1837
+ }
1838
+ }
1839
+ /**
1840
+ * Schedule a solve. Returns a promise that:
1841
+ * - Resolves with the compute response on success.
1842
+ * - Rejects with `ComputeError` on failure.
1843
+ * - Rejects with `code: ErrorCodes.SUPERSEDED` when the call was canceled because
1844
+ * newer values arrived (latest-wins mode).
1845
+ * - Rejects with `code: ErrorCodes.ABORTED` when the call was canceled via
1846
+ * its own `signal` (queued or running) or `cancelAll()`.
1847
+ * - Rejects with `code: ErrorCodes.QUEUE_FULL` when `maxQueueDepth` is set and
1848
+ * the queue was already full (backpressure; `statusCode: 503`).
1849
+ * - Rejects with `code: ErrorCodes.QUEUE_TIMEOUT` when `queueWaitMs` is set and
1850
+ * the call sat queued longer than that before starting (`statusCode: 503`).
1851
+ *
1852
+ * Don't mutate a `Uint8Array` definition after solving it. Responses served
1853
+ * from the cache (and via `lastResult`) are shared objects, not copies —
1854
+ * treat them as immutable. Mutating one poisons every later cache hit for
1855
+ * that key.
1856
+ *
1857
+ * Partial-success contract: unlike `GrasshopperClient.solve()`, which throws
1858
+ * `COMPUTATION_ERROR` when the response carries solver errors, this RESOLVES
1859
+ * with the response as-is — check `response.errors` yourself if a partial
1860
+ * success must not be treated as a result (see `cacheErroredSolves` for the
1861
+ * caching side of the same distinction).
1862
+ */
1863
+ solve(definition, dataTree, options) {
1864
+ return this.solveDetailed(definition, dataTree, options).then((detail) => detail.response);
1865
+ }
1866
+ /** {@link solve}, also resolving with whether this call hit the cache and how long it took. */
1867
+ solveDetailed(definition, dataTree, options) {
1868
+ if (this.disposed) return Promise.reject(new ComputeError("SolveScheduler has been disposed and cannot be used", ErrorCodes.INVALID_STATE));
1869
+ const hashes = new SolveInputHashes(() => this.hashDefinitionMemo(definition), dataTree);
1870
+ let key = "";
1871
+ if (this.cache.enabled || this.onStart || this.onSettle || this.onSuperseded) try {
1872
+ key = hashes.key;
1873
+ } catch (error) {
1874
+ return Promise.reject(new ComputeError("Solve input could not be hashed", ErrorCodes.INVALID_INPUT, { originalError: error }));
1875
+ }
1876
+ const seq = ++this.solveSeq;
1877
+ const ctx = {
1878
+ key,
1879
+ enqueuedAt: Date.now(),
1880
+ startedAt: null
1881
+ };
1882
+ if (options?.signal?.aborted) return Promise.reject(this.makeAbortError(ctx));
1883
+ const cached = this.cache.get(key);
1884
+ if (cached) {
1885
+ if (this.mode === "latest-wins") this.supersedeCurrent();
1886
+ const result = {
1887
+ status: "success",
1888
+ response: cached,
1889
+ durationMs: 0,
1890
+ fromCache: true
1891
+ };
1892
+ this.writeLastState(seq, {
1893
+ result: cached,
1894
+ durationMs: 0
1895
+ });
1896
+ this.runHook(this.onStart, ctx);
1897
+ this.runHook(this.onSettle, ctx, result);
1898
+ this.notify();
1899
+ return Promise.resolve({
1900
+ response: cached,
1901
+ fromCache: true,
1902
+ durationMs: 0
1903
+ });
1904
+ }
1905
+ return new Promise((resolve, reject) => {
1906
+ const item = {
1907
+ definition,
1908
+ dataTree,
1909
+ hashes,
1910
+ ctx,
1911
+ seq,
1912
+ resolve,
1913
+ reject,
1914
+ externalSignal: options?.signal,
1915
+ selvaevents: options?.selvaevents,
1916
+ onServerTiming: options?.onServerTiming
1917
+ };
1918
+ if (item.externalSignal) {
1919
+ item.onExternalAbort = () => {
1920
+ if (item.controller) item.controller.abort();
1921
+ else this.abortQueuedItem(item);
1922
+ };
1923
+ item.externalSignal.addEventListener("abort", item.onExternalAbort, { once: true });
1924
+ }
1925
+ this.enqueue(item);
1926
+ });
1927
+ }
1928
+ /** A slow solve settling late must not overwrite state a newer solve or cache hit published. */
1929
+ writeLastState(seq, state) {
1930
+ if (seq < this.lastStateSeq) return;
1931
+ this.lastStateSeq = seq;
1932
+ if ("result" in state) {
1933
+ this._lastResult = state.result;
1934
+ this._lastError = null;
1935
+ } else this._lastError = state.error;
1936
+ this._lastDurationMs = state.durationMs;
1937
+ }
1938
+ /** latest-wins: supersede the pending item and abort everything in flight. */
1939
+ supersedeCurrent() {
1940
+ if (this.pendingForLatestWins) {
1941
+ this.supersede(this.pendingForLatestWins);
1942
+ this.pendingForLatestWins = null;
1943
+ }
1944
+ for (const inflight of this.inFlight) {
1945
+ this.supersede(inflight);
1946
+ inflight.controller.abort();
1947
+ }
1948
+ }
1949
+ dequeue(item) {
1950
+ if (this.pendingForLatestWins === item) this.pendingForLatestWins = null;
1951
+ this.fifo.remove(item);
1952
+ }
1953
+ abortQueuedItem(item) {
1954
+ this.dequeue(item);
1955
+ this.fail(item, this.makeAbortError(item.ctx));
1956
+ this.notify();
1957
+ }
1958
+ /** The structured context lets an HTTP layer map QUEUE_FULL to 503 + Retry-After. */
1959
+ shedAsQueueFull(item) {
1960
+ const err = new ComputeError("Solve queue is full; request rejected (backpressure)", ErrorCodes.QUEUE_FULL, {
1961
+ statusCode: 503,
1962
+ context: {
1963
+ queueDepth: this.fifo.size,
1964
+ maxQueueDepth: this.maxQueueDepth
1965
+ }
1966
+ });
1967
+ this.fail(item, err);
1968
+ this.notify();
1969
+ }
1970
+ armQueueWaitTimer(item) {
1971
+ if (this.queueWaitMs === void 0) return;
1972
+ const waitMs = this.queueWaitMs;
1973
+ item.queueWaitTimer = setTimeout(() => {
1974
+ if (item.settled) return;
1975
+ this.dequeue(item);
1976
+ const err = new ComputeError(`Solve waited longer than ${waitMs}ms in queue; rejected (backpressure)`, ErrorCodes.QUEUE_TIMEOUT, {
1977
+ statusCode: 503,
1978
+ context: {
1979
+ waitedMs: Date.now() - item.ctx.enqueuedAt,
1980
+ queueWaitMs: waitMs
1981
+ }
1982
+ });
1983
+ this.fail(item, err);
1984
+ this.notify();
1985
+ }, waitMs);
1986
+ }
1987
+ clearQueueWaitTimer(item) {
1988
+ if (item.queueWaitTimer !== void 0) {
1989
+ clearTimeout(item.queueWaitTimer);
1990
+ item.queueWaitTimer = void 0;
1991
+ }
1992
+ }
1993
+ enqueue(item) {
1994
+ switch (this.mode) {
1995
+ case "latest-wins":
1996
+ this.supersedeCurrent();
1997
+ if (this.inFlight.size === 0) this.execute(item);
1998
+ else this.pendingForLatestWins = item;
1999
+ break;
2000
+ case "queue": if (this.inFlight.size < this.maxConcurrent) this.execute(item);
2001
+ else if (this.maxQueueDepth !== void 0 && this.fifo.size >= this.maxQueueDepth) this.shedAsQueueFull(item);
2002
+ else {
2003
+ this.armQueueWaitTimer(item);
2004
+ this.fifo.push(item);
2005
+ }
2006
+ }
2007
+ this.notify();
2008
+ }
2009
+ async execute(item) {
2010
+ const controller = new AbortController();
2011
+ const inflight = item;
2012
+ inflight.controller = controller;
2013
+ this.inFlight.add(inflight);
2014
+ item.ctx.startedAt = Date.now();
2015
+ this.clearQueueWaitTimer(item);
2016
+ this.runHook(this.onStart, item.ctx);
2017
+ this.notify();
2018
+ const startTime = performance.now();
2019
+ try {
2020
+ const config = {
2021
+ ...this.baseConfig,
2022
+ signal: controller.signal,
2023
+ ...this.timeoutMs !== void 0 && { timeoutMs: this.timeoutMs },
2024
+ ...this.retry !== void 0 && { retry: this.retry },
2025
+ ...item.selvaevents !== void 0 && { selvaevents: item.selvaevents }
2026
+ };
2027
+ const callTiming = item.onServerTiming;
2028
+ if (callTiming) {
2029
+ const baseTiming = this.baseConfig.onServerTiming;
2030
+ config.onServerTiming = (timing, requestId) => {
2031
+ try {
2032
+ baseTiming?.(timing, requestId);
2033
+ } finally {
2034
+ callTiming(timing, requestId);
2035
+ }
2036
+ };
2037
+ }
2038
+ const { response, definitionReuploaded } = await this.runExecutor(item, config);
2039
+ const durationMs = performance.now() - startTime;
2040
+ if (!this.disposed) this.cache.set(item.ctx.key, response);
2041
+ if (!this.settleSuccess(item, {
2042
+ response,
2043
+ fromCache: false,
2044
+ definitionReuploaded,
2045
+ durationMs
2046
+ })) return;
2047
+ this.writeLastState(item.seq, {
2048
+ result: response,
2049
+ durationMs
2050
+ });
2051
+ this.runHook(this.onSettle, item.ctx, {
2052
+ status: "success",
2053
+ response,
2054
+ durationMs,
2055
+ fromCache: false,
2056
+ definitionReuploaded
2057
+ });
2058
+ } catch (error) {
2059
+ const durationMs = performance.now() - startTime;
2060
+ const err = this.normalizeExecutionError(error, inflight);
2061
+ this.writeLastState(item.seq, {
2062
+ error: err,
2063
+ durationMs
2064
+ });
2065
+ this.fail(item, err, durationMs);
2066
+ } finally {
2067
+ this.inFlight.delete(inflight);
2068
+ this.drainNext();
2069
+ this.notify();
2070
+ }
2071
+ }
2072
+ async runExecutor(item, config) {
2073
+ const { definition, dataTree } = item;
2074
+ if (!this.reuseServerDefinitionCache || isDefinitionUrl(definition)) {
2075
+ const { response } = await this.executor(definition, dataTree, null, config);
2076
+ return { response };
2077
+ }
2078
+ const definitionHash = item.hashes.definitionHash;
2079
+ const knownKey = this.serverCacheKeys.get(definitionHash) ?? null;
2080
+ const result = await this.executor(definition, dataTree, knownKey, config);
2081
+ this.serverCacheKeys.delete(definitionHash);
2082
+ if (result.cacheKey) {
2083
+ this.serverCacheKeys.set(definitionHash, result.cacheKey);
2084
+ if (this.serverCacheKeys.size > SERVER_CACHE_KEYS_MAX) this.serverCacheKeys.delete(this.serverCacheKeys.keys().next().value);
2085
+ }
2086
+ return {
2087
+ response: result.response,
2088
+ definitionReuploaded: result.missed
2089
+ };
2090
+ }
2091
+ hashDefinitionMemo(definition) {
2092
+ if (typeof definition !== "string") return hashDefinitionOnce(definition);
2093
+ if (this.lastStringDefinition?.value !== definition) this.lastStringDefinition = {
2094
+ value: definition,
2095
+ hash: hashDefinition(definition)
2096
+ };
2097
+ return this.lastStringDefinition.hash;
2098
+ }
2099
+ drainNext() {
2100
+ if (this.disposed) return;
2101
+ if (this.mode === "latest-wins") {
2102
+ if (this.pendingForLatestWins && this.inFlight.size === 0) {
2103
+ const next = this.pendingForLatestWins;
2104
+ this.pendingForLatestWins = null;
2105
+ this.execute(next);
2106
+ }
2107
+ return;
2108
+ }
2109
+ while (this.fifo.size > 0 && this.inFlight.size < this.maxConcurrent) this.execute(this.fifo.shift());
2110
+ }
2111
+ supersede(item) {
2112
+ const err = new ComputeError("Superseded by newer solve", ErrorCodes.SUPERSEDED, { context: { enqueuedAt: item.ctx.enqueuedAt } });
2113
+ if (this.settleError(item, err)) this.runHook(this.onSuperseded, item.ctx);
2114
+ }
2115
+ makeAbortError(ctx) {
2116
+ return abortedByCaller({ enqueuedAt: ctx.enqueuedAt });
2117
+ }
2118
+ /**
2119
+ * The executor, `supersede`, `cancelAll`, abort and the queue limits can all race to settle one
2120
+ * item, and a promise ignores a second settle silently. The first settle wins; the return value
2121
+ * says whether this call won, so only the winner fires its hook. Every settle path goes through
2122
+ * here or {@link settleSuccess}.
2123
+ */
2124
+ settleError(item, err) {
2125
+ if (!this.claimSettle(item, { error: err })) return false;
2126
+ item.reject(err);
2127
+ return true;
2128
+ }
2129
+ /**
2130
+ * Fires `onSettle` for queued items too, so a consumer counting solves in progress doesn't
2131
+ * leak one. A supersede settles through {@link settleError} and fires `onSuperseded` instead.
2132
+ */
2133
+ fail(item, err, durationMs = 0) {
2134
+ if (this.settleError(item, err)) this.runHook(this.onSettle, item.ctx, {
2135
+ status: "error",
2136
+ error: err,
2137
+ durationMs
2138
+ });
2139
+ }
2140
+ settleSuccess(item, detail) {
2141
+ if (!this.claimSettle(item, { ok: true })) return false;
2142
+ item.resolve(detail);
2143
+ return true;
2144
+ }
2145
+ claimSettle(item, settled) {
2146
+ if (item.settled) return false;
2147
+ item.settled = settled;
2148
+ this.clearQueueWaitTimer(item);
2149
+ if (item.onExternalAbort) item.externalSignal?.removeEventListener("abort", item.onExternalAbort);
2150
+ return true;
2151
+ }
2152
+ normalizeExecutionError(error, item) {
2153
+ if (item.settled && "error" in item.settled) return item.settled.error;
2154
+ if (error instanceof ComputeError) return error;
2155
+ if (error instanceof Error && error.name === "AbortError") return this.makeAbortError(item.ctx);
2156
+ return new ComputeError(error instanceof Error ? error.message : String(error), ErrorCodes.UNKNOWN_ERROR, { originalError: error });
2157
+ }
2158
+ /** Cancel everything — in-flight and pending. */
2159
+ cancelAll() {
2160
+ if (this.pendingForLatestWins) {
2161
+ this.fail(this.pendingForLatestWins, this.makeAbortError(this.pendingForLatestWins.ctx));
2162
+ this.pendingForLatestWins = null;
2163
+ }
2164
+ for (let item = this.fifo.shift(); item; item = this.fifo.shift()) this.fail(item, this.makeAbortError(item.ctx));
2165
+ for (const inflight of this.inFlight) {
2166
+ const durationMs = inflight.ctx.startedAt ? Date.now() - inflight.ctx.startedAt : 0;
2167
+ this.fail(inflight, this.makeAbortError(inflight.ctx), durationMs);
2168
+ inflight.controller.abort();
2169
+ }
2170
+ this.notify();
2171
+ }
2172
+ clearCache() {
2173
+ this.cache.clear();
2174
+ }
2175
+ /**
2176
+ * Observability snapshot of the solve cache: current size plus lifetime
2177
+ * hit/miss/eviction counters.
2178
+ *
2179
+ * `hits`/`misses` count cache CONSULTATIONS, so `hits / (hits + misses)` is the
2180
+ * hit rate. A TTL-expired entry counts as a miss (the solve runs either way).
2181
+ * `evictions` counts only entries dropped under size/byte pressure — not
2182
+ * replace-in-place writes or TTL expiry, which are not capacity signals.
2183
+ * Counters are cumulative and survive `clearCache()`.
2184
+ */
2185
+ cacheStats() {
2186
+ return this.cache.stats();
2187
+ }
2188
+ dispose() {
2189
+ if (this.disposed) return;
2190
+ this.disposed = true;
2191
+ this.cancelAll();
2192
+ this.subscribers.clear();
2193
+ this.clearCache();
2194
+ }
2195
+ runHook(hook, ...args) {
2196
+ if (!hook) return;
2197
+ try {
2198
+ hook(...args);
2199
+ } catch (err) {
2200
+ getLogger().error("[SolveScheduler] hook threw:", err);
2201
+ }
2202
+ }
2203
+ };
2204
+ // src/grasshopper/client/grasshopper-client.ts
2205
+ function describeDefinition(definition) {
2206
+ if (isDefinitionRef(definition)) return `ref:${definition.key}`;
2207
+ if (typeof definition !== "string" || definition.length >= 200) return "...content...";
2208
+ return isDefinitionUrl(definition) ? redactUrl(definition) : definition;
2209
+ }
2210
+ /** Identifiers only: the error's `context` holds outputs and its message the server's words. */
2211
+ function describeFailure(error) {
2212
+ if (!(error instanceof ComputeError)) return `Compute failed: ${error instanceof Error ? error.name : typeof error}`;
2213
+ const status = error.statusCode === void 0 ? "" : ` HTTP ${error.statusCode}`;
2214
+ const requestId = error.context?.requestId;
2215
+ return `Compute failed: ${error.code}${status}${requestId ? ` [${requestId}]` : ""}`;
2216
+ }
2217
+ /**
2218
+ * Stands in for the input tree on errors: the tree can hold multi-MB geometry, and every logger
2219
+ * or telemetry sink that keeps the error would pin it. Never throws on a malformed tree.
2220
+ */
2221
+ function summarizeDataTree(dataTree) {
2222
+ if (!Array.isArray(dataTree)) return [];
2223
+ return dataTree.map((tree) => {
2224
+ let items = 0;
2225
+ let bytes = 0;
2226
+ const innerTree = readField(tree, "innerTree");
2227
+ if (innerTree && typeof innerTree === "object") for (const branch of Object.values(innerTree)) {
2228
+ if (!Array.isArray(branch)) continue;
2229
+ items += branch.length;
2230
+ for (const item of branch) {
2231
+ const data = item?.data;
2232
+ if (typeof data === "string") bytes += data.length;
2233
+ }
2234
+ }
2235
+ return {
2236
+ param: readField(tree, "paramName") ?? "<unnamed>",
2237
+ items,
2238
+ bytes
2239
+ };
2240
+ });
2241
+ }
2242
+ /**
2243
+ * The high-level API for solving Grasshopper definitions on a Rhino Compute server.
2244
+ *
2245
+ * In a browser, the server URL and API key are exposed to users: run it server-side, or proxy
2246
+ * requests through your own backend.
2247
+ *
2248
+ * @example
2249
+ * ```typescript
2250
+ * const client = await GrasshopperClient.create({
2251
+ * serverUrl: 'http://localhost:6500',
2252
+ * apiKey: 'your-api-key'
2253
+ * });
2254
+ *
2255
+ * try {
2256
+ * const io = await client.getIO(definitionUrl);
2257
+ * const result = await client.solve(definitionUrl, TreeBuilder.fromInputParams(io.inputs));
2258
+ * } finally {
2259
+ * await client.dispose(); // Clean up resources
2260
+ * }
2261
+ * ```
2262
+ */
2263
+ var GrasshopperClient = class GrasshopperClient {
2264
+ config;
2265
+ serverStats;
2266
+ schedulers = /* @__PURE__ */ new Set();
2267
+ disposed = false;
2268
+ /**
2269
+ * Per-probe timeout for the `create()` liveness gate: a healthy `GET /` answers in milliseconds,
2270
+ * so this only bounds a silent server, and every extra second is paid once per retry.
2271
+ */
2272
+ static CREATE_PROBE_TIMEOUT_MS = 2e3;
2273
+ constructor(config) {
2274
+ this.config = {
2275
+ ...config,
2276
+ serverUrl: validateServerUrl(config.serverUrl),
2277
+ debug: config.debug ?? false
2278
+ };
2279
+ this.serverStats = new ComputeServerStats(this.config.serverUrl, this.config);
2280
+ }
2281
+ /**
2282
+ * Creates a client once a liveness probe (`GET /`) says the server is up. A cold server can read
2283
+ * as offline, so the probe retries with a short backoff, stopping early when
2284
+ * {@link classifyProbeFailure} says waiting can't change the answer. `config.signal` cancels the
2285
+ * ladder.
2286
+ *
2287
+ * @throws {ComputeError} `NETWORK_ERROR` if the server stays unreachable across all attempts.
2288
+ * `context.probeVerdict` carries the verdict, so a caller can render a specific cause.
2289
+ * @throws {ComputeError} `ABORTED` if `config.signal` fires first.
2290
+ * @throws {ComputeError} `INVALID_CONFIG` if the configuration is invalid.
2291
+ */
2292
+ static async create(config, options = {}) {
2293
+ const client = new GrasshopperClient(config);
2294
+ const { signal } = config;
2295
+ const attempts = Math.max(1, (options.probe?.attempts ?? 2) + 1);
2296
+ const baseDelayMs = options.probe?.baseDelayMs ?? 250;
2297
+ const maxDelayMs = options.probe?.maxDelayMs ?? 1e3;
2298
+ let lastProbe;
2299
+ let failure = null;
2300
+ let used = 0;
2301
+ for (let attempt = 0; attempt < attempts && !signal?.aborted; attempt++) {
2302
+ used = attempt + 1;
2303
+ lastProbe = await client.serverStats.probeServer(GrasshopperClient.CREATE_PROBE_TIMEOUT_MS, signal);
2304
+ if (lastProbe.online) return client;
2305
+ failure = classifyProbeFailure(lastProbe);
2306
+ if (failure && !failure.retryable) break;
2307
+ if (attempt < attempts - 1) {
2308
+ const delay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
2309
+ await sleep(delay, signal).catch(() => {});
2310
+ }
2311
+ }
2312
+ await client.dispose();
2313
+ if (signal?.aborted) throw abortedByCaller({
2314
+ serverUrl: client.config.serverUrl,
2315
+ attempts: used
2316
+ });
2317
+ const status = lastProbe?.status;
2318
+ const message = failure ? `Rhino Compute server is not available. ${failure.summary}` : "Rhino Compute server is not online";
2319
+ throw new ComputeError(message, ErrorCodes.NETWORK_ERROR, {
2320
+ ...status !== void 0 && { statusCode: status },
2321
+ context: {
2322
+ serverUrl: client.config.serverUrl,
2323
+ attempts: used,
2324
+ maxAttempts: attempts,
2325
+ ...failure && {
2326
+ probeVerdict: failure.verdict,
2327
+ probeRetryable: failure.retryable
2328
+ },
2329
+ ...status !== void 0 && { lastProbeStatus: status },
2330
+ ...lastProbe?.error !== void 0 && { lastProbeError: lastProbe.error }
2331
+ }
2332
+ });
2333
+ }
2334
+ /**
2335
+ * Get input/output parameters of a Grasshopper definition.
2336
+ */
2337
+ async getIO(definition) {
2338
+ this.ensureNotDisposed();
2339
+ return fetchParsedDefinitionIO(definition, this.config);
2340
+ }
2341
+ async getRawIO(definition) {
2342
+ this.ensureNotDisposed();
2343
+ return fetchDefinitionIO(definition, this.config);
2344
+ }
2345
+ /**
2346
+ * Run a compute job with a Grasshopper definition.
2347
+ *
2348
+ * @throws {ComputeError} with code INVALID_INPUT if definition is empty
2349
+ * @throws {ComputeError} with code NETWORK_ERROR if server is offline
2350
+ * @throws {ComputeError} with code COMPUTATION_ERROR if computation fails.
2351
+ * On a partial-success response (some outputs computed, some errored) the
2352
+ * error's `context.values` carries the outputs that did compute — pass
2353
+ * `{ values } as GrasshopperComputeResponse` to the response processors to
2354
+ * render them. `context.inputSummary` describes the inputs (param names,
2355
+ * item counts, byte sizes) without pinning the full data tree.
2356
+ */
2357
+ async solve(definition, dataTree, options) {
2358
+ this.ensureNotDisposed();
2359
+ const describeCall = () => ({
2360
+ definition: describeDefinition(definition),
2361
+ inputSummary: summarizeDataTree(dataTree)
2362
+ });
2363
+ try {
2364
+ if (typeof definition === "string" && !definition.trim()) throw new ComputeError("Definition URL/content is required", ErrorCodes.INVALID_INPUT, { context: { receivedUrl: definition } });
2365
+ else if (definition instanceof Uint8Array && definition.length === 0) throw new ComputeError("Definition content is empty", ErrorCodes.INVALID_INPUT);
2366
+ else if (isDefinitionRef(definition) && !definition.key.trim()) throw new ComputeError("DefinitionRef key is empty", ErrorCodes.INVALID_INPUT);
2367
+ const result = await solveGrasshopperDefinition(dataTree, definition, {
2368
+ ...this.config,
2369
+ ...options?.signal !== void 0 && { signal: options.signal },
2370
+ ...options?.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
2371
+ ...options?.retry !== void 0 && { retry: options.retry },
2372
+ ...options?.selvaevents !== void 0 && { selvaevents: options.selvaevents }
2373
+ });
2374
+ const solveErrors = readField(result, "errors");
2375
+ if (Array.isArray(solveErrors) && solveErrors.length > 0) throw new ComputeError(solveErrors.map(String).join("; ") || "Computation failed", ErrorCodes.COMPUTATION_ERROR, { context: {
2376
+ ...describeCall(),
2377
+ errors: solveErrors,
2378
+ warnings: readField(result, "warnings"),
2379
+ values: result.values
2380
+ } });
2381
+ return result;
2382
+ } catch (error) {
2383
+ if (this.config.debug) getLogger().error(describeFailure(error));
2384
+ if (error instanceof ComputeError) throw error;
2385
+ throw new ComputeError(error instanceof Error ? error.message : String(error), ErrorCodes.COMPUTATION_ERROR, {
2386
+ context: describeCall(),
2387
+ originalError: error
2388
+ });
2389
+ }
2390
+ }
2391
+ /**
2392
+ * Create a scheduler bound to this client. Use a scheduler for any UI surface
2393
+ * that fires solves frequently (sliders, live editors) or that needs cancel
2394
+ * semantics, response caching, or state observability.
2395
+ *
2396
+ * Multiple schedulers can be created from a single client — typically one per
2397
+ * UI surface so their queues stay independent.
2398
+ *
2399
+ * @example
2400
+ * ```ts
2401
+ * const sliderScheduler = client.createScheduler({ mode: 'latest-wins' });
2402
+ * const submitScheduler = client.createScheduler({ mode: 'queue', timeoutMs: 0 });
2403
+ * ```
2404
+ */
2405
+ createScheduler(options) {
2406
+ this.ensureNotDisposed();
2407
+ const scheduler = new SolveScheduler((definition, dataTree, cacheKey, config) => solveWithServerCacheKey(dataTree, cacheKey, definition, config), this.config, options);
2408
+ this.schedulers.add(scheduler);
2409
+ return scheduler;
2410
+ }
2411
+ /** Disposes of client resources, including every scheduler it created. */
2412
+ async dispose() {
2413
+ if (this.disposed) return;
2414
+ this.disposed = true;
2415
+ for (const scheduler of this.schedulers) scheduler.dispose();
2416
+ this.schedulers.clear();
2417
+ await this.serverStats.dispose();
2418
+ }
2419
+ ensureNotDisposed() {
2420
+ if (this.disposed) throw new ComputeError("GrasshopperClient has been disposed and cannot be used", ErrorCodes.INVALID_STATE);
2421
+ }
2422
+ };
2423
+ // src/grasshopper/io/output/rhino-objects.ts
2424
+ /** Type guard for the {@link RhinoDecodeError} sentinel. */
2425
+ function isRhinoDecodeError(value) {
2426
+ return typeof value === "object" && value !== null && value.__decodeError === true;
2427
+ }
2428
+ function makeDecodeError(rhinoType, raw) {
2429
+ return {
2430
+ __decodeError: true,
2431
+ type: rhinoType,
2432
+ raw
2433
+ };
2434
+ }
2435
+ /**
2436
+ * Free every rhino3dm WASM object reachable from `value`.
2437
+ *
2438
+ * rhino3dm objects are emscripten bindings — JS GC never reclaims their WASM
2439
+ * heap allocation, so everything decoded from a solve response (`getValues`,
2440
+ * `getValue`) must be deleted explicitly or the heap
2441
+ * grows monotonically across solves (e.g. a UI decoding per slider tick).
2442
+ *
2443
+ * Walks arrays and plain objects recursively; anything exposing a `delete()`
2444
+ * method is treated as a WASM binding and freed (skipped if already deleted).
2445
+ * Safe to call more than once and on values containing no WASM objects.
2446
+ */
2447
+ function disposeRhinoObjects(value) {
2448
+ const seen = /* @__PURE__ */ new WeakSet();
2449
+ const walk = (v) => {
2450
+ if (!v || typeof v !== "object") return;
2451
+ if (seen.has(v)) return;
2452
+ seen.add(v);
2453
+ const del = v.delete;
2454
+ if (typeof del === "function") {
2455
+ const isDeleted = v.isDeleted;
2456
+ if (typeof isDeleted !== "function" || !isDeleted.call(v)) del.call(v);
2457
+ return;
2458
+ }
2459
+ if (Array.isArray(v)) {
2460
+ for (const item of v) walk(item);
2461
+ return;
2462
+ }
2463
+ const proto = Object.getPrototypeOf(v);
2464
+ if (proto === Object.prototype || proto === null) for (const item of Object.values(v)) walk(item);
2465
+ };
2466
+ walk(value);
2467
+ }
2468
+ // src/grasshopper/io/output/rhino-decoder.ts
2469
+ const decoders = /* @__PURE__ */ new Map([["Rhino.Geometry.Point3d", (rhino, data) => {
2470
+ const d = data;
2471
+ if (!d || typeof d.X !== "number") return null;
2472
+ return new rhino.Point([
2473
+ d.X,
2474
+ d.Y,
2475
+ d.Z
2476
+ ]);
2477
+ }], ["Rhino.Geometry.Line", (rhino, data) => {
2478
+ const d = data;
2479
+ if (!d || !d.From || !d.To) return null;
2480
+ return new rhino.Line([
2481
+ d.From.X,
2482
+ d.From.Y,
2483
+ d.From.Z
2484
+ ], [
2485
+ d.To.X,
2486
+ d.To.Y,
2487
+ d.To.Z
2488
+ ]);
2489
+ }]]);
2490
+ /**
2491
+ * Whether the parsed item looks like a rhino3dm serialization envelope
2492
+ * (`{version, archive3dm, opennurbs, data: "<base64>"}`). `CommonObject.decode` expects the WHOLE
2493
+ * envelope — unwrapping `.data` first hands it the bare base64 string, which throws or decodes to
2494
+ * garbage.
2495
+ */
2496
+ function isDecodableEnvelope(parsedData) {
2497
+ return Boolean(parsedData && typeof parsedData === "object" && typeof parsedData.data === "string");
2498
+ }
2499
+ /**
2500
+ * Decode one typed payload from a solve response into a rhino3dm object.
2501
+ *
2502
+ * Every call yields exactly one of three outcomes:
2503
+ *
2504
+ * 1. **Decoded value** — a registered decoder recognized the shape, or the
2505
+ * payload was a rhino3dm serialization envelope and `CommonObject.decode`
2506
+ * succeeded.
2507
+ * 2. **Raw passthrough** (`=== parsedData`) — no decode path *applied*: no
2508
+ * decoder matched the type (or the matched decoder returned `null`, meaning
2509
+ * "not my shape") and the payload is not a decodable envelope. Not an error;
2510
+ * the caller keeps the parsed JSON as-is.
2511
+ * 3. **{@link RhinoDecodeError} sentinel** — a decode was *attempted and threw*
2512
+ * (a registered decoder threw and no envelope fallback could recover, or
2513
+ * `CommonObject.decode` threw). Detect with {@link isRhinoDecodeError}.
2514
+ *
2515
+ * A decoder returning `null` is a soft miss, never an error: it falls through
2516
+ * to the envelope fallback.
2517
+ */
2518
+ function decodeRhinoGeometry(parsedData, rhinoType, rhino) {
2519
+ const decoder = decoders.get(rhinoType);
2520
+ let decoderThrew = false;
2521
+ if (decoder) try {
2522
+ const decoded = decoder(rhino, parsedData);
2523
+ if (decoded != null) return decoded;
2524
+ } catch (error) {
2525
+ getLogger().warn(`Failed to decode Rhino type ${rhinoType}:`, error);
2526
+ decoderThrew = true;
2527
+ }
2528
+ try {
2529
+ if (isDecodableEnvelope(parsedData)) return rhino.CommonObject.decode(parsedData);
2530
+ } catch (error) {
2531
+ getLogger().warn(`Failed to decode ${rhinoType} with CommonObject:`, error);
2532
+ return makeDecodeError(rhinoType, parsedData);
2533
+ }
2534
+ return decoderThrew ? makeDecodeError(rhinoType, parsedData) : parsedData;
2535
+ }
2536
+ // src/grasshopper/io/output/response-processors.ts
2537
+ const RHINO_GEOMETRY_PREFIX = "Rhino.Geometry.";
2538
+ const FILE_DATA_TYPE = "FileData";
2539
+ /** WebDisplay items are skipped entirely: kept, they would write `null` into the result. */
2540
+ function skipItem(type, stringOnly) {
2541
+ return type.includes("WebDisplay") || stringOnly && type !== "System.String";
2542
+ }
2543
+ const looksStructured = (text) => text.startsWith("{") || text.startsWith("[");
2544
+ function tryDecodeJSON(value) {
2545
+ if (typeof value !== "string") return value;
2546
+ const trimmed = value.trim();
2547
+ if (!looksStructured(trimmed) && !trimmed.startsWith("\"")) return value;
2548
+ const first = tryParseJson(trimmed) ?? value;
2549
+ return typeof first === "string" && looksStructured(first.trim()) ? tryParseJson(first) ?? first : first;
2550
+ }
2551
+ function decodeBySystemType(raw, type, rhino) {
2552
+ if (type === "System.String") return typeof raw === "string" ? raw.replace(/^"(.*)"$/, "$1") : raw;
2553
+ if (INTEGER_ITEM_TYPES.has(type)) {
2554
+ const value = Number.parseInt(String(raw), 10);
2555
+ return Number.isNaN(value) || Number.isSafeInteger(value) ? value : raw;
2556
+ }
2557
+ if (NUMERIC_ITEM_TYPES.has(type)) return Number.parseFloat(String(raw));
2558
+ if (type === "System.Boolean") return String(raw).toLowerCase() === "true";
2559
+ if (rhino && type.startsWith(RHINO_GEOMETRY_PREFIX)) return decodeRhinoGeometry(raw, type, rhino);
2560
+ return raw;
2561
+ }
2562
+ function extractItemValue(item, type, parseValues, rhino) {
2563
+ if (typeof item.data !== "string") return item.data;
2564
+ const raw = parseValues ? tryDecodeJSON(item.data) : item.data;
2565
+ if (parseValues && type.includes(FILE_DATA_TYPE)) return asFileData(raw) ?? raw;
2566
+ return decodeBySystemType(raw, type, rhino);
2567
+ }
2568
+ /**
2569
+ * A parsed `FileData` blob in the camelCase shape {@link FileData} documents, or
2570
+ * `null` when a required field is missing. Fields are read with `readField`,
2571
+ * since their casing varies by server branch.
2572
+ */
2573
+ function asFileData(value) {
2574
+ if (!value || typeof value !== "object") return null;
2575
+ const fileName = readField(value, "fileName");
2576
+ const fileType = readField(value, "fileType");
2577
+ const subFolder = readField(value, "subFolder");
2578
+ const flag = readField(value, "isBase64Encoded");
2579
+ if (typeof fileName !== "string" || typeof fileType !== "string") return null;
2580
+ if (typeof subFolder !== "string") return null;
2581
+ if (!hasField(value, "data")) return null;
2582
+ if (typeof flag !== "boolean" && typeof flag !== "string") return null;
2583
+ const metadata = readField(value, "metadata");
2584
+ return {
2585
+ fileName,
2586
+ fileType,
2587
+ data: readField(value, "data"),
2588
+ isBase64Encoded: isBase64Flag(flag),
2589
+ subFolder,
2590
+ ...metadata ? { metadata } : {}
2591
+ };
2592
+ }
2593
+ /** Partial-success and warnings-only responses can arrive with no `values` at all. */
2594
+ function getResponseParams(response) {
2595
+ const values = readField(response, "values");
2596
+ return Array.isArray(values) ? values : [];
2597
+ }
2598
+ /** A param's item type, tolerating items whose `type` is missing/non-string. */
2599
+ function itemType(item) {
2600
+ return typeof item.type === "string" ? item.type : "";
2601
+ }
2602
+ /** Tolerates a missing tree (warnings-only partial successes) and skips non-object items. */
2603
+ function forEachTreeItem(tree, handler) {
2604
+ if (!tree || typeof tree !== "object") return;
2605
+ for (const list of Object.values(tree)) if (Array.isArray(list)) {
2606
+ for (const item of list) if (item && typeof item === "object") handler(item);
2607
+ }
2608
+ }
2609
+ /**
2610
+ * Read all output values from a Grasshopper Compute response, keyed by parameter
2611
+ * name (or ID when `byId`). Duplicate keys aggregate into an array.
2612
+ *
2613
+ * Geometry decoded via `rhino` lives on the WASM heap: free it with `dispose()`.
2614
+ *
2615
+ * @param options.parseValues - Parse complex data types into JS objects (default true).
2616
+ * @param options.rhino - Rhino3dm instance for geometry decoding.
2617
+ * @param options.stringOnly - Keep only string-typed items.
2618
+ */
2619
+ function getValues(response, byId = false, options = {}) {
2620
+ const { parseValues = true, rhino, stringOnly = false } = options;
2621
+ const result = {};
2622
+ const aggregated = /* @__PURE__ */ new Set();
2623
+ for (const param of getResponseParams(response)) {
2624
+ const paramName = readField(param, "paramName");
2625
+ forEachTreeItem(readField(param, "innerTree"), (item) => {
2626
+ const type = itemType(item);
2627
+ if (skipItem(type, stringOnly)) return;
2628
+ const key = byId ? item.id : paramName;
2629
+ if (!key) return;
2630
+ const value = extractItemValue(item, type, parseValues, rhino);
2631
+ if (!Object.hasOwn(result, key)) setOwnField(result, key, value);
2632
+ else if (aggregated.has(key)) result[key].push(value);
2633
+ else {
2634
+ result[key] = [result[key], value];
2635
+ aggregated.add(key);
2636
+ }
2637
+ });
2638
+ }
2639
+ return {
2640
+ values: result,
2641
+ dispose: () => disposeRhinoObjects(result)
2642
+ };
2643
+ }
2644
+ /** Decode every file-data item in a response into {@link FileData} objects. */
2645
+ function extractFileData(response) {
2646
+ const output = [];
2647
+ for (const param of getResponseParams(response)) forEachTreeItem(readField(param, "innerTree"), (item) => {
2648
+ if (!itemType(item).includes(FILE_DATA_TYPE)) return;
2649
+ const file = asFileData(tryDecodeJSON(item.data));
2650
+ if (file) output.push(file);
2651
+ });
2652
+ return output;
2653
+ }
2654
+ /**
2655
+ * Read one parameter's value(s) from a response — `byName` matches a `ParamName`,
2656
+ * `byId` matches an item ID. Returns `undefined` if absent, a single value for one
2657
+ * match, or an array for several.
2658
+ *
2659
+ * Geometry decoded via `rhino` lives on the WASM heap: free it with
2660
+ * {@link disposeRhinoObjects} once consumed.
2661
+ *
2662
+ * @param parseOptions.parseValues - Parse raw data into formatted values (default true).
2663
+ * @param parseOptions.rhino - Rhino3dm instance for geometry decoding.
2664
+ * @param parseOptions.stringOnly - Keep only string-typed items.
2665
+ */
2666
+ function getValue(response, options, parseOptions = {}) {
2667
+ const { parseValues = true, rhino, stringOnly = false } = parseOptions;
2668
+ for (const param of getResponseParams(response)) {
2669
+ const tree = readField(param, "innerTree");
2670
+ let matched = false;
2671
+ if ("byName" in options) {
2672
+ if (readField(param, "paramName") !== options.byName) continue;
2673
+ matched = true;
2674
+ }
2675
+ const collected = [];
2676
+ forEachTreeItem(tree, (item) => {
2677
+ if ("byId" in options) {
2678
+ if (item.id !== options.byId) return;
2679
+ matched = true;
2680
+ }
2681
+ const type = itemType(item);
2682
+ if (skipItem(type, stringOnly)) return;
2683
+ collected.push(extractItemValue(item, type, parseValues, rhino));
2684
+ });
2685
+ if (matched) {
2686
+ if (collected.length === 0) return void 0;
2687
+ if (collected.length === 1) return collected[0];
2688
+ return collected;
2689
+ }
2690
+ }
2691
+ }
2692
+ // src/grasshopper/client/grasshopper-response-processor.ts
2693
+ /** Reads parsed values and produced files out of one compute response. */
2694
+ var GrasshopperResponseProcessor = class {
2695
+ response;
2696
+ constructor(response) {
2697
+ this.response = response;
2698
+ }
2699
+ /**
2700
+ * Extract all values in the response.
2701
+ *
2702
+ * @typeParam T - Expected structure of the return value. Defaults to a simple key/value map. (later cast as needed)
2703
+ * @param byId - Key by parameter ID instead of name. Requires the VektorNode rhino.compute branch.
2704
+ * @param options - Controls parsing behavior such as Rhino geometry decoding.
2705
+ * @returns Parsed Grasshopper output values.
2706
+ *
2707
+ * @example
2708
+ * ```ts
2709
+ * const processor = new GrasshopperResponseProcessor(response);
2710
+ * const { values } = processor.getValues();
2711
+ * ```
2712
+ *
2713
+ */
2714
+ getValues(byId = false, options = {}) {
2715
+ return getValues(this.response, byId, options);
2716
+ }
2717
+ /**
2718
+ * Retrieve a specific value by parameter name or ID.
2719
+ *
2720
+ * @param selector - `{ byName }` for the human-readable name, `{ byId }` for the parameter GUID.
2721
+ * @param options - Parsing configuration (e.g. disable parsing or enable Rhino).
2722
+ * @returns Single parsed value, array of values, or undefined if the parameter is absent.
2723
+ *
2724
+ * `byId` requires the VektorNode rhino.compute branch.
2725
+ *
2726
+ * @example
2727
+ * ```ts
2728
+ * const schema = processor.getValue({ byName: 'Schema' });
2729
+ * const output = processor.getValue({ byId: 'a4be1c1e-23f9-4c27-b942-7f3bb2c45c6f' });
2730
+ * ```
2731
+ */
2732
+ getValue(selector, options) {
2733
+ return getValue(this.response, selector, options);
2734
+ }
2735
+ /**
2736
+ * Download all files generated by Grasshopper, optionally including
2737
+ * additional user-provided files.
2738
+ *
2739
+ * @param folderName - Name of the zip archive, without the extension.
2740
+ * @param additionalFiles - Extra files to package (single file, array, or null).
2741
+ *
2742
+ * @example
2743
+ * ```ts
2744
+ * await processor.getAndDownloadFiles('gh-output');
2745
+ * ```
2746
+ *
2747
+ * @example
2748
+ * ```ts
2749
+ * const extra = { fileName: 'notes.txt', filePath: 'https://example.com/notes.txt' };
2750
+ * await processor.getAndDownloadFiles('project', extra);
2751
+ * ```
2752
+ */
2753
+ async getAndDownloadFiles(folderName, additionalFiles) {
2754
+ await downloadFileData(extractFileData(this.response), folderName, additionalFiles);
2755
+ }
2756
+ };
2757
+ // src/grasshopper/io/schema-endpoint.ts
2758
+ /**
2759
+ * Read compute's `/grasshopper/schema` body: `[{ FileName, Schemas }]` per
2760
+ * uploaded file, or a bare object for a single file.
2761
+ *
2762
+ * Use this rather than unwrapping the body by hand: the wrapper keys' casing
2763
+ * varies by server branch, and a fixed-key read yields "no schemas" with a 200.
2764
+ * Only the two wrapper keys are read; schema contents pass through untouched.
2765
+ *
2766
+ * @typeParam TSchema - Your schema type (e.g. `UISchema`). Not inspected.
2767
+ */
2768
+ function readSchemaResults(raw) {
2769
+ return (Array.isArray(raw) ? raw : [raw]).map((entry) => ({
2770
+ schemas: readField(entry, "schemas"),
2771
+ error: readField(entry, "error")
2772
+ }));
2773
+ }
2774
+ // src/grasshopper/io/normalize-ui-schema.ts
2775
+ /**
2776
+ * Canonicalize a `/grasshopper/schema` body's key casing.
2777
+ *
2778
+ * Compute serializes the plugin's `UISchema`, whose camelCase wire names live in
2779
+ * Newtonsoft `[JsonProperty]` attributes. `Selva.gha` ILRepack-merges Newtonsoft,
2780
+ * so those attributes are `Selva!Newtonsoft.Json.JsonPropertyAttribute`. When
2781
+ * compute's OWN Newtonsoft serializes, it doesn't recognize them, reads no
2782
+ * attributes, and emits raw CLR names (`Inputs`, `SchemaVersion`). Nothing throws:
2783
+ * `schema.inputs` reads `undefined`, and so does the `schemaVersion` gate.
2784
+ *
2785
+ * A casing rule, not a key allowlist: the wire names are the CLR names run
2786
+ * through Newtonsoft's camelCase strategy, so reproducing it covers every key
2787
+ * without drifting each time `UISchema` gains a field.
2788
+ */
2789
+ /** Maps keyed by the definition's author (dropdown labels): copied verbatim. */
2790
+ const USER_AUTHORED_MAPS = /* @__PURE__ */ new Set([
2791
+ "options",
2792
+ "defaultOptions",
2793
+ "values"
2794
+ ]);
2795
+ /** Maps with author or hash keys whose entries are schema objects: keys verbatim, entries normalized. */
2796
+ const USER_KEYED_OBJECT_MAPS = /* @__PURE__ */ new Set(["optionMedia", "assets"]);
2797
+ const isUpper = (ch) => ch >= "A" && ch <= "Z";
2798
+ /**
2799
+ * Newtonsoft's `CamelCaseNamingStrategy`: a leading run of capitals is lowercased
2800
+ * whole, except the last one when a lowercase word follows. `UV` → `uv`,
2801
+ * `UVMapping` → `uvMapping` (a first-character rule gives `uVMapping`).
2802
+ */
2803
+ function toCamelCase(key) {
2804
+ if (key.length === 0 || !isUpper(key[0])) return key;
2805
+ let run = 0;
2806
+ while (run < key.length && isUpper(key[run])) run++;
2807
+ if (run > 1 && run < key.length) run--;
2808
+ return key.slice(0, run).toLowerCase() + key.slice(run);
2809
+ }
2810
+ /**
2811
+ * Returns a schema whose structural keys are camelCase, whichever Newtonsoft
2812
+ * serialized it. Always a fresh object the caller may mutate; non-objects are
2813
+ * returned as-is.
2814
+ */
2815
+ function normalizeUISchemaCasing(raw) {
2816
+ return normalizeValue(raw);
2817
+ }
2818
+ function normalizeValue(value) {
2819
+ if (Array.isArray(value)) return value.map(normalizeValue);
2820
+ if (value === null || typeof value !== "object") return value;
2821
+ const out = {};
2822
+ for (const [key, child] of Object.entries(value)) {
2823
+ const canonical = toCamelCase(key);
2824
+ setOwnField(out, canonical, USER_AUTHORED_MAPS.has(canonical) ? child : USER_KEYED_OBJECT_MAPS.has(canonical) ? normalizeEntries(child) : normalizeValue(child));
2825
+ }
2826
+ return out;
2827
+ }
2828
+ function normalizeEntries(value) {
2829
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
2830
+ const out = {};
2831
+ for (const [key, child] of Object.entries(value)) setOwnField(out, key, normalizeValue(child));
2832
+ return out;
2833
+ }
2834
+ // src/grasshopper/data-tree/data-tree.ts
2835
+ /**
2836
+ * Builds the Grasshopper data trees a solve takes as input.
2837
+ *
2838
+ * @example
2839
+ * ```ts
2840
+ * const tree = new TreeBuilder('MyParam')
2841
+ * .append([0], [1, 2, 3])
2842
+ * .append([1], [4, 5])
2843
+ * .toComputeFormat();
2844
+ * ```
2845
+ */
2846
+ var TreeBuilder = class TreeBuilder {
2847
+ innerTree;
2848
+ paramName;
2849
+ constructor(paramName) {
2850
+ this.paramName = paramName;
2851
+ this.innerTree = {};
2852
+ }
2853
+ /** Append values to the branch at `path` (`[0, 1]` is `{0;1}`). A `null` item is skipped. */
2854
+ append(path, items) {
2855
+ const branch = this.innerTree[TreeBuilder.formatPathString(path)] ??= [];
2856
+ for (const item of items) if (item !== null) branch.push({ data: TreeBuilder.serializeValue(item) });
2857
+ return this;
2858
+ }
2859
+ /** Replace the tree with a branch-path map like `{ '{0;1}': [1, 2] }`. */
2860
+ fromDataTreeDefault(treeData) {
2861
+ this.innerTree = {};
2862
+ for (const [pathStr, items] of Object.entries(treeData)) {
2863
+ if (!Array.isArray(items)) continue;
2864
+ const path = TreeBuilder.parsePathString(pathStr);
2865
+ this.append(path, items);
2866
+ }
2867
+ return this;
2868
+ }
2869
+ /** Append a value, or each value of an array, to branch `{0}`. */
2870
+ appendFlat(values) {
2871
+ const items = Array.isArray(values) ? values : [values];
2872
+ return this.append([0], items);
2873
+ }
2874
+ /** The tree in the shape a solve request sends. */
2875
+ toComputeFormat() {
2876
+ return {
2877
+ ParamName: this.paramName,
2878
+ InnerTree: this.innerTree
2879
+ };
2880
+ }
2881
+ /**
2882
+ * One tree per input that has a default, keyed by its nickname. Defaults are coerced and
2883
+ * clamped by the same rules as {@link TreeBuilder.fromValues}.
2884
+ */
2885
+ static fromInputParams(inputs) {
2886
+ return TreeBuilder.fromValues(inputs.map((input) => ({
2887
+ paramName: input.nickname || "unnamed",
2888
+ paramType: input.paramType,
2889
+ value: input.default,
2890
+ ..."minimum" in input && {
2891
+ minimum: input.minimum,
2892
+ maximum: input.maximum
2893
+ }
2894
+ })));
2895
+ }
2896
+ /**
2897
+ * One tree per entry, coercing UI values the way Grasshopper reads them. Number and Integer
2898
+ * items go through the numeric coercion (Integer rounds) and are clamped to
2899
+ * `minimum`/`maximum`; Boolean items also accept `'true'`/`'false'`; every other type passes
2900
+ * through. An item that fails coercion is dropped with a warning. An entry left with no items
2901
+ * is omitted, so Grasshopper uses its own value; `''` is an item, not an empty value.
2902
+ */
2903
+ static fromValues(entries) {
2904
+ const trees = [];
2905
+ for (const entry of entries) {
2906
+ const tree = new TreeBuilder(entry.paramName);
2907
+ const coerce = TreeBuilder.itemCoercer(entry);
2908
+ for (const [path, items] of TreeBuilder.branchesOf(entry.value)) {
2909
+ const kept = [];
2910
+ for (const item of items) {
2911
+ if (item === null || item === void 0) continue;
2912
+ const coerced = coerce(item);
2913
+ if (coerced === null) getLogger().warn(`${entry.paramName}: dropped an item that is not a valid ${entry.paramType}`);
2914
+ else kept.push(coerced);
2915
+ }
2916
+ if (kept.length > 0) tree.append(path, kept);
2917
+ }
2918
+ if (Object.keys(tree.innerTree).length > 0) trees.push(tree.toComputeFormat());
2919
+ }
2920
+ return trees;
2921
+ }
2922
+ static branchesOf(value) {
2923
+ if (!TreeBuilder.hasValidValue(value)) return [];
2924
+ if (isDataTreeDefault(value)) return Object.entries(value).map(([path, items]) => [TreeBuilder.parsePathString(path), items]);
2925
+ return [[[0], Array.isArray(value) ? value : [value]]];
2926
+ }
2927
+ /** `null` means the item was rejected. */
2928
+ static itemCoercer(entry) {
2929
+ const { paramName, minimum, maximum } = entry;
2930
+ const paramType = entry.paramType.toLowerCase();
2931
+ if (paramType === "number" || paramType === "integer") return (item) => {
2932
+ const n = numericTransformer(item);
2933
+ if (n === null) return null;
2934
+ const value = paramType === "integer" ? Math.round(n) : n;
2935
+ return TreeBuilder.clampValue(value, minimum, maximum, paramName);
2936
+ };
2937
+ if (paramType === "boolean") return booleanTransformer;
2938
+ return (item) => item;
2939
+ }
2940
+ /**
2941
+ * Set or add `paramName`'s tree to `newValue`: a scalar, an array (branch `{0}`), or a
2942
+ * branch-path map. Copy-on-write: returns a new array and leaves the caller's untouched, so
2943
+ * assign the result.
2944
+ */
2945
+ static replaceTreeValue(trees, paramName, newValue) {
2946
+ const compiled = TreeBuilder.buildFromValue(paramName, newValue).toComputeFormat();
2947
+ const result = trees.slice();
2948
+ const idx = result.findIndex((t) => TreeBuilder.paramNameOf(t) === paramName);
2949
+ if (idx === -1) result.push(compiled);
2950
+ else result[idx] = compiled;
2951
+ return result;
2952
+ }
2953
+ static buildFromValue(paramName, value) {
2954
+ const tree = new TreeBuilder(paramName);
2955
+ if (isDataTreeDefault(value)) tree.fromDataTreeDefault(value);
2956
+ else tree.appendFlat(value);
2957
+ return tree;
2958
+ }
2959
+ /**
2960
+ * `paramName`'s whole tree from a solve result's `values`, keyed by branch path (`'{0;1}'`).
2961
+ * `null` when no tree has that param, an empty `Map` when it has no branches. An item without
2962
+ * `data` reads as `null` in place, so indices don't shift.
2963
+ */
2964
+ static getTree(trees, paramName) {
2965
+ const tree = trees.find((t) => TreeBuilder.paramNameOf(t) === paramName);
2966
+ if (!tree) return null;
2967
+ const branches = /* @__PURE__ */ new Map();
2968
+ const inner = readField(tree, "InnerTree");
2969
+ if (!inner || typeof inner !== "object") return branches;
2970
+ for (const [path, items] of Object.entries(inner)) {
2971
+ if (!Array.isArray(items)) continue;
2972
+ branches.set(path, items.map((item) => item?.data !== void 0 ? TreeBuilder.deserializeValue(item.data) : null));
2973
+ }
2974
+ return branches;
2975
+ }
2976
+ /**
2977
+ * `paramName`'s value when its tree has a single branch: one item unwrapped (`5`, not `[5]`),
2978
+ * several as an array. `null` for a missing param or an empty tree.
2979
+ *
2980
+ * @throws {ComputeError} `INVALID_INPUT` when the tree has more than one branch; read those
2981
+ * with {@link TreeBuilder.getTree}.
2982
+ */
2983
+ static getTreeValue(trees, paramName) {
2984
+ const tree = TreeBuilder.getTree(trees, paramName);
2985
+ if (!tree || tree.size === 0) return null;
2986
+ if (tree.size > 1) throw new ComputeError(`"${paramName}" has ${tree.size} branches; read it with TreeBuilder.getTree`, ErrorCodes.INVALID_INPUT, { context: {
2987
+ paramName,
2988
+ branchCount: tree.size
2989
+ } });
2990
+ const [items] = tree.values();
2991
+ if (items.length === 0) return null;
2992
+ return items.length === 1 ? items[0] : items;
2993
+ }
2994
+ static paramNameOf(tree) {
2995
+ return readField(tree, "ParamName");
2996
+ }
2997
+ /**
2998
+ * Parse a branch path like `"{0;1;2}"` into `[0, 1, 2]`.
2999
+ *
3000
+ * @throws {ComputeError} `INVALID_INPUT` for a malformed path, rather than
3001
+ * collapsing it to a default branch where two bad keys would merge.
3002
+ */
3003
+ static parsePathString(pathStr) {
3004
+ const match = pathStr.match(TREE_PATH_RE);
3005
+ if (!match) throw new ComputeError(`Invalid Grasshopper tree path: "${pathStr}". Expected "{}", "{0}", or "{0;1;2}" (negative indices allowed, no empty segments).`, ErrorCodes.INVALID_INPUT, { context: { pathStr } });
3006
+ if (!match[1]) return [];
3007
+ return match[1].split(";").map(Number);
3008
+ }
3009
+ /** `[0, 1, 2]` → `"{0;1;2}"`. */
3010
+ static formatPathString(path) {
3011
+ return `{${path.join(";")}}`;
3012
+ }
3013
+ /** Booleans and numbers stay primitives: Grasshopper reads them typed. */
3014
+ static serializeValue(value) {
3015
+ return typeof value === "object" ? JSON.stringify(value) : value;
3016
+ }
3017
+ static deserializeValue(data) {
3018
+ if (typeof data !== "string") return data;
3019
+ if (data.startsWith("{") || data.startsWith("[")) try {
3020
+ return JSON.parse(data);
3021
+ } catch {
3022
+ return data;
3023
+ }
3024
+ const num = Number(data);
3025
+ if (Number.isFinite(num) && String(num) === data) return num;
3026
+ if (data === "true") return true;
3027
+ if (data === "false") return false;
3028
+ return data;
3029
+ }
3030
+ static hasValidValue(value) {
3031
+ if (value === void 0 || value === null) return false;
3032
+ if (typeof value === "string") return true;
3033
+ if (Array.isArray(value) && value.length === 0) return false;
3034
+ if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) return false;
3035
+ return true;
3036
+ }
3037
+ static clampValue(value, min, max, inputName) {
3038
+ let result = value;
3039
+ if (min !== null && min !== void 0 && result < min) {
3040
+ getLogger().warn(`${inputName}: ${value} below min ${min}, clamping`);
3041
+ result = min;
3042
+ }
3043
+ if (max !== null && max !== void 0 && result > max) {
3044
+ getLogger().warn(`${inputName}: ${value} above max ${max}, clamping`);
3045
+ result = max;
3046
+ }
3047
+ return result;
3048
+ }
3049
+ };
3050
+ export { ComputeServerStats, GrasshopperClient, GrasshopperResponseProcessor, SolveScheduler, TreeBuilder, disposeRhinoObjects, fetchDefinitionIO, fetchParsedDefinitionIO, hashDefinition, hashSolveInput, isRhinoDecodeError, normalizeUISchemaCasing, processInput, readSchemaResults, solveGrasshopperDefinition, stableStringify };
3051
+
2
3052
  //# sourceMappingURL=grasshopper.js.map