@selvajs/compute 4.2.2-beta.0 → 4.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ const e={NETWORK_ERROR:`NETWORK_ERROR`,AUTH_ERROR:`AUTH_ERROR`,VALIDATION_ERROR:`VALIDATION_ERROR`,COMPUTATION_ERROR:`COMPUTATION_ERROR`,TIMEOUT_ERROR:`TIMEOUT_ERROR`,CORS_ERROR:`CORS_ERROR`,NOT_FOUND:`NOT_FOUND`,RATE_LIMIT:`RATE_LIMIT`,INVALID_RESPONSE:`INVALID_RESPONSE`,UNKNOWN_ERROR:`UNKNOWN_ERROR`,INVALID_STATE:`INVALID_STATE`,INVALID_INPUT:`INVALID_INPUT`,INVALID_CONFIG:`INVALID_CONFIG`,BROWSER_ONLY:`BROWSER_ONLY`,ENVIRONMENT_ERROR:`ENVIRONMENT_ERROR`,ENCODING_ERROR:`ENCODING_ERROR`,MALFORMED_DEFAULT:`MALFORMED_DEFAULT`,SUPERSEDED:`SUPERSEDED`,ABORTED:`ABORTED`,QUEUE_FULL:`QUEUE_FULL`,QUEUE_TIMEOUT:`QUEUE_TIMEOUT`,DEFINITION_NOT_CACHED:`DEFINITION_NOT_CACHED`};var t=class t extends Error{code;statusCode;context;originalError;constructor(t,n=e.UNKNOWN_ERROR,r){super(t),this.name=`ComputeError`,this.code=n,this.statusCode=r?.statusCode,this.context=r?.context,this.originalError=r?.originalError,r?.originalError&&(this.cause=r.originalError)}static missingValues(n,r,i){return new t(`Input "${n}" has no values defined${r?` (expected ${r})`:``}`,e.INVALID_INPUT,{context:{inputName:n,expectedType:r,...i}})}static unknownParamType(n,r,i){return new t(`Unknown paramType: ${n}`,e.VALIDATION_ERROR,{context:{receivedParamType:n,paramName:r,...i}})}},n=class{debug(){}info(){}warn(){}error(){}},r=class{debug(e,...t){console.debug(e,...t)}info(e,...t){console.info(e,...t)}warn(e,...t){console.warn(e,...t)}error(e,...t){console.error(e,...t)}};let i=new n;function a(){return i}function o(r){if(r===null){i=new n;return}let a=[`debug`,`info`,`warn`,`error`].filter(e=>typeof r[e]!=`function`);if(a.length>0)throw new t(`Logger is missing required method(s): ${a.join(`, `)}. A logger must implement debug, info, warn and error.`,e.INVALID_CONFIG,{context:{missingMethods:a}});i=r}function s(){o(new r)}function c(e,t){if(!e||typeof e!=`object`)return;let n=e;if(l(n,t))return n[t];let r=f(n).get(t.toLowerCase());return r===void 0?void 0:n[r]}const l=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);function u(e,t){if(!e||typeof e!=`object`)return!1;let n=e;return l(n,t)?!0:f(n).has(t.toLowerCase())}const d=new WeakMap;function f(e){let t=Object.keys(e),n=d.get(e);if(n&&n.keyCount===t.length)return n.map;let r=new Map;for(let e of t){let t=e.toLowerCase();r.has(t)||r.set(t,e)}return d.set(e,{keyCount:t.length,map:r}),r}const p=`compute.rhino3d.com`;function m(n,r){let i=n?.trim()??``;if(!i)throw new t(`serverUrl is required`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});if(!/^https?:\/\//i.test(i))throw new t(`Invalid serverUrl: "${i}". Must start with "http://" or "https://". For example: "http://localhost:5000" or "https://example.com"`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});let a;try{a=new URL(i)}catch(r){throw new t(`Invalid serverUrl: "${i}". Must be a valid URL. Received error: ${r instanceof Error?r.message:String(r)}`,e.INVALID_CONFIG,{context:{receivedServerUrl:n},originalError:r instanceof Error?r:void 0})}if(a.username!==``||a.password!==``)throw new t(`Invalid serverUrl: "${i}". Must not embed credentials (user:pass@host) — fetch rejects credentialed URLs at request time. Pass the API key separately.`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});if(i.includes(`?`)||i.includes(`#`))throw new t(`Invalid serverUrl: "${i}". Must not contain a query string or fragment — endpoint paths are appended to this URL (e.g. "\${serverUrl}/version"), and a "?" or "#" suffix would corrupt every request path.`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});let o=a.hostname.toLowerCase().replace(/\.+$/,``);if((r?.blockedHosts??[`compute.rhino3d.com`]).map(e=>e.toLowerCase().replace(/\.+$/,``)).includes(o))throw new t(`serverUrl must be set to your Compute server URL. The shared public endpoint is not allowed.`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});return i.replace(/\/+$/,``)}function h(e,t){return`${t.replace(/\/+$/,``)}/${e.replace(/^\/+/,``)}`}function g(e){try{let t=new URL(e).hostname.toLowerCase();return t===`localhost`||t===`127.0.0.1`||t===`[::1]`}catch{return/(localhost|127\.0\.0\.1|\[::1\])/i.test(e)}}const _=new Set;function v(e){return{...e.headers,...e.authToken&&{Authorization:e.authToken},...e.apiKey&&{[e.apiKeyHeader??`RhinoComputeKey`]:e.apiKey}}}function y(e,t){let n={...v(t),"X-Request-ID":e,"Content-Type":`application/json`};return!t.apiKey&&!t.authToken&&!_.has(t.serverUrl)&&!g(t.serverUrl)&&(_.add(t.serverUrl),a().warn(`⚠️ [Compute] Request [${e}] targets remote server (${t.serverUrl}) but no API key or auth token is configured. Requests may fail or be rate-limited. (warned once per server)`)),n}function ee(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}const b=/ECONNREFUSED|ERR_CONNECTION_REFUSED|connection refused/i,x=/ENOTFOUND|EAI_AGAIN|getaddrinfo|ERR_NAME_NOT_RESOLVED|dns/i,S=/TimeoutError|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|ENETUNREACH|abort|timed? ?out/i;function te(e){if(e.online)return null;if(e.status!==void 0){if(e.status===401||e.status===403)return{verdict:`unauthorized`,retryable:!1,summary:`The server rejected the liveness probe with HTTP ${e.status} — check the API key.`};let t=e.status>=500;return{verdict:`http_error`,retryable:t,summary:t?`The server answered HTTP ${e.status} — it may still be starting up.`:`The server answered HTTP ${e.status}, which will not change on retry.`}}let t=e.error??``;return b.test(t)?{verdict:`refused`,retryable:!1,summary:`Connection refused — the host is reachable but nothing is listening on that port. Rhino.Compute is not running, or the URL names the wrong port.`}:x.test(t)?{verdict:`dns`,retryable:!0,summary:`The server hostname could not be resolved — check the URL and DNS.`}:S.test(t)?{verdict:`timeout`,retryable:!0,summary:`No response before the timeout — the machine may be powered off, blocked by a firewall, or still booting.`}:{verdict:`unknown`,retryable:!0,summary:t?`The connection failed: ${t}`:`The connection failed.`}}function C(){let e=globalThis.Buffer;return typeof e==`function`?e:void 0}function ne(e){let t=C();return t?t.from(e,`utf-8`).toString(`base64`):E(new TextEncoder().encode(e))}function re(e){if(!e)return null;let t=e.replace(/[\t\n\f\r ]/g,``);t.length%4==0&&(t=t.replace(/={1,2}$/,``));let n=t.length%4;if(n===1||t.length<64||!/^[A-Za-z0-9+/]*$/.test(t))return null;let r=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`.indexOf(t.charAt(t.length-1));return n===2&&r&15||n===3&&r&3?null:n===0?t:t+`==`.slice(0,4-n)}function w(n){let r=n.replace(/[\t\n\f\r ]/g,``);if(r.length%4==0&&(r=r.replace(/={1,2}$/,``)),r.length%4==1||!/^[A-Za-z0-9+/]*$/.test(r))throw new t(`Invalid base64 input.`,e.ENCODING_ERROR,{context:{inputLength:n.length}});let i=C();if(i)return new Uint8Array(i.from(r,`base64`));if(typeof globalThis.atob==`function`){let e=globalThis.atob(r),t=new Uint8Array(e.length);for(let n=0;n<e.length;n++)t[n]=e.charCodeAt(n)&255;return t}throw new t(`Base64 decoding not supported in this environment.`,e.ENVIRONMENT_ERROR,{context:{environmentInfo:`atob or Buffer not available`}})}function T(e){let t=C();if(t)return t.byteLength(e,`utf-8`);let n=0;for(let t=0;t<e.length;t++){let r=e.charCodeAt(t);r<128?n+=1:r<2048?n+=2:r>=55296&&r<=56319&&t+1<e.length&&(e.charCodeAt(t+1)&64512)==56320?(n+=4,t++):n+=3}return n}function E(n){let r=C();if(r)return r.from(n).toString(`base64`);if(typeof globalThis.btoa==`function`){let e=32766,t=[];for(let r=0;r<n.length;r+=e)t.push(globalThis.btoa(String.fromCharCode.apply(null,n.subarray(r,r+e))));return t.join(``)}throw new t(`Base64 encoding not supported in this environment.`,e.ENVIRONMENT_ERROR,{context:{environmentInfo:`btoa or Buffer not available`}})}function D(e,t){t&&a().debug(e)}function ie(e){if(!e)return null;let t={raw:e},n=!1;for(let r of e.split(`,`)){let[e,...i]=r.trim().split(`;`),a=i.find(e=>e.trim().toLowerCase().startsWith(`dur`));if(!a)continue;let o=Number(a.split(`=`)[1]);if(!Number.isFinite(o))continue;let s=e.trim().toLowerCase();(s===`decode`||s===`solve`||s===`encode`)&&(t[s]=o,n=!0)}return n?t:null}function O(e,t,n,r){if(!n)return;let i=ie(e.headers.get(`Server-Timing`));if(i)try{n(i,t)}catch(e){r&&D(` onServerTiming callback threw: ${e}`,!0)}}const k=new WeakMap;function A(e,t){typeof e==`object`&&e&&k.set(e,t)}function ae(e){if(typeof e==`object`&&e)return k.get(e)}const j=4096;function M(e){return e.length<=j?e:`${e.slice(0,j)}… [truncated ${e.length-j} chars]`}function N(n,r,i,a,o,s,c,l,u){let{status:d,statusText:f}=n,p={};n.headers.forEach((e,t)=>{p[t]=e});let m=s.trim(),h=m?` — ${m.slice(0,200)}${m.length>200?`…`:``}`:``,g=l??s,_={url:r,requestId:i,method:`POST`,requestSize:a,serverUrl:o,responseBody:g?M(g):void 0,responseHeaders:p},v={400:{message:`Bad request: ${f}${h}`,code:e.VALIDATION_ERROR},401:{message:`HTTP ${d}: ${f}${h}`,code:e.AUTH_ERROR},403:{message:`HTTP ${d}: ${f}${h}`,code:e.AUTH_ERROR},404:{message:`Endpoint not found: ${r}`,code:e.NOT_FOUND},413:{message:`Request too large: ${(a/1024).toFixed(2)}KB`,code:e.VALIDATION_ERROR},429:{message:`Rate limit exceeded${h}`,code:e.RATE_LIMIT},500:{message:`Server error: ${f}${h}`,code:e.COMPUTATION_ERROR},502:{message:`Bad gateway: ${f}${h}`,code:e.NETWORK_ERROR},503:{message:`Service unavailable: ${f}${h}`,code:e.NETWORK_ERROR},504:{message:`Service unavailable: ${f}${h}`,code:e.NETWORK_ERROR}}[d]||{message:`HTTP ${d}: ${f}${h}`,code:e.UNKNOWN_ERROR},y=P(c,u)??v.code;throw new t(v.message,y,{statusCode:d,context:_})}function P(e,t){if(e&&t)return t[e]}async function F(n,r,i,a,o,s,l,u,d){let f=Math.round(performance.now()-s);if(!n.ok){let e=await n.text(),t=e;l&&(D(`❌ Request [${i}] failed with HTTP ${n.status} in ${f}ms`,!0),D(` URL: ${r}`,!0),D(` Status: ${n.status} ${n.statusText}`,!0),t&&D(` Response body: ${t.substring(0,500)}${t.length>500?`...`:``}`,!0));let s;try{let e=JSON.parse(t);typeof e?.code==`string`&&(s=e.code)}catch{}if(n.status===500)try{let e=JSON.parse(t),r=c(e,`values`),a=c(e,`errors`),o=c(e,`warnings`);if(r&&(a||o))return A(e,t.length),l&&(D(`⚠️ Request [${i}] completed with solver errors in ${f}ms`,!0),a&&a.length>0&&D(` Errors: ${JSON.stringify(a,null,2)}`,!0),o&&o.length>0&&D(` Warnings: ${JSON.stringify(o,null,2)}`,!0)),O(n,i,u,l),e;let s=typeof e?.message==`string`&&e.message||typeof e?.Message==`string`&&e.Message||``,d=typeof e?.ExceptionType==`string`&&e.ExceptionType||``,p=e?.stackTrace??e?.StackTrace,m=Array.isArray(p)?p.join(`
2
+ `):p||``;s?t=`${d?`${d}: `:``}${s}${m?`\n${m}`:``}`:e?.error&&(t=typeof e.error==`string`?e.error:JSON.stringify(e.error,null,2))}catch(e){l&&D(` Failed to parse error body as JSON: ${e}`,!0)}N(n,r,i,a,o,t,s,e,d)}D(`✅ Request [${i}] completed in ${f}ms`,l),O(n,i,u,l);try{let e=await n.text(),t=JSON.parse(e);return A(t,e.length),t}catch(a){if(a instanceof Error&&(a.name===`AbortError`||a.name===`TimeoutError`))throw a;let o=(n.headers.get(`Content-Type`)??``).toLowerCase();throw o!==``&&!o.includes(`json`)?new t(`Server returned a non-JSON response (Content-Type: ${o}) — check the server URL / proxy configuration`,e.INVALID_RESPONSE,{statusCode:n.status,context:{url:r,requestId:i,contentType:o},originalError:a instanceof Error?a:Error(String(a))}):new t(`Failed to parse JSON response`,e.NETWORK_ERROR,{statusCode:n.status,context:{url:r,requestId:i},originalError:a instanceof Error?a:Error(String(a))})}}const I={attempts:0,baseDelayMs:500,maxDelayMs:3e4,retryOn429:!0},L=new Set([502,503,504]);function R(e){return e?{attempts:e.attempts??I.attempts,baseDelayMs:e.baseDelayMs??I.baseDelayMs,maxDelayMs:e.maxDelayMs??I.maxDelayMs,retryOn429:e.retryOn429??I.retryOn429}:I}function z(e){if(!e)return null;let t=Number(e);if(Number.isFinite(t)&&t>=0)return t*1e3;let n=Date.parse(e);if(Number.isFinite(n)){let e=n-Date.now();return e>0?e:0}return null}function B(e,t){let n=t.baseDelayMs*2**e,r=Math.random()*t.baseDelayMs;return Math.min(n+r,t.maxDelayMs)}function V(e,t){return new Promise((n,r)=>{if(t?.aborted){r(new DOMException(`Aborted`,`AbortError`));return}let i=setTimeout(()=>{t?.removeEventListener(`abort`,a),n()},e),a=()=>{clearTimeout(i),r(new DOMException(`Aborted`,`AbortError`))};t?.addEventListener(`abort`,a,{once:!0})})}function H(e,t){let n=()=>{},r=typeof t==`number`&&t>0;if(!e&&!r)return{signal:void 0,cleanup:n};if(e&&!r)return{signal:e,cleanup:n};let i=typeof AbortSignal<`u`&&typeof AbortSignal.timeout==`function`;if(!e){if(i)return{signal:AbortSignal.timeout(t),cleanup:n};let e=new AbortController,r=setTimeout(()=>e.abort(),t);return{signal:e.signal,cleanup:()=>clearTimeout(r)}}let a=new AbortController,o,s;if(i)o=AbortSignal.timeout(t);else{let e=new AbortController;s=setTimeout(()=>e.abort(),t),o=e.signal}let c=[e,o],l=function(){a.abort(this.reason)};for(let e of c){if(e.aborted){a.abort(e.reason);break}e.addEventListener(`abort`,l,{once:!0})}return{signal:a.signal,cleanup:()=>{s!==void 0&&clearTimeout(s);for(let e of c)e.removeEventListener(`abort`,l)}}}async function U(n,r,i,a){let{signal:o,cleanup:s}=H(n.config.signal,n.config.timeoutMs),c=performance.now();try{let s=await fetch(n.fullUrl,{method:`POST`,body:n.body,headers:n.headers,signal:o});if((L.has(s.status)||r.retryOn429&&s.status===429)&&i<a-1){let a=z(s.headers.get(`Retry-After`)),o=a===null?B(i,r):Math.min(a,Math.max(r.maxDelayMs,6e4));return await s.text().catch(()=>{}),{ok:!1,retry:!0,delayMs:o,cause:new t(`HTTP ${s.status} ${s.statusText} (will retry)`,s.status===429?e.RATE_LIMIT:e.NETWORK_ERROR,{statusCode:s.status,context:{requestId:n.requestId}})}}return{ok:!0,value:await F(s,n.fullUrl,n.requestId,n.requestSize,n.config.serverUrl,c,n.config.debug,n.config.onServerTiming,n.config.serverErrorCodes)}}catch(o){if(o instanceof Error&&(o.name===`AbortError`||o.name===`TimeoutError`)){if(n.config.signal?.aborted===!0)return{ok:!1,retry:!1,cause:new t(`Request aborted by caller`,e.ABORTED,{context:{endpoint:n.endpoint,requestId:n.requestId,requestSize:n.requestSize},originalError:o})};let s=typeof n.config.timeoutMs==`number`&&n.config.timeoutMs>0?new t(`Request timed out after ${n.config.timeoutMs}ms`,e.TIMEOUT_ERROR,{context:{serverUrl:n.config.serverUrl,timeoutMs:n.config.timeoutMs,url:n.fullUrl,requestId:n.requestId,endpoint:n.endpoint,requestSize:n.requestSize},originalError:o}):new t(`Request aborted by the runtime (${o.name}): ${o.message}`,e.NETWORK_ERROR,{context:{serverUrl:n.config.serverUrl,url:n.fullUrl,requestId:n.requestId,endpoint:n.endpoint,requestSize:n.requestSize},originalError:o});return i<a-1?{ok:!1,retry:!0,delayMs:B(i,r),cause:s}:{ok:!1,retry:!1,cause:s}}if(o instanceof TypeError){let s=typeof window<`u`&&window.document!==void 0,c={serverUrl:n.config.serverUrl,url:n.fullUrl,requestId:n.requestId,endpoint:n.endpoint,requestSize:n.requestSize},l=s?new t(`Request failed: ${o.message} — in a browser this usually means a CORS misconfiguration on the server (or a network failure; the browser does not distinguish them)`,e.CORS_ERROR,{context:c,originalError:o}):new t(`Network error: ${o.message}`,e.NETWORK_ERROR,{context:c,originalError:o});return i<a-1?{ok:!1,retry:!0,delayMs:B(i,r),cause:l}:{ok:!1,retry:!1,cause:l}}if(o instanceof t){let t=o.statusCode;return(o.code===e.NETWORK_ERROR&&t!==void 0&&t>=200&&t<300||t!==void 0&&(L.has(t)||r.retryOn429&&t===429))&&i<a-1?{ok:!1,retry:!0,delayMs:B(i,r),cause:o}:{ok:!1,retry:!1,cause:o}}return{ok:!1,retry:!1,cause:new t(o instanceof Error?o.message:String(o),e.UNKNOWN_ERROR,{context:{endpoint:n.endpoint,requestId:n.requestId},originalError:o instanceof Error?o:Error(String(o))})}}finally{s()}}async function W(n,r,i){let a=ee(),o;try{o=JSON.stringify(r)}catch(r){throw new t(`Request body is not JSON-serializable: ${r instanceof Error?r.message:String(r)}`,e.INVALID_INPUT,{context:{endpoint:n,requestId:a},originalError:r instanceof Error?r:Error(String(r))})}let s=T(o),c=h(n,i.serverUrl),l=y(a,i),u=R(i.retry),d=u.attempts+1;if(i.debug){let e=(s/1024).toFixed(2);D(`${s>1e5?`⚠️`:`🚀`} Starting compute request [${a}]: ${n} (${e}KB)`,!0)}let f={endpoint:n,body:o,requestSize:s,fullUrl:c,requestId:a,headers:l,config:i};for(let r=0;;r++){let o=await U(f,u,r,d);if(o.ok)return o.value;if(!o.retry)throw o.cause;i.debug&&D(`🔁 Request [${a}] retrying after ${o.delayMs}ms (attempt ${r+2}/${d}): ${o.cause.message}`,!0);try{await V(o.delayMs,i.signal)}catch{throw new t(`Request aborted by caller`,e.ABORTED,{context:{endpoint:n,requestId:a,requestSize:s},originalError:o.cause})}}}function G(e){return typeof e==`object`&&!(e instanceof Uint8Array)&&typeof e.key==`string`&&typeof e.load==`function`}const K=e=>(e??``).replace(/::/g,`/`).replace(/\\/g,`/`).split(`/`).map(e=>e.trim()).filter(e=>e!==``),q=e=>K(e.subFolder)[0]??``,oe=e=>K(e.subFolder).slice(1).join(`/`),J=e=>{let t=new Map;for(let n of e){let e=q(n),r=t.get(e);r?r.push(n):t.set(e,[n])}return Array.from(t,([e,t])=>({root:e,files:t}))},se=e=>{let t=e.replace(/[/\\:*?"<>|]/g,`_`).replace(/\s+/g,` `).trim().replace(/^\.+/,``);return t===``?`files`:t},ce=async(e,t=null)=>Q(e,t),Y=async(n,r,i=null)=>{if(typeof document>`u`||typeof Blob>`u`)throw new t(`File download functionality is only available in browser environments. This function requires the DOM API (document, Blob).`,e.BROWSER_ONLY,{context:{environment:typeof window<`u`?`browser (SSR)`:`Node.js`,documentAvailable:typeof document<`u`,blobAvailable:typeof Blob<`u`}});try{await fe(await Q(n,i),r)}catch(n){throw n instanceof t?n:new t(`Failed to download files from compute response`,e.INVALID_STATE,{context:{originalError:n instanceof Error?n.message:String(n)},originalError:n instanceof Error?n:void 0})}},le=async(e,t,n=null)=>{let r=J(e);if(r.length===0){await Y([],t,n);return}let i=(r.find(e=>e.root===``)??r[0]).root;for(let{root:e,files:a}of r)await Y(e===``?a:a.map(e=>({...e,subFolder:oe(e)})),e===``?t:se(e),e===i?n:null)},X=e=>e.replace(/::/g,`/`).replace(/\\/g,`/`).split(`/`).map(e=>e.trim()).filter(e=>e!==``&&e!==`.`&&e!==`..`&&!/^[a-zA-Z]:$/.test(e)).join(`/`),ue=e=>e===!0||typeof e==`string`&&e.trim().toLowerCase()===`true`,de=e=>{let t=[];return e.forEach(e=>{let n=c(e,`fileName`)??``,r=c(e,`fileType`)??``,i=X(`${n}${r}`);if(i===``){a().warn(`Skipping file with unusable name "${n}${r}": no safe archive path remains after sanitization.`);return}let o=c(e,`subFolder`),s=o?X(o):``,l=s===``?i:`${s}/${i}`,u=c(e,`data`);if(typeof u!=`string`||u===``){a().warn(`Skipping file "${l}": item carries no usable data.`);return}let d=c(e,`metadata`);if(ue(c(e,`isBase64Encoded`)))try{t.push({fileName:i,content:w(u),path:l,subFolder:s,...d?{metadata:d}:{}})}catch(e){a().warn(`Skipping file "${l}": base64 decode failed.`,e)}else t.push({fileName:i,content:u,path:l,subFolder:s,...d?{metadata:d}:{}})}),t},Z=async e=>(await Promise.all(e.map(async e=>{try{let t=typeof AbortSignal<`u`&&typeof AbortSignal.timeout==`function`?AbortSignal.timeout(3e4):void 0,n=await fetch(e.filePath,{signal:t});if(!n.ok)return a().warn(`Failed to fetch additional file from URL: ${e.filePath}`),null;let r=await n.arrayBuffer(),i=X(e.fileName);if(i===``)return a().warn(`Skipping fetched file with unusable name: ${e.fileName}`),null;let o=e.subFolder?X(e.subFolder):``;return{fileName:i,content:new Uint8Array(r),path:o===``?i:`${o}/${i}`,subFolder:o}}catch(t){return a().error(`Error fetching additional file from URL: ${e.filePath}`,t),null}}))).filter(e=>e!==null),Q=async(e,t)=>{let n=de(e);if(t){let e=Array.isArray(t)?t:[t];n.push(...await Z(e))}let r=new Set;return n.map(e=>{let t=$(e.path,r);return r.add(t),t===e.path?e:(a().warn(`Duplicate archive path "${e.path}" — storing as "${t}".`),{...e,path:t,fileName:t.slice(t.lastIndexOf(`/`)+1)})})};async function fe(e,t){let{zip:n,strToU8:r}=await import(`fflate`),i={},o=new Set;e.forEach(e=>{let t=$(e.path,o);o.add(t),t!==e.path&&a().warn(`Duplicate archive path "${e.path}" — storing as "${t}".`),i[t]=typeof e.content==`string`?r(e.content):e.content});let s=await new Promise((e,t)=>{n(i,{level:6},(n,r)=>n?t(n):e(r))});pe(new Blob([s],{type:`application/zip`}),`${t}.zip`)}function $(e,t){if(!t.has(e))return e;let n=e.lastIndexOf(`/`),r=e.lastIndexOf(`.`),i=r>n?r:e.length,a=e.slice(0,i),o=e.slice(i);for(let e=2;;e++){let n=`${a}-${e}${o}`;if(!t.has(n))return n}}function pe(n,r){if(typeof document>`u`)throw new t(`saveFile requires a browser environment with DOM API access.`,e.BROWSER_ONLY,{context:{function:`saveFile`,requiredAPI:`document`}});let i=URL.createObjectURL(n),a=document.createElement(`a`);a.href=i,a.download=r,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(i),1e4)}export{t as C,o as S,m as _,K as a,s as b,ae as c,w as d,re as f,p as g,v as h,J as i,A as l,te as m,le as n,G as o,ne as p,ce as r,W as s,Y as t,E as u,u as v,e as w,a as x,c as y};
3
+ //# sourceMappingURL=core-rp3fyoca.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-rp3fyoca.js","names":[],"sources":["../src/core/errors.ts","../src/core/utils/logger.ts","../src/core/utils/read-field.ts","../src/core/server/validate-server-url.ts","../src/core/compute-fetch/request.ts","../src/core/server/classify-probe-failure.ts","../src/core/utils/encoding.ts","../src/core/compute-fetch/log.ts","../src/core/compute-fetch/server-timing.ts","../src/core/compute-fetch/wire-size.ts","../src/core/compute-fetch/response.ts","../src/core/compute-fetch/retry.ts","../src/core/compute-fetch/signal.ts","../src/core/compute-fetch/compute-fetch.ts","../src/core/definition-ref.ts","../src/core/files/sub-folder.ts","../src/core/files/handle-files.ts"],"sourcesContent":["export const ErrorCodes = {\n\tNETWORK_ERROR: 'NETWORK_ERROR',\n\tAUTH_ERROR: 'AUTH_ERROR',\n\tVALIDATION_ERROR: 'VALIDATION_ERROR',\n\tCOMPUTATION_ERROR: 'COMPUTATION_ERROR',\n\tTIMEOUT_ERROR: 'TIMEOUT_ERROR',\n\tCORS_ERROR: 'CORS_ERROR',\n\t/** HTTP 404 — the endpoint path does not exist on the server (usually a typo'd route or wrong server). Never retried. */\n\tNOT_FOUND: 'NOT_FOUND',\n\t/** HTTP 429 — the server is rate-limiting requests. Retried (honoring `Retry-After`) once a retry policy sets `attempts > 0`; opt out via `RetryPolicy.retryOn429`. */\n\tRATE_LIMIT: 'RATE_LIMIT',\n\t/**\n\t * The server answered 2xx but the body is deterministically not JSON (the\n\t * response declared a non-JSON `Content-Type`, e.g. an HTML captive-portal or\n\t * proxy login page, or a misconfigured endpoint). Never retried — refetching\n\t * returns the same page.\n\t */\n\tINVALID_RESPONSE: 'INVALID_RESPONSE',\n\tUNKNOWN_ERROR: 'UNKNOWN_ERROR',\n\tINVALID_STATE: 'INVALID_STATE',\n\tINVALID_INPUT: 'INVALID_INPUT',\n\tINVALID_CONFIG: 'INVALID_CONFIG',\n\tBROWSER_ONLY: 'BROWSER_ONLY',\n\t/** The runtime lacks a required capability (e.g. no base64 codec — neither `Buffer` nor `atob`/`btoa`). */\n\tENVIRONMENT_ERROR: 'ENVIRONMENT_ERROR',\n\tENCODING_ERROR: 'ENCODING_ERROR',\n\t/** An input's `default` had a shape the normalizer didn't recognize (no innerTree key). */\n\tMALFORMED_DEFAULT: 'MALFORMED_DEFAULT',\n\t/** Scheduler latest-wins: this call was replaced by a newer one. */\n\tSUPERSEDED: 'SUPERSEDED',\n\t/** Scheduler / caller-supplied AbortSignal: this call was aborted. */\n\tABORTED: 'ABORTED',\n\t/**\n\t * Scheduler backpressure: the queue was already at `maxQueueDepth` when this\n\t * call arrived, so it was rejected immediately rather than queued. Retryable — the\n\t * caller (or an HTTP layer, as 503 + Retry-After) should back off and retry.\n\t * The error `context` carries `{ queueDepth, maxQueueDepth }`.\n\t */\n\tQUEUE_FULL: 'QUEUE_FULL',\n\t/**\n\t * Scheduler backpressure: this call sat queued longer than `queueWaitMs`\n\t * without starting, so it was rejected before wasting compute on a stale request.\n\t * Retryable. The error `context` carries `{ waitedMs, queueWaitMs }`.\n\t */\n\tQUEUE_TIMEOUT: 'QUEUE_TIMEOUT',\n\t/**\n\t * A solve referenced a definition by `pointer` (server-side cache key), but the\n\t * server no longer holds that definition (evicted / GC'd / a different child in\n\t * the pool / server restarted). The caller should retry with the full\n\t * definition. Surfaced when the server tags its error body with\n\t * `code: \"definition_not_cached\"`, so it survives production message-scrubbing.\n\t */\n\tDEFINITION_NOT_CACHED: 'DEFINITION_NOT_CACHED'\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n/**\n * Simplified error for Rhino Compute operations\n *\n * @public Use this for error handling with error codes and context.\n */\nexport class ComputeError extends Error {\n\tpublic readonly code: ErrorCode;\n\tpublic readonly statusCode?: number;\n\tpublic readonly context?: Record<string, unknown>;\n\tpublic readonly originalError?: Error;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: ErrorCode = ErrorCodes.UNKNOWN_ERROR,\n\t\toptions?: { statusCode?: number; context?: Record<string, unknown>; originalError?: Error }\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'ComputeError';\n\t\tthis.code = code;\n\t\tthis.statusCode = options?.statusCode;\n\t\tthis.context = options?.context;\n\t\tthis.originalError = options?.originalError;\n\t\tif (options?.originalError) {\n\t\t\t(this as { cause?: unknown }).cause = options.originalError;\n\t\t}\n\t}\n\n\t/**\n\t * Create an error for missing/empty values\n\t */\n\tstatic missingValues(\n\t\tinputName: string,\n\t\texpectedType?: string,\n\t\tcontext?: Record<string, unknown>\n\t) {\n\t\treturn new ComputeError(\n\t\t\t`Input \"${inputName}\" has no values defined${expectedType ? ` (expected ${expectedType})` : ''}`,\n\t\t\tErrorCodes.INVALID_INPUT,\n\t\t\t{ context: { inputName, expectedType, ...context } }\n\t\t);\n\t}\n\n\t/**\n\t * Create an error for unknown parameter type\n\t */\n\tstatic unknownParamType(\n\t\tparamType: string,\n\t\tparamName?: string,\n\t\tcontext?: Record<string, unknown>\n\t) {\n\t\treturn new ComputeError(`Unknown paramType: ${paramType}`, ErrorCodes.VALIDATION_ERROR, {\n\t\t\tcontext: { receivedParamType: paramType, paramName, ...context }\n\t\t});\n\t}\n}\n","import { ComputeError, ErrorCodes } from '../errors';\n\n/**\n * Logger interface for structured logging\n *\n * @public Implement this interface to provide custom logging behavior.\n */\nexport interface Logger {\n\tdebug(message: string, ...args: unknown[]): void;\n\tinfo(message: string, ...args: unknown[]): void;\n\twarn(message: string, ...args: unknown[]): void;\n\terror(message: string, ...args: unknown[]): void;\n}\n\n/**\n * No-op logger implementation (default)\n * @internal\n */\nclass NoOpLogger implements Logger {\n\tdebug(): void {}\n\tinfo(): void {}\n\twarn(): void {}\n\terror(): void {}\n}\n\n/**\n * Console logger implementation\n * @internal\n */\nclass ConsoleLogger implements Logger {\n\tdebug(message: string, ...args: unknown[]): void {\n\t\tconsole.debug(message, ...args);\n\t}\n\n\tinfo(message: string, ...args: unknown[]): void {\n\t\tconsole.info(message, ...args);\n\t}\n\n\twarn(message: string, ...args: unknown[]): void {\n\t\tconsole.warn(message, ...args);\n\t}\n\n\terror(message: string, ...args: unknown[]): void {\n\t\tconsole.error(message, ...args);\n\t}\n}\n\n/**\n * Internal logger instance\n * @internal\n */\nlet internalLogger: Logger = new NoOpLogger();\n\n/**\n * Get the current logger instance\n *\n * @returns The current logger instance\n */\nexport function getLogger(): Logger {\n\treturn internalLogger;\n}\n\n/**\n * Set a custom logger instance\n *\n * @public Use this to configure custom logging behavior.\n *\n * @param logger - Custom logger implementation or null to disable logging\n * @throws {ComputeError} `INVALID_CONFIG` if the logger is missing any of\n * the four required methods — failing here beats a confusing\n * \"getLogger().debug is not a function\" at some later, unrelated call site.\n *\n * @example\n * ```typescript\n * import { setLogger } from '@selvajs/compute/core';\n *\n * // Enable console logging\n * setLogger(console);\n *\n * // Use a custom logger\n * setLogger({\n * debug: (msg, ...args) => myLogger.debug(msg, ...args),\n * info: (msg, ...args) => myLogger.info(msg, ...args),\n * warn: (msg, ...args) => myLogger.warn(msg, ...args),\n * error: (msg, ...args) => myLogger.error(msg, ...args)\n * });\n *\n * // Disable logging\n * setLogger(null);\n * ```\n */\nexport function setLogger(logger: Logger | Console | null): void {\n\tif (logger === null) {\n\t\tinternalLogger = new NoOpLogger();\n\t\treturn;\n\t}\n\n\tconst missing = (['debug', 'info', 'warn', 'error'] as const).filter(\n\t\t(method) => typeof (logger as unknown as Record<string, unknown>)[method] !== 'function'\n\t);\n\tif (missing.length > 0) {\n\t\tthrow new ComputeError(\n\t\t\t`Logger is missing required method(s): ${missing.join(', ')}. A logger must implement debug, info, warn and error.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { missingMethods: missing } }\n\t\t);\n\t}\n\n\tinternalLogger = logger as Logger;\n}\n\n/**\n * Enable debug logging to console\n *\n * @public Convenience method to enable console logging.\n *\n * @example\n * ```typescript\n * import { enableDebugLogging } from '@selvajs/compute/core';\n *\n * enableDebugLogging();\n * ```\n */\nexport function enableDebugLogging(): void {\n\tsetLogger(new ConsoleLogger());\n}\n","/**\n * Case-insensitive single-key reader for wire payloads.\n *\n * The Rhino Compute family serializes the same logical field with different\n * casing depending on the server branch:\n *\n * - mcneel 8.x / 9.x: the IO schema is PascalCase (`ParamType`, `Default`,\n * `InnerTree`, …) because those C# classes carry no `[JsonProperty]`.\n * - VektorNode Compute8: the IO schema is camelCase (`paramType`, `default`,\n * …) because the fork added `[JsonProperty(\"camelCase\")]`, BUT the nested\n * `default` DataTree wrapper stays PascalCase (`ParamName` / `InnerTree`)\n * since `Resthopper.IO.DataTree` is an external type the fork can't attribute.\n *\n * So a single response can mix casings, and which casing a given field uses\n * depends on the server branch. Rather than deep-camelCasing the whole payload\n * (the old `camelcaseKeys` approach — which corrupted user-authored value-list\n * label keys and item `data` JSON), read the specific fields we care about\n * case-insensitively and leave everything else verbatim.\n *\n * Prefers an exact-case match when present, then falls back to the first\n * case-insensitive match. Returns `undefined` when no key matches.\n *\n * @param obj - The source object (any non-object input yields `undefined`).\n * @param name - The logical field name, in any casing.\n */\nexport function readField<T = unknown>(obj: unknown, name: string): T | undefined {\n\tif (!obj || typeof obj !== 'object') return undefined;\n\n\tconst record = obj as Record<string, unknown>;\n\t// Own-property check to avoid prototype chain lookups (see lowerKeyMap for full explanation).\n\tif (hasOwn(record, name)) return record[name] as T;\n\n\tconst key = lowerKeyMap(record).get(name.toLowerCase());\n\treturn key === undefined ? undefined : (record[key] as T);\n}\n\nconst hasOwn = (record: object, key: string): boolean =>\n\tObject.prototype.hasOwnProperty.call(record, key);\n\n/**\n * True when `obj` has a key matching `name` (case-insensitively). Distinguishes\n * \"field present but value is null/undefined\" from \"field absent\" — needed where\n * presence itself carries meaning (e.g. an `innerTree` that exists but is empty).\n */\nexport function hasField(obj: unknown, name: string): boolean {\n\tif (!obj || typeof obj !== 'object') return false;\n\tconst record = obj as Record<string, unknown>;\n\t// Own-property check only.\n\tif (hasOwn(record, name)) return true;\n\treturn lowerKeyMap(record).has(name.toLowerCase());\n}\n\n/**\n * Per-object cache of lowercased key → actual key. Invalidated by key-count changes.\n * Known limitation: key replacement without count change goes undetected (acceptable\n * since this mutation pattern doesn't occur in this codebase).\n */\nconst lowerKeyCache = new WeakMap<object, { keyCount: number; map: Map<string, string> }>();\n\nfunction lowerKeyMap(record: Record<string, unknown>): Map<string, string> {\n\tconst keys = Object.keys(record);\n\tconst cached = lowerKeyCache.get(record);\n\tif (cached && cached.keyCount === keys.length) return cached.map;\n\n\tconst map = new Map<string, string>();\n\tfor (const key of keys) {\n\t\tconst lower = key.toLowerCase();\n\t\tif (!map.has(lower)) map.set(lower, key);\n\t}\n\tlowerKeyCache.set(record, { keyCount: keys.length, map });\n\treturn map;\n}\n","import { ComputeError, ErrorCodes } from '@/core/errors';\n\n/**\n * The public McNeel endpoint's host — the default blocked host; users must point at\n * their own server. Compared against the parsed hostname lowercased and with any\n * trailing dot stripped, so the FQDN form (`compute.rhino3d.com.`) can't bypass it.\n * Known limitation: the endpoint's raw IP is not blocked — it sits behind a load\n * balancer with no single stable, verifiable address to pin.\n */\nexport const DEFAULT_BLOCKED_HOST = 'compute.rhino3d.com';\n\nexport interface ValidateServerUrlOptions {\n\t/**\n\t * Hostnames rejected as a `serverUrl` — a backend's shared public endpoint,\n\t * which callers must not point at. Defaults to `[DEFAULT_BLOCKED_HOST]`; pass\n\t * `[]` to block nothing. Compared lowercased with any trailing dot stripped.\n\t */\n\tblockedHosts?: readonly string[];\n}\n\n/**\n * Validate and normalize a compute `serverUrl`.\n *\n * This is the single source of truth for \"is this a usable server URL?\" — both\n * `GrasshopperClient` (via `normalizeComputeConfig`) and the standalone-exported\n * `ComputeServerStats` constructor delegate here, so a given URL is accepted or\n * rejected identically no matter which entry point a caller uses.\n *\n * Rules (all enforced, on the *trimmed* input — the trimmed form is what's\n * returned, so no stray whitespace survives into later `fetch` calls):\n * - non-empty (after trim)\n * - `http://` or `https://` scheme (case-insensitive, per RFC 3986)\n * - parseable by `new URL()`\n * - no embedded credentials (`http://user:pass@host`) — `fetch`/`new Request`\n * reject credentialed URLs at runtime, so they must fail here instead\n * - no query string or fragment — endpoint paths are appended to this URL\n * (`${serverUrl}/version`), which a `?…` or `#…` suffix would corrupt\n * - not a blocked host (by default the public McNeel endpoint) — compared by\n * parsed hostname (lowercased, trailing dot stripped), so scheme, casing, port,\n * path, trailing-slash, or FQDN-dot variants can't slip past the block\n *\n * @param raw - The candidate server URL.\n * @param options - Override the blocked-host list for a non-Rhino backend.\n * @returns The trimmed, normalized URL with any trailing slashes removed.\n * @throws {ComputeError} `INVALID_CONFIG` if any rule fails.\n */\nexport function validateServerUrl(raw: string, options?: ValidateServerUrlOptions): string {\n\tconst trimmed = raw?.trim() ?? '';\n\tif (!trimmed) {\n\t\tthrow new ComputeError('serverUrl is required', ErrorCodes.INVALID_CONFIG, {\n\t\t\tcontext: { receivedServerUrl: raw }\n\t\t});\n\t}\n\n\tif (!/^https?:\\/\\//i.test(trimmed)) {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must start with \"http://\" or \"https://\". ` +\n\t\t\t\t`For example: \"http://localhost:5000\" or \"https://example.com\"`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(trimmed);\n\t} catch (err) {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must be a valid URL. ` +\n\t\t\t\t`Received error: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{\n\t\t\t\tcontext: { receivedServerUrl: raw },\n\t\t\t\toriginalError: err instanceof Error ? err : undefined\n\t\t\t}\n\t\t);\n\t}\n\n\tif (parsed.username !== '' || parsed.password !== '') {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must not embed credentials (user:pass@host) — ` +\n\t\t\t\t`fetch rejects credentialed URLs at request time. Pass the API key separately.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\t// String check rather than parsed.search/hash: a bare trailing \"?\" or \"#\"\n\t// parses to an empty search/hash but still corrupts endpoint concatenation.\n\tif (trimmed.includes('?') || trimmed.includes('#')) {\n\t\tthrow new ComputeError(\n\t\t\t`Invalid serverUrl: \"${trimmed}\". Must not contain a query string or fragment — ` +\n\t\t\t\t`endpoint paths are appended to this URL (e.g. \"\\${serverUrl}/version\"), ` +\n\t\t\t\t`and a \"?\" or \"#\" suffix would corrupt every request path.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\t// Lowercase + strip any trailing dot so the FQDN form can't bypass the block.\n\tconst hostname = parsed.hostname.toLowerCase().replace(/\\.+$/, '');\n\tconst blocked = (options?.blockedHosts ?? [DEFAULT_BLOCKED_HOST]).map((h) =>\n\t\th.toLowerCase().replace(/\\.+$/, '')\n\t);\n\tif (blocked.includes(hostname)) {\n\t\tthrow new ComputeError(\n\t\t\t'serverUrl must be set to your Compute server URL. The shared public endpoint is not allowed.',\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { receivedServerUrl: raw } }\n\t\t);\n\t}\n\n\treturn trimmed.replace(/\\/+$/, '');\n}\n","import { getLogger } from '../utils/logger';\n\nimport type { ComputeAuth, ComputeConfig } from '../types';\n\nexport function buildUrl(endpoint: string, serverUrl: string): string {\n\tconst base = serverUrl.replace(/\\/+$/, '');\n\tconst path = endpoint.replace(/^\\/+/, '');\n\treturn `${base}/${path}`;\n}\n\nexport function isLocalhost(serverUrl: string): boolean {\n\ttry {\n\t\t// `hostname` (not `host`) strips the port; IPv6 hostnames keep their\n\t\t// brackets, so `http://[::1]:6500` yields `[::1]`.\n\t\tconst hostname = new URL(serverUrl).hostname.toLowerCase();\n\t\treturn hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';\n\t} catch {\n\t\treturn /(localhost|127\\.0\\.0\\.1|\\[::1\\])/i.test(serverUrl);\n\t}\n}\n\n/** Server URLs already warned about missing auth — warn once per server, not per request. */\nconst warnedNoAuth = new Set<string>();\n\n/** Header name rhino.compute reads the API key from. Override via `ComputeConfig.apiKeyHeader`. */\nexport const DEFAULT_API_KEY_HEADER = 'RhinoComputeKey';\n\n/**\n * Caller headers plus credentials. Every request to the server builds its headers here,\n * so a liveness probe authenticates exactly like the solve it gates.\n */\nexport function buildAuthHeaders(auth: ComputeAuth): Record<string, string> {\n\treturn {\n\t\t// Caller headers first so the credentials below OVERWRITE them.\n\t\t...auth.headers,\n\t\t...(auth.authToken && { Authorization: auth.authToken }),\n\t\t...(auth.apiKey && { [auth.apiKeyHeader ?? DEFAULT_API_KEY_HEADER]: auth.apiKey })\n\t};\n}\n\nexport function buildHeaders(requestId: string, config: ComputeConfig): HeadersInit {\n\tconst headers: HeadersInit = {\n\t\t...buildAuthHeaders(config),\n\t\t'X-Request-ID': requestId,\n\t\t'Content-Type': 'application/json'\n\t};\n\n\tif (\n\t\t!config.apiKey &&\n\t\t!config.authToken &&\n\t\t!warnedNoAuth.has(config.serverUrl) &&\n\t\t!isLocalhost(config.serverUrl)\n\t) {\n\t\twarnedNoAuth.add(config.serverUrl);\n\t\tgetLogger().warn(\n\t\t\t`⚠️ [Compute] Request [${requestId}] targets remote server (${config.serverUrl}) but no API key or auth token is configured. Requests may fail or be rate-limited. (warned once per server)`\n\t\t);\n\t}\n\n\treturn headers;\n}\n\nexport function generateRequestId(): string {\n\treturn `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\n}\n","/**\n * Why a liveness probe failed, and whether waiting can change the answer.\n *\n * `retryable` is the whole point: a powered-off VM and a booting one both read\n * as \"offline\" from a single probe, but only one of them will ever come up. A\n * caller that can't tell them apart has to assume the optimistic case and burn\n * its full retry window on a machine that is simply off.\n */\nexport type ProbeVerdict =\n\t/** Nothing is listening on the port — the host answered, and said no. */\n\t| 'refused'\n\t/** The hostname does not resolve. */\n\t| 'dns'\n\t/** No answer within the timeout: a booting host swallows packets rather than refusing them. */\n\t| 'timeout'\n\t/** The server answered, but rejected the probe's credentials (401/403). */\n\t| 'unauthorized'\n\t/** The server answered with a non-2xx that isn't an auth rejection. */\n\t| 'http_error'\n\t/** Connection failed in a way we can't attribute. */\n\t| 'unknown';\n\nexport interface ProbeFailure {\n\tverdict: ProbeVerdict;\n\t/** Whether retrying the same probe could plausibly succeed later. */\n\tretryable: boolean;\n\t/** Operator-facing sentence. Never includes the API key. */\n\tsummary: string;\n}\n\n/**\n * `ECONNREFUSED` arrives spelled differently depending on the runtime and how\n * many layers wrapped it: Node puts the code on the error, undici nests it under\n * `cause`, and browsers collapse everything into an opaque \"Failed to fetch\".\n * Matching the stringified probe error covers all of them without the caller\n * having to normalize first.\n */\nconst REFUSED_PATTERN = /ECONNREFUSED|ERR_CONNECTION_REFUSED|connection refused/i;\nconst DNS_PATTERN = /ENOTFOUND|EAI_AGAIN|getaddrinfo|ERR_NAME_NOT_RESOLVED|dns/i;\nconst TIMEOUT_PATTERN =\n\t/TimeoutError|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|ENETUNREACH|abort|timed? ?out/i;\n\n/**\n * Classify a {@link ComputeServerStats.probeServer} result.\n *\n * A DNS failure is treated as retryable even though a missing record won't fix\n * itself: `EAI_AGAIN` is a *timeout* talking to the resolver, and the two are\n * not distinguishable from the error alone. Retrying a genuinely-wrong hostname\n * costs one window; giving up on a resolver hiccup breaks a server that is fine.\n *\n * @param probe - The `{ online, status?, error? }` a probe returned. An `online`\n * probe is not a failure and yields `null`.\n */\nexport function classifyProbeFailure(probe: {\n\tonline: boolean;\n\tstatus?: number;\n\terror?: string;\n}): ProbeFailure | null {\n\tif (probe.online) return null;\n\n\tif (probe.status !== undefined) {\n\t\tif (probe.status === 401 || probe.status === 403) {\n\t\t\treturn {\n\t\t\t\tverdict: 'unauthorized',\n\t\t\t\tretryable: false,\n\t\t\t\tsummary: `The server rejected the liveness probe with HTTP ${probe.status} — check the API key.`\n\t\t\t};\n\t\t}\n\t\t// 5xx from a proxy in front of a starting child does clear on its own;\n\t\t// a 4xx means we asked for something this server will never serve.\n\t\tconst retryable = probe.status >= 500;\n\t\treturn {\n\t\t\tverdict: 'http_error',\n\t\t\tretryable,\n\t\t\tsummary: retryable\n\t\t\t\t? `The server answered HTTP ${probe.status} — it may still be starting up.`\n\t\t\t\t: `The server answered HTTP ${probe.status}, which will not change on retry.`\n\t\t};\n\t}\n\n\tconst error = probe.error ?? '';\n\n\tif (REFUSED_PATTERN.test(error)) {\n\t\treturn {\n\t\t\tverdict: 'refused',\n\t\t\tretryable: false,\n\t\t\tsummary:\n\t\t\t\t'Connection refused — the host is reachable but nothing is listening on that port. ' +\n\t\t\t\t'Rhino.Compute is not running, or the URL names the wrong port.'\n\t\t};\n\t}\n\n\tif (DNS_PATTERN.test(error)) {\n\t\treturn {\n\t\t\tverdict: 'dns',\n\t\t\tretryable: true,\n\t\t\tsummary: 'The server hostname could not be resolved — check the URL and DNS.'\n\t\t};\n\t}\n\n\tif (TIMEOUT_PATTERN.test(error)) {\n\t\treturn {\n\t\t\tverdict: 'timeout',\n\t\t\tretryable: true,\n\t\t\tsummary:\n\t\t\t\t'No response before the timeout — the machine may be powered off, blocked by a ' +\n\t\t\t\t'firewall, or still booting.'\n\t\t};\n\t}\n\n\treturn {\n\t\tverdict: 'unknown',\n\t\tretryable: true,\n\t\tsummary: error ? `The connection failed: ${error}` : 'The connection failed.'\n\t};\n}\n","import { ComputeError, ErrorCodes } from '../errors';\n\n/** Node's `Buffer` when present (faster path), else `undefined` in browsers/workers. */\nfunction getNodeBuffer(): typeof Buffer | undefined {\n\tconst buf = (globalThis as { Buffer?: typeof Buffer }).Buffer;\n\treturn typeof buf === 'function' ? buf : undefined;\n}\n\n/**\n * Encodes a string to base64 (Node 20+ safe)\n *\n * @internal\n * @param str - String to encode\n * @returns Base64 encoded string\n */\nexport function encodeStringToBase64(str: string): string {\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\treturn Buffer.from(str, 'utf-8').toString('base64');\n\t}\n\t// Browser/worker fallback: UTF-8 encode, then reuse the byte-array encoder.\n\treturn base64ByteArray(new TextEncoder().encode(str));\n}\n\n/**\n * Checks if a string is syntactically valid base64 (strict form: length a multiple of 4).\n * Note: this is validity checking, not detection — `\"test\"` is valid base64 here.\n * To detect whether an untyped string should be treated as base64 content, use {@link detectBase64Payload}.\n *\n * @internal\n * @param str - String to check\n * @returns True if the string is valid base64\n */\nexport function isBase64(str: string): boolean {\n\tif (!str || str.length < 2) return false;\n\t// Length must be a multiple of 4, only alphabet chars + at most 2 trailing '='\n\tif (str.length % 4 !== 0) return false;\n\treturn /^[A-Za-z0-9+/]+={0,2}$/.test(str);\n}\n\n/**\n * Minimum base64 length to treat bare strings as base64 content (excludes padding).\n * 64 chars ≈ 48 decoded bytes — high enough to avoid false positives on human strings.\n *\n * @internal\n */\nexport const BASE64_DETECT_MIN_LENGTH = 64;\n\n/** Lookup for a base64 character's 6-bit value; -1 when not in the alphabet. */\nconst B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\n/**\n * Detects whether an untyped string is base64-encoded content.\n * Uses normalization, length checks, and canonical round-trip validation.\n * Perfect detection is impossible — pass `Uint8Array` instead if exact detection is required.\n *\n * @internal\n * @param str - String to inspect\n * @returns Canonical base64 form when confident, else `null` (treat as plain text)\n */\nexport function detectBase64Payload(str: string): string | null {\n\tif (!str) return null;\n\t// Forgiving-base64 normalization, mirroring decodeBase64ToBinary: strip\n\t// ASCII whitespace, then drop legal trailing padding.\n\tlet data = str.replace(/[\\t\\n\\f\\r ]/g, '');\n\tif (data.length % 4 === 0) data = data.replace(/={1,2}$/, '');\n\n\tconst rem = data.length % 4;\n\tif (rem === 1) return null; // no base64 encoding produces this length\n\tif (data.length < BASE64_DETECT_MIN_LENGTH) return null;\n\tif (!/^[A-Za-z0-9+/]*$/.test(data)) return null;\n\n\t// Canonical round-trip check without decoding: the final character's\n\t// unused low bits must be zero, otherwise re-encoding the decoded bytes\n\t// would not reproduce the input (i.e. it wasn't produced by an encoder).\n\tconst lastValue = B64_ALPHABET.indexOf(data.charAt(data.length - 1));\n\tif (rem === 2 && (lastValue & 0x0f) !== 0) return null;\n\tif (rem === 3 && (lastValue & 0x03) !== 0) return null;\n\n\treturn rem === 0 ? data : data + '=='.slice(0, 4 - rem);\n}\n\n/**\n * Decodes a base64 string to binary data (Uint8Array).\n * Normalizes and validates input per WHATWG forgiving-base64 so both runtimes fail consistently.\n *\n * @param base64File - Base64 encoded string\n * @returns Decoded binary data as Uint8Array\n * @throws {ComputeError} `ENCODING_ERROR` if invalid, or `ENVIRONMENT_ERROR` if the runtime has no decoder\n */\nexport function decodeBase64ToBinary(base64File: string): Uint8Array {\n\t// Forgiving-base64 normalization: strip ASCII whitespace (wrapped /\n\t// pretty-printed payloads), then drop trailing padding only where the spec\n\t// allows it (total length a multiple of 4).\n\tlet data = base64File.replace(/[\\t\\n\\f\\r ]/g, '');\n\tif (data.length % 4 === 0) data = data.replace(/={1,2}$/, '');\n\tif (data.length % 4 === 1 || !/^[A-Za-z0-9+/]*$/.test(data)) {\n\t\tthrow new ComputeError('Invalid base64 input.', ErrorCodes.ENCODING_ERROR, {\n\t\t\tcontext: { inputLength: base64File.length }\n\t\t});\n\t}\n\n\t// Prefer Buffer in Node — it's faster and avoids the latin-1 string detour\n\t// that atob + charCodeAt requires.\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\t// Copy the bytes out of the Buffer: small Buffer.from results are views\n\t\t// over Node's shared 8 KiB pool slab, so returning a view would retain\n\t\t// the whole slab and expose unrelated pooled bytes to any consumer that\n\t\t// touches `.buffer` (re-wrapping, structuredClone, postMessage transfer).\n\t\t// `new Uint8Array(typedArray)` copies into a fresh, exactly-sized buffer.\n\t\treturn new Uint8Array(Buffer.from(data, 'base64'));\n\t}\n\tif (typeof globalThis.atob === 'function') {\n\t\tconst binary = globalThis.atob(data);\n\t\tconst bytes = new Uint8Array(binary.length);\n\t\tfor (let i = 0; i < binary.length; i++) {\n\t\t\tbytes[i] = binary.charCodeAt(i) & 0xff;\n\t\t}\n\t\treturn bytes;\n\t}\n\n\tthrow new ComputeError(\n\t\t'Base64 decoding not supported in this environment.',\n\t\tErrorCodes.ENVIRONMENT_ERROR,\n\t\t{ context: { environmentInfo: 'atob or Buffer not available' } }\n\t);\n}\n\n/**\n * UTF-8 byte length of a string without allocating an encoded copy.\n * Avoids doubling memory on large strings that `TextEncoder.encode` would require.\n *\n * @internal\n */\nexport function utf8ByteLength(str: string): number {\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\treturn Buffer.byteLength(str, 'utf-8');\n\t}\n\tlet bytes = 0;\n\tfor (let i = 0; i < str.length; i++) {\n\t\tconst code = str.charCodeAt(i);\n\t\tif (code < 0x80) {\n\t\t\tbytes += 1;\n\t\t} else if (code < 0x800) {\n\t\t\tbytes += 2;\n\t\t} else if (\n\t\t\tcode >= 0xd800 &&\n\t\t\tcode <= 0xdbff &&\n\t\t\ti + 1 < str.length &&\n\t\t\t(str.charCodeAt(i + 1) & 0xfc00) === 0xdc00\n\t\t) {\n\t\t\t// Surrogate pair → one 4-byte code point; lone surrogates fall through\n\t\t\t// to 3 bytes (the replacement-character encoding TextEncoder emits).\n\t\t\tbytes += 4;\n\t\t\ti++;\n\t\t} else {\n\t\t\tbytes += 3;\n\t\t}\n\t}\n\treturn bytes;\n}\n\n/**\n * Encodes binary data (Uint8Array) to base64 string.\n * Uses Node's `Buffer` when available, falls back to `btoa` in browsers/workers.\n *\n * @internal\n */\nexport function base64ByteArray(bytes: Uint8Array): string {\n\tconst Buffer = getNodeBuffer();\n\tif (Buffer) {\n\t\treturn Buffer.from(bytes).toString('base64');\n\t}\n\tif (typeof globalThis.btoa === 'function') {\n\t\t// Encode chunk-by-chunk and join the base64 pieces, instead of building\n\t\t// one full-input latin-1 string and btoa-ing it (which peaked at ~3×\n\t\t// input memory). The chunk size MUST be a multiple of 3 bytes so every\n\t\t// non-final chunk encodes to whole base64 quanta with no padding —\n\t\t// concatenating the pieces is then byte-for-byte identical to encoding\n\t\t// the whole input at once. 32766 = 3 × 10922, and stays well under the\n\t\t// fromCharCode.apply argument-count limit.\n\t\tconst CHUNK = 32766;\n\t\tconst parts: string[] = [];\n\t\tfor (let i = 0; i < bytes.length; i += CHUNK) {\n\t\t\t// A Uint8Array subarray is array-like, so pass it straight to\n\t\t\t// fromCharCode.apply — no need to copy it into a plain Array first.\n\t\t\tparts.push(\n\t\t\t\tglobalThis.btoa(\n\t\t\t\t\tString.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK) as unknown as number[])\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\treturn parts.join('');\n\t}\n\tthrow new ComputeError(\n\t\t'Base64 encoding not supported in this environment.',\n\t\tErrorCodes.ENVIRONMENT_ERROR,\n\t\t{ context: { environmentInfo: 'btoa or Buffer not available' } }\n\t);\n}\n","import { getLogger } from '../utils/logger';\n\n/**\n * Debug-gated log. Every transport module routes through this rather than calling the logger\n * directly, so `config.debug` is the single switch for the whole request path.\n */\nexport function log(message: string, debug?: boolean): void {\n\tif (debug) getLogger().debug(message);\n}\n","import { log } from './log';\n\nimport type { ServerTiming } from '../types';\n\n/**\n * Parse a `Server-Timing` header value into typed durations (ms).\n *\n * Header grammar (RFC 9110 §10.1.10), as emitted by the solve endpoint:\n * `decode;dur=3, solve;dur=120, encode;dur=8`\n *\n * Returns null when the header is absent or carries no recognizable metric, so\n * the caller can skip the callback entirely.\n *\n * @internal exported for tests\n */\nexport function parseServerTiming(headerValue: string | null): ServerTiming | null {\n\tif (!headerValue) return null;\n\tconst timing: ServerTiming = { raw: headerValue };\n\tlet sawMetric = false;\n\tfor (const part of headerValue.split(',')) {\n\t\tconst [name, ...params] = part.trim().split(';');\n\t\tconst durParam = params.find((p) => p.trim().toLowerCase().startsWith('dur'));\n\t\tif (!durParam) continue;\n\t\tconst dur = Number(durParam.split('=')[1]);\n\t\tif (!Number.isFinite(dur)) continue;\n\t\tconst key = name.trim().toLowerCase();\n\t\tif (key === 'decode' || key === 'solve' || key === 'encode') {\n\t\t\ttiming[key] = dur;\n\t\t\tsawMetric = true;\n\t\t}\n\t}\n\treturn sawMetric ? timing : null;\n}\n\n/**\n * Surface the server's per-request timing breakdown (if it sent one and a\n * caller is listening). Best-effort: a throwing callback must not fail the\n * request. Called on the success path AND the 500-with-values partial-success\n * path — solves that completed with Grasshopper errors carry real timings too.\n */\nexport function fireServerTiming(\n\tresponse: Response,\n\trequestId: string,\n\tonServerTiming: ((timing: ServerTiming, requestId: string) => void) | undefined,\n\tdebug?: boolean\n): void {\n\tif (!onServerTiming) return;\n\tconst timing = parseServerTiming(response.headers.get('Server-Timing'));\n\tif (!timing) return;\n\ttry {\n\t\tonServerTiming(timing, requestId);\n\t} catch (err) {\n\t\tif (debug) log(` onServerTiming callback threw: ${err}`, true);\n\t}\n}\n","/**\n * Side-channel carrying a response's wire size (its JSON text length) alongside\n * the parsed object, so downstream caches can budget by bytes without\n * re-serializing (audit C2/C3 — the response tree can be hundreds of MB, and\n * every extra `JSON.stringify` pass over it is a real cost).\n *\n * A `WeakMap` rather than a property on the response: the response type is the\n * server's schema, and an extra enumerable field would leak into every\n * `JSON.stringify(response)` on the way back out to clients. Derived copies\n * (e.g. the `algo`-stripped shallow copy in `runSolve`) must re-register — the\n * hint follows object identity, not content.\n *\n * The size is `text.length` (UTF-16 code units), not strict UTF-8 bytes —\n * compute responses are ASCII-dominated JSON (base64 + numerals), so the two\n * are interchangeable for budgeting purposes and `.length` is free.\n */\n\nconst wireSizes = new WeakMap<object, number>();\n\n/** Record `response`'s wire size. No-op for non-object/null values. */\nexport function setResponseWireSize(response: unknown, size: number): void {\n\tif (typeof response !== 'object' || response === null) return;\n\twireSizes.set(response, size);\n}\n\n/** The wire size recorded for `response`, or undefined when never registered. */\nexport function getResponseWireSize(response: unknown): number | undefined {\n\tif (typeof response !== 'object' || response === null) return undefined;\n\treturn wireSizes.get(response);\n}\n","import { ComputeError, ErrorCodes, type ErrorCode } from '../errors';\nimport { readField } from '../utils/read-field';\nimport { log } from './log';\nimport { fireServerTiming } from './server-timing';\nimport { setResponseWireSize } from './wire-size';\n\nimport type { ServerErrorCodeMap, ServerTiming } from '../types';\n\n/** Upper bound for the raw server body stored on error `context.responseBody`. */\nconst MAX_CONTEXT_BODY_CHARS = 4096;\n\n/** Truncate a server body for storage on error context — bounded, with an honest marker. */\nfunction truncateBody(body: string): string {\n\tif (body.length <= MAX_CONTEXT_BODY_CHARS) return body;\n\treturn `${body.slice(0, MAX_CONTEXT_BODY_CHARS)}… [truncated ${body.length - MAX_CONTEXT_BODY_CHARS} chars]`;\n}\n\nexport function throwHttpError(\n\tresponse: Response,\n\tfullUrl: string,\n\trequestId: string,\n\trequestSize: number,\n\tserverUrl: string,\n\terrorBody: string,\n\tserverCode?: string,\n\trawBody?: string,\n\tserverErrorCodes?: ServerErrorCodeMap\n): never {\n\tconst { status, statusText } = response;\n\n\tconst responseHeaders: Record<string, string> = {};\n\tresponse.headers.forEach((value, key) => {\n\t\tresponseHeaders[key] = value;\n\t});\n\n\tconst trimmed = errorBody.trim();\n\tconst bodyHint = trimmed ? ` — ${trimmed.slice(0, 200)}${trimmed.length > 200 ? '…' : ''}` : '';\n\n\t// context.responseBody holds the RAW server body (what actually came over the\n\t// wire), bounded to MAX_CONTEXT_BODY_CHARS so a huge body isn't pinned for the\n\t// error's lifetime. `errorBody` may have been rewritten into a synthesized\n\t// message (500 exception shape) — that goes in the message, not the context.\n\tconst storedBody = rawBody ?? errorBody;\n\tconst context = {\n\t\turl: fullUrl,\n\t\trequestId,\n\t\tmethod: 'POST',\n\t\trequestSize,\n\t\tserverUrl,\n\t\tresponseBody: storedBody ? truncateBody(storedBody) : undefined,\n\t\tresponseHeaders\n\t};\n\n\tconst errorMap: Record<number, { message: string; code: ErrorCode }> = {\n\t\t400: {\n\t\t\tmessage: `Bad request: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.VALIDATION_ERROR\n\t\t},\n\t\t401: { message: `HTTP ${status}: ${statusText}${bodyHint}`, code: ErrorCodes.AUTH_ERROR },\n\t\t403: { message: `HTTP ${status}: ${statusText}${bodyHint}`, code: ErrorCodes.AUTH_ERROR },\n\t\t404: { message: `Endpoint not found: ${fullUrl}`, code: ErrorCodes.NOT_FOUND },\n\t\t413: {\n\t\t\tmessage: `Request too large: ${(requestSize / 1024).toFixed(2)}KB`,\n\t\t\tcode: ErrorCodes.VALIDATION_ERROR\n\t\t},\n\t\t429: { message: `Rate limit exceeded${bodyHint}`, code: ErrorCodes.RATE_LIMIT },\n\t\t500: { message: `Server error: ${statusText}${bodyHint}`, code: ErrorCodes.COMPUTATION_ERROR },\n\t\t502: {\n\t\t\tmessage: `Bad gateway: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.NETWORK_ERROR\n\t\t},\n\t\t503: {\n\t\t\tmessage: `Service unavailable: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.NETWORK_ERROR\n\t\t},\n\t\t504: {\n\t\t\tmessage: `Service unavailable: ${statusText}${bodyHint}`,\n\t\t\tcode: ErrorCodes.NETWORK_ERROR\n\t\t}\n\t};\n\n\tconst error = errorMap[status] || {\n\t\tmessage: `HTTP ${status}: ${statusText}${bodyHint}`,\n\t\tcode: ErrorCodes.UNKNOWN_ERROR\n\t};\n\n\t// A machine code in the server's error body outranks the status-based mapping:\n\t// it's stable across the server's production message-scrubbing, where the human\n\t// message is replaced with a generic string. Keep the status-derived message\n\t// for context.\n\tconst code = mapServerErrorCode(serverCode, serverErrorCodes) ?? error.code;\n\n\tthrow new ComputeError(error.message, code, { statusCode: status, context });\n}\n\n/**\n * Map a server-supplied error code (from the JSON error body's `code` field) to\n * one of our {@link ErrorCodes}. Returns `undefined` for an absent or unknown\n * code so the caller falls back to its status-based mapping.\n *\n * Which wire codes exist is backend-specific, so the table comes from the caller\n * ({@link ComputeConfig.serverErrorCodes}) — core does not know any of them.\n */\nexport function mapServerErrorCode(\n\tserverCode?: string,\n\tserverErrorCodes?: ServerErrorCodeMap\n): ErrorCode | undefined {\n\tif (!serverCode || !serverErrorCodes) return undefined;\n\treturn serverErrorCodes[serverCode];\n}\n\nexport async function handleResponse(\n\tresponse: Response,\n\tfullUrl: string,\n\trequestId: string,\n\trequestSize: number,\n\tserverUrl: string,\n\tstartTime: number,\n\tdebug?: boolean,\n\tonServerTiming?: (timing: ServerTiming, requestId: string) => void,\n\tserverErrorCodes?: ServerErrorCodeMap\n): Promise<any> {\n\tconst responseTime = Math.round(performance.now() - startTime);\n\n\tif (!response.ok) {\n\t\t// Read body once and reuse. `rawBody` stays what came over the wire (stored\n\t\t// on error context); `errorBody` may be rewritten into a friendlier message.\n\t\tconst rawBody = await response.text();\n\t\tlet errorBody = rawBody;\n\n\t\t// Enhanced logging for errors\n\t\tif (debug) {\n\t\t\tlog(\n\t\t\t\t`❌ Request [${requestId}] failed with HTTP ${response.status} in ${responseTime}ms`,\n\t\t\t\ttrue\n\t\t\t);\n\t\t\tlog(` URL: ${fullUrl}`, true);\n\t\t\tlog(` Status: ${response.status} ${response.statusText}`, true);\n\t\t\tif (errorBody) {\n\t\t\t\tlog(\n\t\t\t\t\t` Response body: ${errorBody.substring(0, 500)}${errorBody.length > 500 ? '...' : ''}`,\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// A machine-readable code the server may tag onto its error body. Unlike the\n\t\t// human `message`, it isn't scrubbed in the server's production (non-debug)\n\t\t// mode, so it's the reliable signal for classifying the error (see\n\t\t// throwHttpError → mapServerErrorCode). It can ride any error status, not\n\t\t// just 500, so read it here.\n\t\tlet serverCode: string | undefined;\n\n\t\ttry {\n\t\t\tconst parsedForCode = JSON.parse(errorBody);\n\t\t\tif (typeof parsedForCode?.code === 'string') serverCode = parsedForCode.code;\n\t\t} catch {\n\t\t\t// Non-JSON body — nothing to extract.\n\t\t}\n\n\t\t// Check if it's a valid compute response with errors/warnings\n\t\tif (response.status === 500) {\n\t\t\ttry {\n\t\t\t\tconst parsed = JSON.parse(errorBody);\n\t\t\t\t// If it has values, it's a partial success with errors. Read the fields\n\t\t\t\t// case-insensitively: `values`/`errors`/`warnings` arrive PascalCase from\n\t\t\t\t// stock mcneel servers — a casing miss here would throw the partial\n\t\t\t\t// values away as a hard failure.\n\t\t\t\tconst values = readField(parsed, 'values');\n\t\t\t\tconst solveErrors = readField<unknown[]>(parsed, 'errors');\n\t\t\t\tconst solveWarnings = readField<unknown[]>(parsed, 'warnings');\n\t\t\t\tif (values && (solveErrors || solveWarnings)) {\n\t\t\t\t\tsetResponseWireSize(parsed, errorBody.length);\n\t\t\t\t\tif (debug) {\n\t\t\t\t\t\tlog(\n\t\t\t\t\t\t\t`⚠️ Request [${requestId}] completed with solver errors in ${responseTime}ms`,\n\t\t\t\t\t\t\ttrue\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (solveErrors && solveErrors.length > 0) {\n\t\t\t\t\t\t\tlog(` Errors: ${JSON.stringify(solveErrors, null, 2)}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (solveWarnings && solveWarnings.length > 0) {\n\t\t\t\t\t\t\tlog(` Warnings: ${JSON.stringify(solveWarnings, null, 2)}`, true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tfireServerTiming(response, requestId, onServerTiming, debug);\n\t\t\t\t\treturn parsed;\n\t\t\t\t}\n\n\t\t\t\t// Raw server-side exception. The Compute8 server's exception handler\n\t\t\t\t// (compute.geometry Startup.cs) emits:\n\t\t\t\t// { error: \"Internal Server Error\", message: \"<category>: <detail>\",\n\t\t\t\t// stackTrace?: string[] } // stackTrace only when Config.Debug\n\t\t\t\t// The actionable part is `message` — surface it, with the optional\n\t\t\t\t// stack appended for debugging. We prefer `message`/`error` (current\n\t\t\t\t// server) and keep `Message`/`ExceptionType`/`StackTrace` (old\n\t\t\t\t// PascalCase .NET shape) as a back-compat fallback so an older server\n\t\t\t\t// still produces a useful message.\n\t\t\t\tconst serverMessage =\n\t\t\t\t\t(typeof parsed?.message === 'string' && parsed.message) ||\n\t\t\t\t\t(typeof parsed?.Message === 'string' && parsed.Message) ||\n\t\t\t\t\t'';\n\t\t\t\tconst exceptionType =\n\t\t\t\t\t(typeof parsed?.ExceptionType === 'string' && parsed.ExceptionType) || '';\n\t\t\t\tconst stack = parsed?.stackTrace ?? parsed?.StackTrace;\n\t\t\t\tconst stackStr = Array.isArray(stack) ? stack.join('\\n') : stack || '';\n\n\t\t\t\tif (serverMessage) {\n\t\t\t\t\t// Don't repeat the generic \"Internal Server Error\" label when the\n\t\t\t\t\t// message already carries the real detail.\n\t\t\t\t\tconst prefix = exceptionType ? `${exceptionType}: ` : '';\n\t\t\t\t\terrorBody = `${prefix}${serverMessage}${stackStr ? `\\n${stackStr}` : ''}`;\n\t\t\t\t} else if (parsed?.error) {\n\t\t\t\t\terrorBody =\n\t\t\t\t\t\ttypeof parsed.error === 'string' ? parsed.error : JSON.stringify(parsed.error, null, 2);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tif (debug) {\n\t\t\t\t\tlog(` Failed to parse error body as JSON: ${e}`, true);\n\t\t\t\t}\n\t\t\t\t// Not valid JSON, proceed with HTTP error\n\t\t\t}\n\t\t}\n\n\t\tthrowHttpError(\n\t\t\tresponse,\n\t\t\tfullUrl,\n\t\t\trequestId,\n\t\t\trequestSize,\n\t\t\tserverUrl,\n\t\t\terrorBody,\n\t\t\tserverCode,\n\t\t\trawBody,\n\t\t\tserverErrorCodes\n\t\t);\n\t}\n\n\tlog(`✅ Request [${requestId}] completed in ${responseTime}ms`, debug);\n\n\tfireServerTiming(response, requestId, onServerTiming, debug);\n\n\ttry {\n\t\t// text-then-parse (what `response.json()` does internally) so the body's\n\t\t// wire size rides along with the parsed object — downstream byte-budgeted\n\t\t// caches read it instead of re-serializing a potentially huge tree.\n\t\tconst rawBody = await response.text();\n\t\tconst parsed = JSON.parse(rawBody);\n\t\tsetResponseWireSize(parsed, rawBody.length);\n\t\treturn parsed;\n\t} catch (error) {\n\t\t// The signal stays armed while the body streams: an abort or timeout here must reach the\n\t\t// caller's abort/timeout classification, not read as a truncated (retryable) body.\n\t\tif (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) {\n\t\t\tthrow error;\n\t\t}\n\t\t// Classify by the declared Content-Type (issue 87). A 2xx that DECLARES a\n\t\t// non-JSON body (HTML from a captive portal, a reverse-proxy login page, a\n\t\t// misconfigured endpoint) is deterministic — refetching returns the same\n\t\t// page — so fail immediately with INVALID_RESPONSE (never retried). A body\n\t\t// that fails to parse under a JSON (or absent) Content-Type means the\n\t\t// stream was likely cut mid-body — as transient as any network error — and\n\t\t// keeps the retryable NETWORK_ERROR classification.\n\t\tconst contentType = (response.headers.get('Content-Type') ?? '').toLowerCase();\n\t\tconst declaredNonJson = contentType !== '' && !contentType.includes('json');\n\t\tif (declaredNonJson) {\n\t\t\tthrow new ComputeError(\n\t\t\t\t`Server returned a non-JSON response (Content-Type: ${contentType}) — check the server URL / proxy configuration`,\n\t\t\t\tErrorCodes.INVALID_RESPONSE,\n\t\t\t\t{\n\t\t\t\t\tstatusCode: response.status,\n\t\t\t\t\tcontext: { url: fullUrl, requestId, contentType },\n\t\t\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\tthrow new ComputeError('Failed to parse JSON response', ErrorCodes.NETWORK_ERROR, {\n\t\t\tstatusCode: response.status,\n\t\t\tcontext: {\n\t\t\t\turl: fullUrl,\n\t\t\t\trequestId\n\t\t\t},\n\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t});\n\t}\n}\n","import type { RetryPolicy } from '../types';\n\nconst DEFAULT_RETRY: Required<RetryPolicy> = {\n\tattempts: 0,\n\tbaseDelayMs: 500,\n\tmaxDelayMs: 30_000,\n\tretryOn429: true\n};\n\nexport const RETRYABLE_STATUS = new Set([502, 503, 504]);\n\n/**\n * Absolute ceiling for a server-supplied `Retry-After` wait. The server's\n * stated window wins over `retryPolicy.maxDelayMs` (retrying earlier all but\n * guarantees another 429), but a bad/hostile header must not park the client\n * for minutes — anything above this cap is clamped.\n */\nexport const RETRY_AFTER_CAP_MS = 60_000;\n\nexport function resolveRetryPolicy(policy: RetryPolicy | undefined): Required<RetryPolicy> {\n\tif (!policy) return DEFAULT_RETRY;\n\treturn {\n\t\tattempts: policy.attempts ?? DEFAULT_RETRY.attempts,\n\t\tbaseDelayMs: policy.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,\n\t\tmaxDelayMs: policy.maxDelayMs ?? DEFAULT_RETRY.maxDelayMs,\n\t\tretryOn429: policy.retryOn429 ?? DEFAULT_RETRY.retryOn429\n\t};\n}\n\n/**\n * Parse a Retry-After header value (seconds-int or HTTP-date) into ms.\n * Returns null if the header is missing or unparseable.\n */\nexport function parseRetryAfter(headerValue: string | null): number | null {\n\tif (!headerValue) return null;\n\tconst seconds = Number(headerValue);\n\tif (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;\n\tconst dateMs = Date.parse(headerValue);\n\tif (Number.isFinite(dateMs)) {\n\t\tconst delta = dateMs - Date.now();\n\t\treturn delta > 0 ? delta : 0;\n\t}\n\treturn null;\n}\n\nexport function backoffDelay(attempt: number, policy: Required<RetryPolicy>): number {\n\tconst exponential = policy.baseDelayMs * Math.pow(2, attempt);\n\tconst jitter = Math.random() * policy.baseDelayMs;\n\treturn Math.min(exponential + jitter, policy.maxDelayMs);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new DOMException('Aborted', 'AbortError'));\n\t\t\treturn;\n\t\t}\n\t\tconst id = setTimeout(() => {\n\t\t\tsignal?.removeEventListener('abort', onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(id);\n\t\t\treject(new DOMException('Aborted', 'AbortError'));\n\t\t};\n\t\tsignal?.addEventListener('abort', onAbort, { once: true });\n\t});\n}\n","/**\n * Compose a caller-supplied AbortSignal with an optional timeout. Returns a\n * combined signal, or `undefined` if neither was given.\n *\n * Uses `AbortSignal.timeout` (not setTimeout) so the timer is not throttled\n * when the tab is hidden. Falls back to a manual timer for older runtimes.\n *\n * @internal exported for tests\n */\nexport function composeSignal(\n\tcallerSignal: AbortSignal | undefined,\n\ttimeoutMs: number | undefined\n): { signal: AbortSignal | undefined; cleanup: () => void } {\n\tconst noCleanup = () => {};\n\tconst wantsTimeout = typeof timeoutMs === 'number' && timeoutMs > 0;\n\n\tif (!callerSignal && !wantsTimeout) return { signal: undefined, cleanup: noCleanup };\n\tif (callerSignal && !wantsTimeout) return { signal: callerSignal, cleanup: noCleanup };\n\n\tconst supportsTimeout =\n\t\ttypeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function';\n\n\t// Timeout only: nothing is registered on a caller signal, so there is nothing to clean up on\n\t// the modern path (the pending timer is small and self-expires at timeoutMs).\n\tif (!callerSignal) {\n\t\tif (supportsTimeout) return { signal: AbortSignal.timeout(timeoutMs!), cleanup: noCleanup };\n\t\tconst ctrl = new AbortController();\n\t\tconst id = setTimeout(() => ctrl.abort(), timeoutMs);\n\t\treturn { signal: ctrl.signal, cleanup: () => clearTimeout(id) };\n\t}\n\n\t// Caller signal + timeout: composed manually rather than with AbortSignal.any — `any` offers no\n\t// way to unregister its dependent link on the caller's signal, so an app reusing one long-lived\n\t// signal across many solves accumulates a registration per attempt for the full timeoutMs after\n\t// each response (and forever on Node versions with the known AbortSignal.any leak).\n\tconst ctrl = new AbortController();\n\n\tlet timeoutSignal: AbortSignal;\n\tlet timerId: ReturnType<typeof setTimeout> | undefined;\n\tif (supportsTimeout) {\n\t\ttimeoutSignal = AbortSignal.timeout(timeoutMs!);\n\t} else {\n\t\tconst timeoutCtrl = new AbortController();\n\t\ttimerId = setTimeout(() => timeoutCtrl.abort(), timeoutMs);\n\t\ttimeoutSignal = timeoutCtrl.signal;\n\t}\n\n\tconst sources = [callerSignal, timeoutSignal];\n\t// Forward the source's reason so fetch rejects with the right error name\n\t// ('TimeoutError' vs 'AbortError'), matching AbortSignal.any semantics.\n\tconst onAbort = function (this: AbortSignal) {\n\t\tctrl.abort(this.reason);\n\t};\n\n\tfor (const s of sources) {\n\t\tif (s.aborted) {\n\t\t\tctrl.abort(s.reason);\n\t\t\tbreak;\n\t\t}\n\t\ts.addEventListener('abort', onAbort, { once: true });\n\t}\n\n\treturn {\n\t\tsignal: ctrl.signal,\n\t\tcleanup: () => {\n\t\t\tif (timerId !== undefined) clearTimeout(timerId);\n\t\t\tfor (const s of sources) s.removeEventListener('abort', onAbort);\n\t\t}\n\t};\n}\n","import { ComputeError, ErrorCodes } from '../errors';\nimport { utf8ByteLength } from '../utils/encoding';\nimport { log } from './log';\nimport { buildUrl, buildHeaders, generateRequestId } from './request';\nimport { handleResponse } from './response';\nimport {\n\tRETRYABLE_STATUS,\n\tRETRY_AFTER_CAP_MS,\n\tbackoffDelay,\n\tparseRetryAfter,\n\tresolveRetryPolicy,\n\tsleep\n} from './retry';\nimport { composeSignal } from './signal';\n\nimport type { ComputeConfig, RetryPolicy } from '../types';\n\ninterface AttemptContext {\n\tendpoint: string;\n\tbody: string;\n\trequestSize: number;\n\tfullUrl: string;\n\trequestId: string;\n\theaders: HeadersInit;\n\tconfig: ComputeConfig;\n}\n\ninterface AttemptResult {\n\tok: true;\n\tvalue: any;\n}\n\ninterface AttemptRetry {\n\tok: false;\n\tretry: true;\n\tdelayMs: number;\n\tcause: ComputeError;\n}\n\ninterface AttemptFatal {\n\tok: false;\n\tretry: false;\n\tcause: ComputeError;\n}\n\nasync function attemptFetch(\n\tctx: AttemptContext,\n\tretryPolicy: Required<RetryPolicy>,\n\tattempt: number,\n\ttotalAttempts: number\n): Promise<AttemptResult | AttemptRetry | AttemptFatal> {\n\tconst { signal, cleanup } = composeSignal(ctx.config.signal, ctx.config.timeoutMs);\n\tconst startTime = performance.now();\n\n\ttry {\n\t\tconst response = await fetch(ctx.fullUrl, {\n\t\t\tmethod: 'POST',\n\t\t\tbody: ctx.body,\n\t\t\theaders: ctx.headers,\n\t\t\tsignal\n\t\t});\n\n\t\t// 429 with Retry-After or retryable 5xx → maybe retry\n\t\tconst isRetryableStatus =\n\t\t\tRETRYABLE_STATUS.has(response.status) || (retryPolicy.retryOn429 && response.status === 429);\n\n\t\tif (isRetryableStatus && attempt < totalAttempts - 1) {\n\t\t\tconst retryAfterMs = parseRetryAfter(response.headers.get('Retry-After'));\n\t\t\t// The server's stated Retry-After window wins over maxDelayMs — retrying\n\t\t\t// before it all but guarantees another 429 and burns an attempt. It is\n\t\t\t// only clamped by the absolute RETRY_AFTER_CAP_MS safety cap (or by a\n\t\t\t// caller-configured maxDelayMs that is even larger), so a bad header\n\t\t\t// can't force a pathological sleep. backoffDelay already clamps itself.\n\t\t\tconst delayMs =\n\t\t\t\tretryAfterMs !== null\n\t\t\t\t\t? Math.min(retryAfterMs, Math.max(retryPolicy.maxDelayMs, RETRY_AFTER_CAP_MS))\n\t\t\t\t\t: backoffDelay(attempt, retryPolicy);\n\t\t\t// Drain the body so the connection can be reused on the next attempt.\n\t\t\t// On the *final* attempt we deliberately fall through — handleResponse\n\t\t\t// reads the body itself to surface the error context.\n\t\t\tawait response.text().catch(() => {});\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\tretry: true,\n\t\t\t\tdelayMs,\n\t\t\t\tcause: new ComputeError(\n\t\t\t\t\t`HTTP ${response.status} ${response.statusText} (will retry)`,\n\t\t\t\t\tresponse.status === 429 ? ErrorCodes.RATE_LIMIT : ErrorCodes.NETWORK_ERROR,\n\t\t\t\t\t{ statusCode: response.status, context: { requestId: ctx.requestId } }\n\t\t\t\t)\n\t\t\t};\n\t\t}\n\n\t\tconst value = await handleResponse(\n\t\t\tresponse,\n\t\t\tctx.fullUrl,\n\t\t\tctx.requestId,\n\t\t\tctx.requestSize,\n\t\t\tctx.config.serverUrl,\n\t\t\tstartTime,\n\t\t\tctx.config.debug,\n\t\t\tctx.config.onServerTiming,\n\t\t\tctx.config.serverErrorCodes\n\t\t);\n\t\treturn { ok: true, value };\n\t} catch (error) {\n\t\t// Caller-aborted vs timeout-aborted distinction\n\t\tif (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) {\n\t\t\tconst callerAborted = ctx.config.signal?.aborted === true;\n\n\t\t\tif (callerAborted) {\n\t\t\t\t// Caller cancellation is never retried — propagate immediately\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: false,\n\t\t\t\t\tcause: new ComputeError('Request aborted by caller', ErrorCodes.ABORTED, {\n\t\t\t\t\t\tcontext: {\n\t\t\t\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\t\t\t\trequestId: ctx.requestId,\n\t\t\t\t\t\t\trequestSize: ctx.requestSize\n\t\t\t\t\t\t},\n\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t})\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Only claim a timeout when we actually armed one (issue 88). Without a\n\t\t\t// configured timeoutMs, a non-caller abort came from the runtime itself\n\t\t\t// (e.g. an undici socket teardown) — as transient as any network drop, so\n\t\t\t// it stays retryable, but it is NOT a timeout: report it truthfully\n\t\t\t// instead of \"timed out after undefinedms\" / TIMEOUT_ERROR.\n\t\t\tconst timeoutArmed = typeof ctx.config.timeoutMs === 'number' && ctx.config.timeoutMs > 0;\n\t\t\tconst fatal = timeoutArmed\n\t\t\t\t? new ComputeError(\n\t\t\t\t\t\t`Request timed out after ${ctx.config.timeoutMs}ms`,\n\t\t\t\t\t\tErrorCodes.TIMEOUT_ERROR,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcontext: {\n\t\t\t\t\t\t\t\tserverUrl: ctx.config.serverUrl,\n\t\t\t\t\t\t\t\ttimeoutMs: ctx.config.timeoutMs,\n\t\t\t\t\t\t\t\turl: ctx.fullUrl,\n\t\t\t\t\t\t\t\trequestId: ctx.requestId,\n\t\t\t\t\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\t\t\t\t\trequestSize: ctx.requestSize\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t\t}\n\t\t\t\t\t)\n\t\t\t\t: new ComputeError(\n\t\t\t\t\t\t`Request aborted by the runtime (${error.name}): ${error.message}`,\n\t\t\t\t\t\tErrorCodes.NETWORK_ERROR,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcontext: {\n\t\t\t\t\t\t\t\tserverUrl: ctx.config.serverUrl,\n\t\t\t\t\t\t\t\turl: ctx.fullUrl,\n\t\t\t\t\t\t\t\trequestId: ctx.requestId,\n\t\t\t\t\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\t\t\t\t\trequestSize: ctx.requestSize\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t\t}\n\t\t\t\t\t);\n\t\t\tif (attempt < totalAttempts - 1) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: true,\n\t\t\t\t\tdelayMs: backoffDelay(attempt, retryPolicy),\n\t\t\t\t\tcause: fatal\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { ok: false, retry: false, cause: fatal };\n\t\t}\n\n\t\t// Network error (TypeError) — retryable (issue 90).\n\t\t//\n\t\t// Duplicate-POST caveat: a connection reset can strike after the body was\n\t\t// sent, in which case the server may already have executed this POST and a\n\t\t// retry runs it again. Compute solves are deterministic, so a duplicate is\n\t\t// wasted work rather than corruption; RetryPolicy documents the risk and\n\t\t// defaults to attempts: 0.\n\t\t//\n\t\t// In a real browser, a fetch TypeError with no response is most often a\n\t\t// CORS misconfiguration (the browser hides the details by design), so it is\n\t\t// classified CORS_ERROR there to stop callers chasing phantom network\n\t\t// failures. Retries are kept in both environments: the same TypeError is\n\t\t// also what a flaky/offline network produces, and there is no way to tell\n\t\t// them apart.\n\t\tif (error instanceof TypeError) {\n\t\t\tconst inBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';\n\t\t\tconst errorContext = {\n\t\t\t\tserverUrl: ctx.config.serverUrl,\n\t\t\t\turl: ctx.fullUrl,\n\t\t\t\trequestId: ctx.requestId,\n\t\t\t\tendpoint: ctx.endpoint,\n\t\t\t\trequestSize: ctx.requestSize\n\t\t\t};\n\t\t\tconst fatal = inBrowser\n\t\t\t\t? new ComputeError(\n\t\t\t\t\t\t`Request failed: ${error.message} — in a browser this usually means a CORS misconfiguration on the server (or a network failure; the browser does not distinguish them)`,\n\t\t\t\t\t\tErrorCodes.CORS_ERROR,\n\t\t\t\t\t\t{ context: errorContext, originalError: error }\n\t\t\t\t\t)\n\t\t\t\t: new ComputeError(`Network error: ${error.message}`, ErrorCodes.NETWORK_ERROR, {\n\t\t\t\t\t\tcontext: errorContext,\n\t\t\t\t\t\toriginalError: error\n\t\t\t\t\t});\n\t\t\tif (attempt < totalAttempts - 1) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: true,\n\t\t\t\t\tdelayMs: backoffDelay(attempt, retryPolicy),\n\t\t\t\t\tcause: fatal\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { ok: false, retry: false, cause: fatal };\n\t\t}\n\t\t// ComputeError thrown from handleResponse — already has full context.\n\t\t// Retryable only if it carries a retryable status code.\n\t\tif (error instanceof ComputeError) {\n\t\t\tconst status = error.statusCode;\n\t\t\t// A 2xx whose body failed to parse UNDER A JSON CONTENT-TYPE\n\t\t\t// (NETWORK_ERROR from handleResponse) means the stream was cut mid-body —\n\t\t\t// as transient as any network error. A 2xx that declared a non-JSON body\n\t\t\t// arrives here as INVALID_RESPONSE (deterministic — captive portal /\n\t\t\t// login page) and deliberately does NOT match: it is never retried.\n\t\t\tconst isTruncatedSuccess =\n\t\t\t\terror.code === ErrorCodes.NETWORK_ERROR &&\n\t\t\t\tstatus !== undefined &&\n\t\t\t\tstatus >= 200 &&\n\t\t\t\tstatus < 300;\n\t\t\tconst retryable =\n\t\t\t\tisTruncatedSuccess ||\n\t\t\t\t(status !== undefined &&\n\t\t\t\t\t(RETRYABLE_STATUS.has(status) || (retryPolicy.retryOn429 && status === 429)));\n\t\t\tif (retryable && attempt < totalAttempts - 1) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tretry: true,\n\t\t\t\t\tdelayMs: backoffDelay(attempt, retryPolicy),\n\t\t\t\t\tcause: error\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { ok: false, retry: false, cause: error };\n\t\t}\n\n\t\t// Unknown — wrap and don't retry\n\t\treturn {\n\t\t\tok: false,\n\t\t\tretry: false,\n\t\t\tcause: new ComputeError(\n\t\t\t\terror instanceof Error ? error.message : String(error),\n\t\t\t\tErrorCodes.UNKNOWN_ERROR,\n\t\t\t\t{\n\t\t\t\t\tcontext: { endpoint: ctx.endpoint, requestId: ctx.requestId },\n\t\t\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t\t\t}\n\t\t\t)\n\t\t};\n\t} finally {\n\t\tcleanup();\n\t}\n}\n\n/**\n * Generic Rhino Compute fetch function.\n * Sends a POST request to any Compute endpoint with pre-prepared arguments.\n *\n * Use this for advanced, low-level control over compute requests. For most use cases, prefer higher-level APIs.\n *\n * The transport is response-type-agnostic: it does not know which response a\n * given endpoint returns. Callers supply the response type via `R` (defaulting\n * to `unknown`, which forces an explicit narrowing before use).\n *\n * Timeout semantics: `config.timeoutMs` is a PER-ATTEMPT timeout, re-armed for\n * every retry. With `retry: { attempts: N }` the worst-case wall clock is\n * `(N + 1) × timeoutMs` plus backoff sleeps (and a server `Retry-After` can\n * stretch a single sleep up to 60s). For a hard overall deadline, pass\n * `config.signal` (e.g. `AbortSignal.timeout(totalMs)`) — a caller abort wins\n * immediately, including during backoff.\n *\n * Retry caveats: requests are POSTs, so a retry after a mid-flight connection\n * loss may re-execute a request the server already ran (see {@link RetryPolicy}).\n * A 2xx response that declares a non-JSON `Content-Type` (captive portal,\n * proxy login page) fails immediately with `INVALID_RESPONSE` and is never\n * retried; a body that fails to parse under a JSON content-type is treated as\n * a truncated stream and is retried.\n *\n * @typeParam R - The expected response shape. The caller names it at the call site.\n * @param endpoint - The Compute API endpoint (e.g., 'grasshopper', 'io', 'mesh').\n * @param args - Pre-prepared arguments for the request body.\n * @param config - Compute configuration (server URL, API key, timeout, debug, retry, signal).\n * @returns The parsed JSON response from the server, typed as `R`.\n *\n * @example\n * // Basic usage for the Grasshopper endpoint:\n * const response = await fetchCompute(\n * 'grasshopper',\n * { ... },\n * {\n * serverUrl: 'https://my-server.com',\n * debug: true,\n * timeoutMs: 30_000,\n * retry: { attempts: 2 },\n * signal: controller.signal,\n * }\n * );\n */\nexport async function fetchCompute<R = unknown>(\n\tendpoint: string,\n\targs: Record<string, any>,\n\tconfig: ComputeConfig\n): Promise<R> {\n\tconst requestId = generateRequestId();\n\t// A circular or BigInt-containing payload makes JSON.stringify throw a raw\n\t// TypeError; surface it as the INVALID_INPUT ComputeError this function's\n\t// contract promises, before anything touches the network.\n\tlet body: string;\n\ttry {\n\t\tbody = JSON.stringify(args);\n\t} catch (error) {\n\t\tthrow new ComputeError(\n\t\t\t`Request body is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\tErrorCodes.INVALID_INPUT,\n\t\t\t{\n\t\t\t\tcontext: { endpoint, requestId },\n\t\t\t\toriginalError: error instanceof Error ? error : new Error(String(error))\n\t\t\t}\n\t\t);\n\t}\n\t// Wire size in UTF-8 bytes — `body.length` counts UTF-16 code units and\n\t// undercounts non-ASCII payloads (matters for the 413 message and size logs).\n\tconst requestSize = utf8ByteLength(body);\n\tconst fullUrl = buildUrl(endpoint, config.serverUrl);\n\tconst headers = buildHeaders(requestId, config);\n\tconst retryPolicy = resolveRetryPolicy(config.retry);\n\tconst totalAttempts = retryPolicy.attempts + 1;\n\n\tif (config.debug) {\n\t\tconst sizeKb = (requestSize / 1024).toFixed(2);\n\t\tconst emoji = requestSize > 100000 ? '⚠️' : '🚀';\n\t\tlog(`${emoji} Starting compute request [${requestId}]: ${endpoint} (${sizeKb}KB)`, true);\n\t}\n\n\tconst ctx: AttemptContext = {\n\t\tendpoint,\n\t\tbody,\n\t\trequestSize,\n\t\tfullUrl,\n\t\trequestId,\n\t\theaders,\n\t\tconfig\n\t};\n\n\t// Every iteration ends in `return` or `throw` (issue 105): attemptFetch never\n\t// asks to retry on the final attempt (all its retry branches are gated on\n\t// `attempt < totalAttempts - 1`), so the exhausted-retries error is the final\n\t// attempt's own `result.cause` — there is no post-loop fallback to reach.\n\tfor (let attempt = 0; ; attempt++) {\n\t\tconst result = await attemptFetch(ctx, retryPolicy, attempt, totalAttempts);\n\n\t\tif (result.ok) return result.value as R;\n\n\t\tif (!result.retry) throw result.cause;\n\n\t\tif (config.debug) {\n\t\t\tlog(\n\t\t\t\t`🔁 Request [${requestId}] retrying after ${result.delayMs}ms (attempt ${attempt + 2}/${totalAttempts}): ${result.cause.message}`,\n\t\t\t\ttrue\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tawait sleep(result.delayMs, config.signal);\n\t\t} catch {\n\t\t\t// Caller cancelled during backoff\n\t\t\tthrow new ComputeError('Request aborted by caller', ErrorCodes.ABORTED, {\n\t\t\t\tcontext: { endpoint, requestId, requestSize },\n\t\t\t\toriginalError: result.cause\n\t\t\t});\n\t\t}\n\t}\n}\n","/**\n * By-reference definition form for solves.\n *\n * Lets a caller that already knows a definition's identity (e.g. a stored\n * version's UUID) schedule solves without materializing the multi-MB bytes:\n * cache keys and the server-pointer map are derived from `key` alone, and\n * `load()` is only called when an upload is genuinely unavoidable (first solve\n * of a definition, or a server-side pointer miss).\n */\n\n/**\n * A definition identified by a stable key, with bytes materialized on demand.\n *\n * **Immutability contract — read this before constructing one.** `key` must\n * identify IMMUTABLE bytes: every `load()` for a given `key` must return the\n * same content, forever. All caching (the scheduler's result cache, the\n * server-pointer map, and any durable cache built on these keys) trusts the\n * key as the definition's identity WITHOUT looking at the bytes. If two\n * different byte contents ever share a key, cached solves from one are served\n * for the other — silent cache poisoning with no diagnostic. Use an identity\n * that can never be reused for different content (e.g. a version UUID), never\n * a mutable name or path.\n */\nexport interface DefinitionRef {\n\t/**\n\t * Identity of immutable bytes (e.g. a version UUID). Two different byte\n\t * contents must never share a key — cache poisoning otherwise.\n\t */\n\tkey: string;\n\t/**\n\t * Materialize the bytes. Called ONLY when an upload is unavoidable — inside\n\t * the solve execution, so it counts toward the solve's abort semantics.\n\t */\n\tload: () => Promise<Uint8Array>;\n}\n\n/**\n * Every definition form accepted by the solve entry points:\n * - a URL, base64 string, or plain string (content-hashed for caching)\n * - raw `.gh` bytes (content-hashed)\n * - a {@link DefinitionRef} (identity-keyed; bytes loaded lazily)\n */\nexport type SolveDefinition = string | Uint8Array | DefinitionRef;\n\n/** Narrow a {@link SolveDefinition} to the by-reference form. */\nexport function isDefinitionRef(definition: SolveDefinition): definition is DefinitionRef {\n\treturn (\n\t\ttypeof definition === 'object' &&\n\t\t!(definition instanceof Uint8Array) &&\n\t\ttypeof definition.key === 'string' &&\n\t\ttypeof definition.load === 'function'\n\t);\n}\n","import type { FileData } from './types';\n\n/**\n * The `Sub Folder` convention, as authored on Selva's Grasshopper file components.\n *\n * `::` nests, matching Rhino's layer separator (`ROOT::Panels`), and `/` and `\\` are accepted\n * because people type them out of habit. The plugin normalizes to `/` before sending, so these\n * helpers also cover payloads from an older plugin that still emit `::`.\n *\n * The first segment is the **root**, which names the archive rather than becoming a folder inside\n * it: `ROOT::Panels` downloads as `ROOT.zip` containing `Panels/…`. Files sharing a root travel\n * together; distinct roots produce separate archives.\n */\n\n/**\n * Split a `Sub Folder` value into folder segments; `/`, `\\` and `::` all separate.\n *\n * Exported because anything rendering a folder tree has to agree with the archive on where the\n * boundaries are — splitting on `/` alone leaves `Main::Panels` as one literal segment and shows\n * a folder named after the separator.\n */\nexport const subFolderSegments = (subFolder: string | undefined): string[] =>\n\t(subFolder ?? '')\n\t\t.replace(/::/g, '/')\n\t\t.replace(/\\\\/g, '/')\n\t\t.split('/')\n\t\t.map((segment) => segment.trim())\n\t\t.filter((segment) => segment !== '');\n\n/** First `Sub Folder` segment, or `''` when the file sits at the top level. */\nexport const rootOf = (file: Pick<FileData, 'subFolder'>): string =>\n\tsubFolderSegments(file.subFolder)[0] ?? '';\n\n/**\n * Everything below the root, as a `/`-joined path. The root names the archive, so repeating it\n * inside would nest it twice.\n */\nexport const pathBelowRoot = (file: Pick<FileData, 'subFolder'>): string =>\n\tsubFolderSegments(file.subFolder).slice(1).join('/');\n\n/**\n * Group files by their `Sub Folder` root, preserving encounter order.\n *\n * Rootless files group under `''` — a caller naming archives supplies its own fallback for that\n * bucket. Useful beyond downloading: a consumer writing to disk gets the same grouping without\n * touching the DOM.\n */\nexport const groupFilesByRoot = <T extends Pick<FileData, 'subFolder'>>(\n\tfiles: readonly T[]\n): Array<{ root: string; files: T[] }> => {\n\tconst groups = new Map<string, T[]>();\n\n\tfor (const file of files) {\n\t\tconst root = rootOf(file);\n\t\tconst bucket = groups.get(root);\n\t\tif (bucket) bucket.push(file);\n\t\telse groups.set(root, [file]);\n\t}\n\n\treturn Array.from(groups, ([root, grouped]) => ({ root, files: grouped }));\n};\n\n/**\n * Make a root usable as a download filename: path separators and the characters Windows forbids\n * would otherwise reach the download attribute verbatim and produce a broken or misdirected save.\n */\nexport const toArchiveName = (root: string): string => {\n\tconst safe = root\n\t\t.replace(/[/\\\\:*?\"<>|]/g, '_')\n\t\t.replace(/\\s+/g, ' ')\n\t\t.trim()\n\t\t.replace(/^\\.+/, '');\n\n\treturn safe === '' ? 'files' : safe;\n};\n","import { ComputeError, ErrorCodes } from '@/core/errors';\nimport { getLogger } from '@/core/utils/logger';\nimport { decodeBase64ToBinary } from '@/core/utils/encoding';\nimport { readField } from '@/core/utils/read-field';\n\nimport { FileBaseInfo, FileData, ProcessedFile } from './types';\nimport { groupFilesByRoot, pathBelowRoot, toArchiveName } from './sub-folder';\n\n/**\n * Extracts and processes files from compute response data without downloading\n * them. Never throws: undecodable/unnamed items and failed external fetches are\n * logged and dropped per-file (see {@link fetchRemoteFiles}).\n */\nexport const extractFilesFromComputeResponse = async (\n\tdownloadableFiles: FileData[],\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null = null\n): Promise<ProcessedFile[]> => {\n\treturn processFiles(downloadableFiles, additionalFiles);\n};\n\n/** Downloads files from a compute response as a ZIP archive. */\nexport const downloadFileData = async (\n\tdownloadableFiles: FileData[],\n\tfileFoldername: string,\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null = null\n): Promise<void> => {\n\t// Check if we're in a browser environment\n\tif (typeof document === 'undefined' || typeof Blob === 'undefined') {\n\t\tthrow new ComputeError(\n\t\t\t'File download functionality is only available in browser environments. This function requires the DOM API (document, Blob).',\n\t\t\tErrorCodes.BROWSER_ONLY,\n\t\t\t{\n\t\t\t\tcontext: {\n\t\t\t\t\tenvironment: typeof window !== 'undefined' ? 'browser (SSR)' : 'Node.js',\n\t\t\t\t\tdocumentAvailable: typeof document !== 'undefined',\n\t\t\t\t\tblobAvailable: typeof Blob !== 'undefined'\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t}\n\n\ttry {\n\t\tconst processedFiles = await processFiles(downloadableFiles, additionalFiles);\n\t\tawait createAndDownloadZip(processedFiles, fileFoldername);\n\t} catch (err) {\n\t\t// Re-throw if it's already a ComputeError\n\t\tif (err instanceof ComputeError) {\n\t\t\tthrow err;\n\t\t}\n\t\tthrow new ComputeError(\n\t\t\t'Failed to download files from compute response',\n\t\t\tErrorCodes.INVALID_STATE,\n\t\t\t{\n\t\t\t\tcontext: { originalError: err instanceof Error ? err.message : String(err) },\n\t\t\t\toriginalError: err instanceof Error ? err : undefined\n\t\t\t}\n\t\t);\n\t}\n};\n\n/**\n * Download files as one archive per `Sub Folder` root.\n *\n * `ROOT::Panels` and `OTHERROOT::Panels` produce `ROOT.zip` and `OTHERROOT.zip`, each containing\n * `Panels/…` — the root names the archive instead of nesting inside it. Files with no root fall\n * back to `fallbackName`, so a definition that never sets `Sub Folder` downloads exactly as before.\n *\n * Archives are saved one at a time: browsers discard concurrent downloads issued in the same tick,\n * and these are user-initiated saves rather than a throughput-bound batch. Note that saving more\n * than one file per gesture may prompt for permission.\n *\n * @param downloadableFiles - `FileData` items from the compute response.\n * @param fallbackName - Archive name for files with no `Sub Folder` root.\n * @param additionalFiles - Extra files to package; they carry no root and join the fallback archive.\n */\nexport const downloadFileDataByRoot = async (\n\tdownloadableFiles: FileData[],\n\tfallbackName: string,\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null = null\n): Promise<void> => {\n\tconst groups = groupFilesByRoot(downloadableFiles);\n\n\t// Extras alone still deserve an archive; without this they'd be dropped for having no root.\n\tif (groups.length === 0) {\n\t\tawait downloadFileData([], fallbackName, additionalFiles);\n\t\treturn;\n\t}\n\n\t// Extras belong to the definition rather than to any one root, so they ride with the rootless\n\t// archive — or with the first one when every file is rooted, rather than being dropped.\n\tconst extrasRoot = (groups.find((group) => group.root === '') ?? groups[0]).root;\n\n\tfor (const { root, files } of groups) {\n\t\tawait downloadFileData(\n\t\t\troot === '' ? files : files.map((file) => ({ ...file, subFolder: pathBelowRoot(file) })),\n\t\t\troot === '' ? fallbackName : toArchiveName(root),\n\t\t\troot === extrasRoot ? additionalFiles : null\n\t\t);\n\t}\n};\n\n/**\n * Reduce a server-controlled path field to safe relative segments for use inside the zip\n * (zip-slip defense): backslashes normalize to `/`, and empty, `.`, `..`, and drive-letter\n * segments are dropped so no entry can escape the extraction directory via traversal or an\n * absolute path. Returns '' when nothing safe remains.\n *\n * `::` nests, matching the `Sub Folder` input's Rhino-layer syntax. The plugin already\n * normalizes it, so this is for payloads from an older one — without it those land in a\n * literal folder named `ROOT::Panels`.\n */\nconst sanitizeArchivePath = (raw: string): string =>\n\traw\n\t\t.replace(/::/g, '/')\n\t\t.replace(/\\\\/g, '/')\n\t\t.split('/')\n\t\t.map((segment) => segment.trim())\n\t\t.filter(\n\t\t\t(segment) =>\n\t\t\t\tsegment !== '' && segment !== '.' && segment !== '..' && !/^[a-zA-Z]:$/.test(segment)\n\t\t)\n\t\t.join('/');\n\n/**\n * Lenient read of the `isBase64Encoded` wire flag: some server branches serialize\n * booleans as strings (`\"true\"`/`\"True\"`), which must still count as base64 rather\n * than silently dropping the file (issue 95).\n */\nconst isBase64Flag = (flag: unknown): boolean =>\n\tflag === true || (typeof flag === 'string' && flag.trim().toLowerCase() === 'true');\n\n/**\n * Decode the inline files carried in a compute response into `ProcessedFile`s.\n *\n * Pure and synchronous: base64 items are decoded to binary, plain-text items are\n * passed through, and the archive path is derived from `subFolder` + name.\n * Degrades per-file like {@link fetchRemoteFiles}: an item with no usable `data`\n * or with undecodable base64 is logged and skipped, never aborting the batch.\n * This is the half of file handling that both public entry points share; it\n * never touches the network and never throws.\n *\n * Wire fields are read case-insensitively via {@link readField}: mcneel-branch\n * servers serialize PascalCase (`FileName`, `Data`, `IsBase64Encoded`, …) while\n * the VektorNode fork uses camelCase — both must decode (issue 95).\n *\n * @param dataItems - `FileData` items from the compute response.\n * @returns The decoded files.\n */\nconst decodeResponseFiles = (dataItems: FileData[]): ProcessedFile[] => {\n\tconst processedFiles: ProcessedFile[] = [];\n\n\tdataItems.forEach((item) => {\n\t\tconst rawName = readField<string>(item, 'fileName') ?? '';\n\t\tconst rawType = readField<string>(item, 'fileType') ?? '';\n\t\tconst fileName = sanitizeArchivePath(`${rawName}${rawType}`);\n\t\tif (fileName === '') {\n\t\t\tgetLogger().warn(\n\t\t\t\t`Skipping file with unusable name \"${rawName}${rawType}\": no safe archive path remains after sanitization.`\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst rawSubFolder = readField<string>(item, 'subFolder');\n\t\tconst subFolder = rawSubFolder ? sanitizeArchivePath(rawSubFolder) : '';\n\t\tconst filePath = subFolder !== '' ? `${subFolder}/${fileName}` : fileName;\n\n\t\t// Only a genuinely absent/empty body is unusable — the flag's exact type\n\t\t// must never decide that (issue 95).\n\t\tconst data = readField<unknown>(item, 'data');\n\t\tif (typeof data !== 'string' || data === '') {\n\t\t\tgetLogger().warn(`Skipping file \"${filePath}\": item carries no usable data.`);\n\t\t\treturn;\n\t\t}\n\n\t\t// GH-authored, not interpreted here — passed through so a consumer that\n\t\t// stores files rather than zipping them keeps the authoring context.\n\t\tconst metadata = readField<Record<string, string>>(item, 'metadata');\n\n\t\tif (isBase64Flag(readField<unknown>(item, 'isBase64Encoded'))) {\n\t\t\t// `decodeBase64ToBinary` already returns a correctly-bounded view;\n\t\t\t// re-wrapping `.buffer` would discard its byteOffset/byteLength and\n\t\t\t// expose the whole (possibly pooled) backing buffer as corrupt content.\n\t\t\ttry {\n\t\t\t\tprocessedFiles.push({\n\t\t\t\t\tfileName,\n\t\t\t\t\tcontent: decodeBase64ToBinary(data),\n\t\t\t\t\tpath: filePath,\n\t\t\t\t\tsubFolder,\n\t\t\t\t\t...(metadata ? { metadata } : {})\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tgetLogger().warn(`Skipping file \"${filePath}\": base64 decode failed.`, err);\n\t\t\t}\n\t\t} else {\n\t\t\tprocessedFiles.push({\n\t\t\t\tfileName,\n\t\t\t\tcontent: data,\n\t\t\t\tpath: filePath,\n\t\t\t\tsubFolder,\n\t\t\t\t...(metadata ? { metadata } : {})\n\t\t\t});\n\t\t}\n\t});\n\n\treturn processedFiles;\n};\n\n/** Abort a hung external-file fetch — one dead URL must degrade the batch, not stall it forever. */\nconst REMOTE_FILE_TIMEOUT_MS = 30_000;\n\n/**\n * Fetch externally-referenced files over HTTP into `ProcessedFile`s.\n *\n * Async and fallible by nature. A failed fetch (network error, non-OK status,\n * or timeout after {@link REMOTE_FILE_TIMEOUT_MS}) is logged and that file is\n * dropped — the rest still resolve — so one dead URL degrades the result rather\n * than aborting the whole batch. This swallow is deliberate and pinned by\n * tests; callers receive only the files that succeeded.\n *\n * @param refs - External file references to fetch.\n * @returns The successfully-fetched files (failures omitted).\n */\nconst fetchRemoteFiles = async (refs: FileBaseInfo[]): Promise<ProcessedFile[]> => {\n\tconst fetched = await Promise.all(\n\t\trefs.map(async (file) => {\n\t\t\ttry {\n\t\t\t\tconst signal =\n\t\t\t\t\ttypeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'\n\t\t\t\t\t\t? AbortSignal.timeout(REMOTE_FILE_TIMEOUT_MS)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tconst response = await fetch(file.filePath, { signal });\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tgetLogger().warn(`Failed to fetch additional file from URL: ${file.filePath}`);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\t// One step, no intermediate Blob allocation (issue 111).\n\t\t\t\tconst arrayBuffer = await response.arrayBuffer();\n\t\t\t\t// Same zip-slip defense as decodeResponseFiles — the name lands in the archive.\n\t\t\t\tconst safeName = sanitizeArchivePath(file.fileName);\n\t\t\t\tif (safeName === '') {\n\t\t\t\t\tgetLogger().warn(`Skipping fetched file with unusable name: ${file.fileName}`);\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst subFolder = file.subFolder ? sanitizeArchivePath(file.subFolder) : '';\n\t\t\t\treturn {\n\t\t\t\t\tfileName: safeName,\n\t\t\t\t\tcontent: new Uint8Array(arrayBuffer),\n\t\t\t\t\tpath: subFolder !== '' ? `${subFolder}/${safeName}` : safeName,\n\t\t\t\t\t// No `metadata`: an external URL carries no GH authoring context.\n\t\t\t\t\tsubFolder\n\t\t\t\t} as ProcessedFile;\n\t\t\t} catch (error) {\n\t\t\t\tgetLogger().error(`Error fetching additional file from URL: ${file.filePath}`, error);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t})\n\t);\n\n\treturn fetched.filter((f): f is ProcessedFile => f !== null);\n};\n\n/**\n * Compose the decoded response files with any fetched external files.\n *\n * Archive paths are de-duplicated here — on the path both public entry points\n * share — so `extractFilesFromComputeResponse` consumers keying by `path` never\n * lose files silently, matching the zip path's rename behavior (issue 111).\n *\n * @param dataItems - `FileData` items from the compute response.\n * @param additionalFiles - Optional external file references to fetch and include.\n * @returns A Promise resolving to the combined `ProcessedFile` list.\n */\nconst processFiles = async (\n\tdataItems: FileData[],\n\tadditionalFiles: FileBaseInfo[] | FileBaseInfo | null\n): Promise<ProcessedFile[]> => {\n\tconst processedFiles = decodeResponseFiles(dataItems);\n\n\tif (additionalFiles) {\n\t\tconst filesArray = Array.isArray(additionalFiles) ? additionalFiles : [additionalFiles];\n\t\tprocessedFiles.push(...(await fetchRemoteFiles(filesArray)));\n\t}\n\n\tconst taken = new Set<string>();\n\treturn processedFiles.map((file) => {\n\t\tconst path = uniqueArchivePath(file.path, taken);\n\t\ttaken.add(path);\n\t\tif (path === file.path) return file;\n\t\tgetLogger().warn(`Duplicate archive path \"${file.path}\" — storing as \"${path}\".`);\n\t\treturn { ...file, path, fileName: path.slice(path.lastIndexOf('/') + 1) };\n\t});\n};\n\n/** Creates a ZIP archive from processed files and triggers a browser download. */\nasync function createAndDownloadZip(files: ProcessedFile[], zipName: string): Promise<void> {\n\tconst { zip, strToU8 } = await import('fflate');\n\n\t// Convert files to fflate format. Zip entries are keyed by path, so two\n\t// files with the same path would silently overwrite each other — rename\n\t// collisions (\"model.txt\" → \"model-2.txt\") instead of losing data.\n\t// (processFiles already de-duplicates; this is a cheap second line of\n\t// defense in case callers construct ProcessedFiles themselves.)\n\tconst zipData: Record<string, Uint8Array> = {};\n\tconst taken = new Set<string>();\n\tfiles.forEach((file) => {\n\t\tconst path = uniqueArchivePath(file.path, taken);\n\t\ttaken.add(path);\n\t\tif (path !== file.path) {\n\t\t\tgetLogger().warn(`Duplicate archive path \"${file.path}\" — storing as \"${path}\".`);\n\t\t}\n\t\tzipData[path] = typeof file.content === 'string' ? strToU8(file.content) : file.content;\n\t});\n\n\t// Async `zip` deflates on a worker thread instead of blocking the main thread\n\t// like `zipSync` — keeps the UI responsive for large geometry exports.\n\tconst zipped = await new Promise<Uint8Array>((resolve, reject) => {\n\t\tzip(zipData, { level: 6 }, (err, data) => (err ? reject(err) : resolve(data)));\n\t});\n\n\tconst blob = new Blob([zipped as BlobPart], { type: 'application/zip' });\n\tsaveFile(blob, `${zipName}.zip`);\n}\n\n/**\n * First archive path not already taken in `taken`, disambiguating with a\n * numeric suffix before the extension: `dir/model.txt` → `dir/model-2.txt`.\n */\nfunction uniqueArchivePath(path: string, taken: ReadonlySet<string>): string {\n\tif (!taken.has(path)) return path;\n\tconst slash = path.lastIndexOf('/');\n\tconst dot = path.lastIndexOf('.');\n\tconst stemEnd = dot > slash ? dot : path.length;\n\tconst stem = path.slice(0, stemEnd);\n\tconst ext = path.slice(stemEnd);\n\tfor (let i = 2; ; i++) {\n\t\tconst candidate = `${stem}-${i}${ext}`;\n\t\tif (!taken.has(candidate)) return candidate;\n\t}\n}\n\n/** Saves a Blob object as a file in the user's browser. */\nfunction saveFile(blob: Blob, filename: string) {\n\tif (typeof document === 'undefined') {\n\t\tthrow new ComputeError(\n\t\t\t'saveFile requires a browser environment with DOM API access.',\n\t\t\tErrorCodes.BROWSER_ONLY,\n\t\t\t{\n\t\t\t\tcontext: { function: 'saveFile', requiredAPI: 'document' }\n\t\t\t}\n\t\t);\n\t}\n\n\tconst url = URL.createObjectURL(blob);\n\tconst a = document.createElement('a');\n\ta.href = url;\n\ta.download = filename;\n\t// Firefox requires the anchor to be in the DOM for the click to download.\n\tdocument.body.appendChild(a);\n\ta.click();\n\ta.remove();\n\t// Revoking synchronously can abort the download in some browsers — the\n\t// browser only pins the blob once the download has actually started.\n\tsetTimeout(() => URL.revokeObjectURL(url), 10_000);\n}\n"],"mappings":"AAAA,MAAa,EAAa,CACzB,cAAe,gBACf,WAAY,aACZ,iBAAkB,mBAClB,kBAAmB,oBACnB,cAAe,gBACf,WAAY,aAEZ,UAAW,YAEX,WAAY,aAOZ,iBAAkB,mBAClB,cAAe,gBACf,cAAe,gBACf,cAAe,gBACf,eAAgB,iBAChB,aAAc,eAEd,kBAAmB,oBACnB,eAAgB,iBAEhB,kBAAmB,oBAEnB,WAAY,aAEZ,QAAS,UAOT,WAAY,aAMZ,cAAe,gBAQf,sBAAuB,uBACxB,EASA,IAAa,EAAb,MAAa,UAAqB,KAAM,CACvC,KACA,WACA,QACA,cAEA,YACC,EACA,EAAkB,EAAW,cAC7B,EACC,CACD,MAAM,CAAO,EACb,KAAK,KAAO,eACZ,KAAK,KAAO,EACZ,KAAK,WAAa,GAAS,WAC3B,KAAK,QAAU,GAAS,QACxB,KAAK,cAAgB,GAAS,cAC1B,GAAS,gBACZ,KAA8B,MAAQ,EAAQ,cAEhD,CAKA,OAAO,cACN,EACA,EACA,EACC,CACD,OAAO,IAAI,EACV,UAAU,EAAU,yBAAyB,EAAe,cAAc,EAAa,GAAK,KAC5F,EAAW,cACX,CAAE,QAAS,CAAE,YAAW,eAAc,GAAG,CAAQ,CAAE,CACpD,CACD,CAKA,OAAO,iBACN,EACA,EACA,EACC,CACD,OAAO,IAAI,EAAa,sBAAsB,IAAa,EAAW,iBAAkB,CACvF,QAAS,CAAE,kBAAmB,EAAW,YAAW,GAAG,CAAQ,CAChE,CAAC,CACF,CACD,EC7FM,EAAN,KAAmC,CAClC,OAAc,CAAC,CACf,MAAa,CAAC,CACd,MAAa,CAAC,CACd,OAAc,CAAC,CAChB,EAMM,EAAN,KAAsC,CACrC,MAAM,EAAiB,GAAG,EAAuB,CAChD,QAAQ,MAAM,EAAS,GAAG,CAAI,CAC/B,CAEA,KAAK,EAAiB,GAAG,EAAuB,CAC/C,QAAQ,KAAK,EAAS,GAAG,CAAI,CAC9B,CAEA,KAAK,EAAiB,GAAG,EAAuB,CAC/C,QAAQ,KAAK,EAAS,GAAG,CAAI,CAC9B,CAEA,MAAM,EAAiB,GAAG,EAAuB,CAChD,QAAQ,MAAM,EAAS,GAAG,CAAI,CAC/B,CACD,EAMA,IAAI,EAAyB,IAAI,EAOjC,SAAgB,GAAoB,CACnC,OAAO,CACR,CA+BA,SAAgB,EAAU,EAAuC,CAChE,GAAI,IAAW,KAAM,CACpB,EAAiB,IAAI,EACrB,MACD,CAEA,IAAM,EAAW,CAAC,QAAS,OAAQ,OAAQ,OAAO,CAAC,CAAW,OAC5D,GAAW,OAAQ,EAA8C,IAAY,UAC/E,EACA,GAAI,EAAQ,OAAS,EACpB,MAAM,IAAI,EACT,yCAAyC,EAAQ,KAAK,IAAI,EAAE,wDAC5D,EAAW,eACX,CAAE,QAAS,CAAE,eAAgB,CAAQ,CAAE,CACxC,EAGD,EAAiB,CAClB,CAcA,SAAgB,GAA2B,CAC1C,EAAU,IAAI,CAAe,CAC9B,CCpGA,SAAgB,EAAuB,EAAc,EAA6B,CACjF,GAAI,CAAC,GAAO,OAAO,GAAQ,SAAU,OAErC,IAAM,EAAS,EAEf,GAAI,EAAO,EAAQ,CAAI,EAAG,OAAO,EAAO,GAExC,IAAM,EAAM,EAAY,CAAM,CAAC,CAAC,IAAI,EAAK,YAAY,CAAC,EACtD,OAAO,IAAQ,IAAA,GAAY,IAAA,GAAa,EAAO,EAChD,CAEA,MAAM,GAAU,EAAgB,IAC/B,OAAO,UAAU,eAAe,KAAK,EAAQ,CAAG,EAOjD,SAAgB,EAAS,EAAc,EAAuB,CAC7D,GAAI,CAAC,GAAO,OAAO,GAAQ,SAAU,MAAO,GAC5C,IAAM,EAAS,EAGf,OADI,EAAO,EAAQ,CAAI,EAAU,GAC1B,EAAY,CAAM,CAAC,CAAC,IAAI,EAAK,YAAY,CAAC,CAClD,CAOA,MAAM,EAAgB,IAAI,QAE1B,SAAS,EAAY,EAAsD,CAC1E,IAAM,EAAO,OAAO,KAAK,CAAM,EACzB,EAAS,EAAc,IAAI,CAAM,EACvC,GAAI,GAAU,EAAO,WAAa,EAAK,OAAQ,OAAO,EAAO,IAE7D,IAAM,EAAM,IAAI,IAChB,IAAK,IAAM,KAAO,EAAM,CACvB,IAAM,EAAQ,EAAI,YAAY,EACzB,EAAI,IAAI,CAAK,GAAG,EAAI,IAAI,EAAO,CAAG,CACxC,CAEA,OADA,EAAc,IAAI,EAAQ,CAAE,SAAU,EAAK,OAAQ,KAAI,CAAC,EACjD,CACR,CC9DA,MAAa,EAAuB,sBAqCpC,SAAgB,EAAkB,EAAa,EAA4C,CAC1F,IAAM,EAAU,GAAK,KAAK,GAAK,GAC/B,GAAI,CAAC,EACJ,MAAM,IAAI,EAAa,wBAAyB,EAAW,eAAgB,CAC1E,QAAS,CAAE,kBAAmB,CAAI,CACnC,CAAC,EAGF,GAAI,CAAC,gBAAgB,KAAK,CAAO,EAChC,MAAM,IAAI,EACT,uBAAuB,EAAQ,2GAE/B,EAAW,eACX,CAAE,QAAS,CAAE,kBAAmB,CAAI,CAAE,CACvC,EAGD,IAAI,EACJ,GAAI,CACH,EAAS,IAAI,IAAI,CAAO,CACzB,OAAS,EAAK,CACb,MAAM,IAAI,EACT,uBAAuB,EAAQ,0CACX,aAAe,MAAQ,EAAI,QAAU,OAAO,CAAG,IACnE,EAAW,eACX,CACC,QAAS,CAAE,kBAAmB,CAAI,EAClC,cAAe,aAAe,MAAQ,EAAM,IAAA,EAC7C,CACD,CACD,CAEA,GAAI,EAAO,WAAa,IAAM,EAAO,WAAa,GACjD,MAAM,IAAI,EACT,uBAAuB,EAAQ,gIAE/B,EAAW,eACX,CAAE,QAAS,CAAE,kBAAmB,CAAI,CAAE,CACvC,EAKD,GAAI,EAAQ,SAAS,GAAG,GAAK,EAAQ,SAAS,GAAG,EAChD,MAAM,IAAI,EACT,uBAAuB,EAAQ,oLAG/B,EAAW,eACX,CAAE,QAAS,CAAE,kBAAmB,CAAI,CAAE,CACvC,EAID,IAAM,EAAW,EAAO,SAAS,YAAY,CAAC,CAAC,QAAQ,OAAQ,EAAE,EAIjE,IAHiB,GAAS,cAAgB,CAAA,qBAAqB,EAAA,CAAG,IAAK,GACtE,EAAE,YAAY,CAAC,CAAC,QAAQ,OAAQ,EAAE,CAEzB,CAAC,CAAC,SAAS,CAAQ,EAC5B,MAAM,IAAI,EACT,+FACA,EAAW,eACX,CAAE,QAAS,CAAE,kBAAmB,CAAI,CAAE,CACvC,EAGD,OAAO,EAAQ,QAAQ,OAAQ,EAAE,CAClC,CC7GA,SAAgB,EAAS,EAAkB,EAA2B,CAGrE,MAAO,GAFM,EAAU,QAAQ,OAAQ,EAE1B,EAAE,GADF,EAAS,QAAQ,OAAQ,EACjB,GACtB,CAEA,SAAgB,EAAY,EAA4B,CACvD,GAAI,CAGH,IAAM,EAAW,IAAI,IAAI,CAAS,CAAC,CAAC,SAAS,YAAY,EACzD,OAAO,IAAa,aAAe,IAAa,aAAe,IAAa,OAC7E,MAAQ,CACP,MAAO,oCAAoC,KAAK,CAAS,CAC1D,CACD,CAGA,MAAM,EAAe,IAAI,IASzB,SAAgB,EAAiB,EAA2C,CAC3E,MAAO,CAEN,GAAG,EAAK,QACR,GAAI,EAAK,WAAa,CAAE,cAAe,EAAK,SAAU,EACtD,GAAI,EAAK,QAAU,EAAG,EAAK,cAAA,mBAAyC,EAAK,MAAO,CACjF,CACD,CAEA,SAAgB,EAAa,EAAmB,EAAoC,CACnF,IAAM,EAAuB,CAC5B,GAAG,EAAiB,CAAM,EAC1B,eAAgB,EAChB,eAAgB,kBACjB,EAcA,MAXC,CAAC,EAAO,QACR,CAAC,EAAO,WACR,CAAC,EAAa,IAAI,EAAO,SAAS,GAClC,CAAC,EAAY,EAAO,SAAS,IAE7B,EAAa,IAAI,EAAO,SAAS,EACjC,EAAU,CAAC,CAAC,KACX,yBAAyB,EAAU,2BAA2B,EAAO,UAAU,6GAChF,GAGM,CACR,CAEA,SAAgB,IAA4B,CAC3C,MAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,EAAG,EAAE,GACnE,CC3BA,MAAM,EAAkB,0DAClB,EAAc,6DACd,EACL,gFAaD,SAAgB,GAAqB,EAIb,CACvB,GAAI,EAAM,OAAQ,OAAO,KAEzB,GAAI,EAAM,SAAW,IAAA,GAAW,CAC/B,GAAI,EAAM,SAAW,KAAO,EAAM,SAAW,IAC5C,MAAO,CACN,QAAS,eACT,UAAW,GACX,QAAS,oDAAoD,EAAM,OAAO,sBAC3E,EAID,IAAM,EAAY,EAAM,QAAU,IAClC,MAAO,CACN,QAAS,aACT,YACA,QAAS,EACN,4BAA4B,EAAM,OAAO,iCACzC,4BAA4B,EAAM,OAAO,kCAC7C,CACD,CAEA,IAAM,EAAQ,EAAM,OAAS,GA8B7B,OA5BI,EAAgB,KAAK,CAAK,EACtB,CACN,QAAS,UACT,UAAW,GACX,QACC,kJAEF,EAGG,EAAY,KAAK,CAAK,EAClB,CACN,QAAS,MACT,UAAW,GACX,QAAS,oEACV,EAGG,EAAgB,KAAK,CAAK,EACtB,CACN,QAAS,UACT,UAAW,GACX,QACC,2GAEF,EAGM,CACN,QAAS,UACT,UAAW,GACX,QAAS,EAAQ,0BAA0B,IAAU,wBACtD,CACD,CChHA,SAAS,GAA2C,CACnD,IAAM,EAAO,WAA0C,OACvD,OAAO,OAAO,GAAQ,WAAa,EAAM,IAAA,EAC1C,CASA,SAAgB,GAAqB,EAAqB,CACzD,IAAM,EAAS,EAAc,EAK7B,OAJI,EACI,EAAO,KAAK,EAAK,OAAO,CAAC,CAAC,SAAS,QAAQ,EAG5C,EAAgB,IAAI,YAAY,CAAC,CAAC,OAAO,CAAG,CAAC,CACrD,CAsCA,SAAgB,GAAoB,EAA4B,CAC/D,GAAI,CAAC,EAAK,OAAO,KAGjB,IAAI,EAAO,EAAI,QAAQ,eAAgB,EAAE,EACrC,EAAK,OAAS,GAAM,IAAG,EAAO,EAAK,QAAQ,UAAW,EAAE,GAE5D,IAAM,EAAM,EAAK,OAAS,EAG1B,GAFI,IAAQ,GACR,EAAK,OAAA,IACL,CAAC,mBAAmB,KAAK,CAAI,EAAG,OAAO,KAK3C,IAAM,EAAY,mEAAa,QAAQ,EAAK,OAAO,EAAK,OAAS,CAAC,CAAC,EAInE,OAHI,IAAQ,GAAM,EAAY,IAC1B,IAAQ,GAAM,EAAY,EAAoB,KAE3C,IAAQ,EAAI,EAAO,EAAO,KAAK,MAAM,EAAG,EAAI,CAAG,CACvD,CAUA,SAAgB,EAAqB,EAAgC,CAIpE,IAAI,EAAO,EAAW,QAAQ,eAAgB,EAAE,EAEhD,GADI,EAAK,OAAS,GAAM,IAAG,EAAO,EAAK,QAAQ,UAAW,EAAE,GACxD,EAAK,OAAS,GAAM,GAAK,CAAC,mBAAmB,KAAK,CAAI,EACzD,MAAM,IAAI,EAAa,wBAAyB,EAAW,eAAgB,CAC1E,QAAS,CAAE,YAAa,EAAW,MAAO,CAC3C,CAAC,EAKF,IAAM,EAAS,EAAc,EAC7B,GAAI,EAMH,OAAO,IAAI,WAAW,EAAO,KAAK,EAAM,QAAQ,CAAC,EAElD,GAAI,OAAO,WAAW,MAAS,WAAY,CAC1C,IAAM,EAAS,WAAW,KAAK,CAAI,EAC7B,EAAQ,IAAI,WAAW,EAAO,MAAM,EAC1C,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,OAAQ,IAClC,EAAM,GAAK,EAAO,WAAW,CAAC,EAAI,IAEnC,OAAO,CACR,CAEA,MAAM,IAAI,EACT,qDACA,EAAW,kBACX,CAAE,QAAS,CAAE,gBAAiB,8BAA+B,CAAE,CAChE,CACD,CAQA,SAAgB,EAAe,EAAqB,CACnD,IAAM,EAAS,EAAc,EAC7B,GAAI,EACH,OAAO,EAAO,WAAW,EAAK,OAAO,EAEtC,IAAI,EAAQ,EACZ,IAAK,IAAI,EAAI,EAAG,EAAI,EAAI,OAAQ,IAAK,CACpC,IAAM,EAAO,EAAI,WAAW,CAAC,EACzB,EAAO,IACV,GAAS,EACC,EAAO,KACjB,GAAS,EAET,GAAQ,OACR,GAAQ,OACR,EAAI,EAAI,EAAI,SACX,EAAI,WAAW,EAAI,CAAC,EAAI,QAAY,OAIrC,GAAS,EACT,KAEA,GAAS,CAEX,CACA,OAAO,CACR,CAQA,SAAgB,EAAgB,EAA2B,CAC1D,IAAM,EAAS,EAAc,EAC7B,GAAI,EACH,OAAO,EAAO,KAAK,CAAK,CAAC,CAAC,SAAS,QAAQ,EAE5C,GAAI,OAAO,WAAW,MAAS,WAAY,CAQ1C,IAAM,EAAQ,MACR,EAAkB,CAAC,EACzB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,OAAQ,GAAK,EAGtC,EAAM,KACL,WAAW,KACV,OAAO,aAAa,MAAM,KAAM,EAAM,SAAS,EAAG,EAAI,CAAK,CAAwB,CACpF,CACD,EAED,OAAO,EAAM,KAAK,EAAE,CACrB,CACA,MAAM,IAAI,EACT,qDACA,EAAW,kBACX,CAAE,QAAS,CAAE,gBAAiB,8BAA+B,CAAE,CAChE,CACD,CCnMA,SAAgB,EAAI,EAAiB,EAAuB,CACvD,GAAO,EAAU,CAAC,CAAC,MAAM,CAAO,CACrC,CCOA,SAAgB,GAAkB,EAAiD,CAClF,GAAI,CAAC,EAAa,OAAO,KACzB,IAAM,EAAuB,CAAE,IAAK,CAAY,EAC5C,EAAY,GAChB,IAAK,IAAM,KAAQ,EAAY,MAAM,GAAG,EAAG,CAC1C,GAAM,CAAC,EAAM,GAAG,GAAU,EAAK,KAAK,CAAC,CAAC,MAAM,GAAG,EACzC,EAAW,EAAO,KAAM,GAAM,EAAE,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,WAAW,KAAK,CAAC,EAC5E,GAAI,CAAC,EAAU,SACf,IAAM,EAAM,OAAO,EAAS,MAAM,GAAG,CAAC,CAAC,EAAE,EACzC,GAAI,CAAC,OAAO,SAAS,CAAG,EAAG,SAC3B,IAAM,EAAM,EAAK,KAAK,CAAC,CAAC,YAAY,GAChC,IAAQ,UAAY,IAAQ,SAAW,IAAQ,YAClD,EAAO,GAAO,EACd,EAAY,GAEd,CACA,OAAO,EAAY,EAAS,IAC7B,CAQA,SAAgB,EACf,EACA,EACA,EACA,EACO,CACP,GAAI,CAAC,EAAgB,OACrB,IAAM,EAAS,GAAkB,EAAS,QAAQ,IAAI,eAAe,CAAC,EACjE,KACL,GAAI,CACH,EAAe,EAAQ,CAAS,CACjC,OAAS,EAAK,CACT,GAAO,EAAI,qCAAqC,IAAO,EAAI,CAChE,CACD,CCrCA,MAAM,EAAY,IAAI,QAGtB,SAAgB,EAAoB,EAAmB,EAAoB,CACtE,OAAO,GAAa,UAAY,GACpC,EAAU,IAAI,EAAU,CAAI,CAC7B,CAGA,SAAgB,GAAoB,EAAuC,CACtE,UAAO,GAAa,UAAY,EACpC,OAAO,EAAU,IAAI,CAAQ,CAC9B,CCpBA,MAAM,EAAyB,KAG/B,SAAS,EAAa,EAAsB,CAE3C,OADI,EAAK,QAAU,EAA+B,EAC3C,GAAG,EAAK,MAAM,EAAG,CAAsB,EAAE,eAAe,EAAK,OAAS,EAAuB,QACrG,CAEA,SAAgB,EACf,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACQ,CACR,GAAM,CAAE,SAAQ,cAAe,EAEzB,EAA0C,CAAC,EACjD,EAAS,QAAQ,SAAS,EAAO,IAAQ,CACxC,EAAgB,GAAO,CACxB,CAAC,EAED,IAAM,EAAU,EAAU,KAAK,EACzB,EAAW,EAAU,MAAM,EAAQ,MAAM,EAAG,GAAG,IAAI,EAAQ,OAAS,IAAM,IAAM,KAAO,GAMvF,EAAa,GAAW,EACxB,EAAU,CACf,IAAK,EACL,YACA,OAAQ,OACR,cACA,YACA,aAAc,EAAa,EAAa,CAAU,EAAI,IAAA,GACtD,iBACD,EA8BM,EAAQ,CA3Bb,IAAK,CACJ,QAAS,gBAAgB,IAAa,IACtC,KAAM,EAAW,gBAClB,EACA,IAAK,CAAE,QAAS,QAAQ,EAAO,IAAI,IAAa,IAAY,KAAM,EAAW,UAAW,EACxF,IAAK,CAAE,QAAS,QAAQ,EAAO,IAAI,IAAa,IAAY,KAAM,EAAW,UAAW,EACxF,IAAK,CAAE,QAAS,uBAAuB,IAAW,KAAM,EAAW,SAAU,EAC7E,IAAK,CACJ,QAAS,uBAAuB,EAAc,KAAA,CAAM,QAAQ,CAAC,EAAE,IAC/D,KAAM,EAAW,gBAClB,EACA,IAAK,CAAE,QAAS,sBAAsB,IAAY,KAAM,EAAW,UAAW,EAC9E,IAAK,CAAE,QAAS,iBAAiB,IAAa,IAAY,KAAM,EAAW,iBAAkB,EAC7F,IAAK,CACJ,QAAS,gBAAgB,IAAa,IACtC,KAAM,EAAW,aAClB,EACA,IAAK,CACJ,QAAS,wBAAwB,IAAa,IAC9C,KAAM,EAAW,aAClB,EACA,IAAK,CACJ,QAAS,wBAAwB,IAAa,IAC9C,KAAM,EAAW,aAClB,CAGoB,EAAE,IAAW,CACjC,QAAS,QAAQ,EAAO,IAAI,IAAa,IACzC,KAAM,EAAW,aAClB,EAMM,EAAO,EAAmB,EAAY,CAAgB,GAAK,EAAM,KAEvE,MAAM,IAAI,EAAa,EAAM,QAAS,EAAM,CAAE,WAAY,EAAQ,SAAQ,CAAC,CAC5E,CAUA,SAAgB,EACf,EACA,EACwB,CACpB,GAAC,GAAe,EACpB,OAAO,EAAiB,EACzB,CAEA,eAAsB,EACrB,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACe,CACf,IAAM,EAAe,KAAK,MAAM,YAAY,IAAI,EAAI,CAAS,EAE7D,GAAI,CAAC,EAAS,GAAI,CAGjB,IAAM,EAAU,MAAM,EAAS,KAAK,EAChC,EAAY,EAGZ,IACH,EACC,cAAc,EAAU,qBAAqB,EAAS,OAAO,MAAM,EAAa,IAChF,EACD,EACA,EAAI,WAAW,IAAW,EAAI,EAC9B,EAAI,cAAc,EAAS,OAAO,GAAG,EAAS,aAAc,EAAI,EAC5D,GACH,EACC,qBAAqB,EAAU,UAAU,EAAG,GAAG,IAAI,EAAU,OAAS,IAAM,MAAQ,KACpF,EACD,GASF,IAAI,EAEJ,GAAI,CACH,IAAM,EAAgB,KAAK,MAAM,CAAS,EACtC,OAAO,GAAe,MAAS,WAAU,EAAa,EAAc,KACzE,MAAQ,CAER,CAGA,GAAI,EAAS,SAAW,IACvB,GAAI,CACH,IAAM,EAAS,KAAK,MAAM,CAAS,EAK7B,EAAS,EAAU,EAAQ,QAAQ,EACnC,EAAc,EAAqB,EAAQ,QAAQ,EACnD,EAAgB,EAAqB,EAAQ,UAAU,EAC7D,GAAI,IAAW,GAAe,GAe7B,OAdA,EAAoB,EAAQ,EAAU,MAAM,EACxC,IACH,EACC,eAAe,EAAU,oCAAoC,EAAa,IAC1E,EACD,EACI,GAAe,EAAY,OAAS,GACvC,EAAI,cAAc,KAAK,UAAU,EAAa,KAAM,CAAC,IAAK,EAAI,EAE3D,GAAiB,EAAc,OAAS,GAC3C,EAAI,gBAAgB,KAAK,UAAU,EAAe,KAAM,CAAC,IAAK,EAAI,GAGpE,EAAiB,EAAU,EAAW,EAAgB,CAAK,EACpD,EAYR,IAAM,EACJ,OAAO,GAAQ,SAAY,UAAY,EAAO,SAC9C,OAAO,GAAQ,SAAY,UAAY,EAAO,SAC/C,GACK,EACJ,OAAO,GAAQ,eAAkB,UAAY,EAAO,eAAkB,GAClE,EAAQ,GAAQ,YAAc,GAAQ,WACtC,EAAW,MAAM,QAAQ,CAAK,EAAI,EAAM,KAAK;CAAI,EAAI,GAAS,GAEhE,EAIH,EAAY,GADG,EAAgB,GAAG,EAAc,IAAM,KAC9B,IAAgB,EAAW,KAAK,IAAa,KAC3D,GAAQ,QAClB,EACC,OAAO,EAAO,OAAU,SAAW,EAAO,MAAQ,KAAK,UAAU,EAAO,MAAO,KAAM,CAAC,EAEzF,OAAS,EAAG,CACP,GACH,EAAI,0CAA0C,IAAK,EAAI,CAGzD,CAGD,EACC,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,CACD,CACD,CAEA,EAAI,cAAc,EAAU,iBAAiB,EAAa,IAAK,CAAK,EAEpE,EAAiB,EAAU,EAAW,EAAgB,CAAK,EAE3D,GAAI,CAIH,IAAM,EAAU,MAAM,EAAS,KAAK,EAC9B,EAAS,KAAK,MAAM,CAAO,EAEjC,OADA,EAAoB,EAAQ,EAAQ,MAAM,EACnC,CACR,OAAS,EAAO,CAGf,GAAI,aAAiB,QAAU,EAAM,OAAS,cAAgB,EAAM,OAAS,gBAC5E,MAAM,EASP,IAAM,GAAe,EAAS,QAAQ,IAAI,cAAc,GAAK,GAAA,CAAI,YAAY,EAa7E,MAZwB,IAAgB,IAAM,CAAC,EAAY,SAAS,MAAM,EAEnE,IAAI,EACT,sDAAsD,EAAY,gDAClE,EAAW,iBACX,CACC,WAAY,EAAS,OACrB,QAAS,CAAE,IAAK,EAAS,YAAW,aAAY,EAChD,cAAe,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CACxE,CACD,EAEK,IAAI,EAAa,gCAAiC,EAAW,cAAe,CACjF,WAAY,EAAS,OACrB,QAAS,CACR,IAAK,EACL,WACD,EACA,cAAe,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CACxE,CAAC,CACF,CACD,CC1RA,MAAM,EAAuC,CAC5C,SAAU,EACV,YAAa,IACb,WAAY,IACZ,WAAY,EACb,EAEa,EAAmB,IAAI,IAAI,CAAC,IAAK,IAAK,GAAG,CAAC,EAUvD,SAAgB,EAAmB,EAAwD,CAE1F,OADK,EACE,CACN,SAAU,EAAO,UAAY,EAAc,SAC3C,YAAa,EAAO,aAAe,EAAc,YACjD,WAAY,EAAO,YAAc,EAAc,WAC/C,WAAY,EAAO,YAAc,EAAc,UAChD,EANoB,CAOrB,CAMA,SAAgB,EAAgB,EAA2C,CAC1E,GAAI,CAAC,EAAa,OAAO,KACzB,IAAM,EAAU,OAAO,CAAW,EAClC,GAAI,OAAO,SAAS,CAAO,GAAK,GAAW,EAAG,OAAO,EAAU,IAC/D,IAAM,EAAS,KAAK,MAAM,CAAW,EACrC,GAAI,OAAO,SAAS,CAAM,EAAG,CAC5B,IAAM,EAAQ,EAAS,KAAK,IAAI,EAChC,OAAO,EAAQ,EAAI,EAAQ,CAC5B,CACA,OAAO,IACR,CAEA,SAAgB,EAAa,EAAiB,EAAuC,CACpF,IAAM,EAAc,EAAO,YAAuB,GAAG,EAC/C,EAAS,KAAK,OAAO,EAAI,EAAO,YACtC,OAAO,KAAK,IAAI,EAAc,EAAQ,EAAO,UAAU,CACxD,CAEA,SAAgB,EAAM,EAAY,EAAqC,CACtE,OAAO,IAAI,SAAS,EAAS,IAAW,CACvC,GAAI,GAAQ,QAAS,CACpB,EAAO,IAAI,aAAa,UAAW,YAAY,CAAC,EAChD,MACD,CACA,IAAM,EAAK,eAAiB,CAC3B,GAAQ,oBAAoB,QAAS,CAAO,EAC5C,EAAQ,CACT,EAAG,CAAE,EACC,MAAgB,CACrB,aAAa,CAAE,EACf,EAAO,IAAI,aAAa,UAAW,YAAY,CAAC,CACjD,EACA,GAAQ,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,CAC1D,CAAC,CACF,CC1DA,SAAgB,EACf,EACA,EAC2D,CAC3D,IAAM,MAAkB,CAAC,EACnB,EAAe,OAAO,GAAc,UAAY,EAAY,EAElE,GAAI,CAAC,GAAgB,CAAC,EAAc,MAAO,CAAE,OAAQ,IAAA,GAAW,QAAS,CAAU,EACnF,GAAI,GAAgB,CAAC,EAAc,MAAO,CAAE,OAAQ,EAAc,QAAS,CAAU,EAErF,IAAM,EACL,OAAO,YAAgB,KAAe,OAAO,YAAY,SAAY,WAItE,GAAI,CAAC,EAAc,CAClB,GAAI,EAAiB,MAAO,CAAE,OAAQ,YAAY,QAAQ,CAAU,EAAG,QAAS,CAAU,EAC1F,IAAM,EAAO,IAAI,gBACX,EAAK,eAAiB,EAAK,MAAM,EAAG,CAAS,EACnD,MAAO,CAAE,OAAQ,EAAK,OAAQ,YAAe,aAAa,CAAE,CAAE,CAC/D,CAMA,IAAM,EAAO,IAAI,gBAEb,EACA,EACJ,GAAI,EACH,EAAgB,YAAY,QAAQ,CAAU,MACxC,CACN,IAAM,EAAc,IAAI,gBACxB,EAAU,eAAiB,EAAY,MAAM,EAAG,CAAS,EACzD,EAAgB,EAAY,MAC7B,CAEA,IAAM,EAAU,CAAC,EAAc,CAAa,EAGtC,EAAU,UAA6B,CAC5C,EAAK,MAAM,KAAK,MAAM,CACvB,EAEA,IAAK,IAAM,KAAK,EAAS,CACxB,GAAI,EAAE,QAAS,CACd,EAAK,MAAM,EAAE,MAAM,EACnB,KACD,CACA,EAAE,iBAAiB,QAAS,EAAS,CAAE,KAAM,EAAK,CAAC,CACpD,CAEA,MAAO,CACN,OAAQ,EAAK,OACb,YAAe,CACV,IAAY,IAAA,IAAW,aAAa,CAAO,EAC/C,IAAK,IAAM,KAAK,EAAS,EAAE,oBAAoB,QAAS,CAAO,CAChE,CACD,CACD,CCxBA,eAAe,EACd,EACA,EACA,EACA,EACuD,CACvD,GAAM,CAAE,SAAQ,WAAY,EAAc,EAAI,OAAO,OAAQ,EAAI,OAAO,SAAS,EAC3E,EAAY,YAAY,IAAI,EAElC,GAAI,CACH,IAAM,EAAW,MAAM,MAAM,EAAI,QAAS,CACzC,OAAQ,OACR,KAAM,EAAI,KACV,QAAS,EAAI,QACb,QACD,CAAC,EAMD,IAFC,EAAiB,IAAI,EAAS,MAAM,GAAM,EAAY,YAAc,EAAS,SAAW,MAEhE,EAAU,EAAgB,EAAG,CACrD,IAAM,EAAe,EAAgB,EAAS,QAAQ,IAAI,aAAa,CAAC,EAMlE,EACL,IAAiB,KAEd,EAAa,EAAS,CAAW,EADjC,KAAK,IAAI,EAAc,KAAK,IAAI,EAAY,WAAY,GAAkB,CAAC,EAM/E,OADA,MAAM,EAAS,KAAK,CAAC,CAAC,UAAY,CAAC,CAAC,EAC7B,CACN,GAAI,GACJ,MAAO,GACP,UACA,MAAO,IAAI,EACV,QAAQ,EAAS,OAAO,GAAG,EAAS,WAAW,eAC/C,EAAS,SAAW,IAAM,EAAW,WAAa,EAAW,cAC7D,CAAE,WAAY,EAAS,OAAQ,QAAS,CAAE,UAAW,EAAI,SAAU,CAAE,CACtE,CACD,CACD,CAaA,MAAO,CAAE,GAAI,GAAM,MAAA,MAXC,EACnB,EACA,EAAI,QACJ,EAAI,UACJ,EAAI,YACJ,EAAI,OAAO,UACX,EACA,EAAI,OAAO,MACX,EAAI,OAAO,eACX,EAAI,OAAO,gBACZ,CACyB,CAC1B,OAAS,EAAO,CAEf,GAAI,aAAiB,QAAU,EAAM,OAAS,cAAgB,EAAM,OAAS,gBAAiB,CAG7F,GAFsB,EAAI,OAAO,QAAQ,UAAY,GAIpD,MAAO,CACN,GAAI,GACJ,MAAO,GACP,MAAO,IAAI,EAAa,4BAA6B,EAAW,QAAS,CACxE,QAAS,CACR,SAAU,EAAI,SACd,UAAW,EAAI,UACf,YAAa,EAAI,WAClB,EACA,cAAe,CAChB,CAAC,CACF,EASD,IAAM,EADe,OAAO,EAAI,OAAO,WAAc,UAAY,EAAI,OAAO,UAAY,EAErF,IAAI,EACJ,2BAA2B,EAAI,OAAO,UAAU,IAChD,EAAW,cACX,CACC,QAAS,CACR,UAAW,EAAI,OAAO,UACtB,UAAW,EAAI,OAAO,UACtB,IAAK,EAAI,QACT,UAAW,EAAI,UACf,SAAU,EAAI,SACd,YAAa,EAAI,WAClB,EACA,cAAe,CAChB,CACD,EACC,IAAI,EACJ,mCAAmC,EAAM,KAAK,KAAK,EAAM,UACzD,EAAW,cACX,CACC,QAAS,CACR,UAAW,EAAI,OAAO,UACtB,IAAK,EAAI,QACT,UAAW,EAAI,UACf,SAAU,EAAI,SACd,YAAa,EAAI,WAClB,EACA,cAAe,CAChB,CACD,EASF,OARI,EAAU,EAAgB,EACtB,CACN,GAAI,GACJ,MAAO,GACP,QAAS,EAAa,EAAS,CAAW,EAC1C,MAAO,CACR,EAEM,CAAE,GAAI,GAAO,MAAO,GAAO,MAAO,CAAM,CAChD,CAgBA,GAAI,aAAiB,UAAW,CAC/B,IAAM,EAAY,OAAO,OAAW,KAAsB,OAAO,WAAa,OACxE,EAAe,CACpB,UAAW,EAAI,OAAO,UACtB,IAAK,EAAI,QACT,UAAW,EAAI,UACf,SAAU,EAAI,SACd,YAAa,EAAI,WAClB,EACM,EAAQ,EACX,IAAI,EACJ,mBAAmB,EAAM,QAAQ,wIACjC,EAAW,WACX,CAAE,QAAS,EAAc,cAAe,CAAM,CAC/C,EACC,IAAI,EAAa,kBAAkB,EAAM,UAAW,EAAW,cAAe,CAC9E,QAAS,EACT,cAAe,CAChB,CAAC,EASH,OARI,EAAU,EAAgB,EACtB,CACN,GAAI,GACJ,MAAO,GACP,QAAS,EAAa,EAAS,CAAW,EAC1C,MAAO,CACR,EAEM,CAAE,GAAI,GAAO,MAAO,GAAO,MAAO,CAAM,CAChD,CAGA,GAAI,aAAiB,EAAc,CAClC,IAAM,EAAS,EAAM,WAuBrB,OAhBC,EAAM,OAAS,EAAW,eAC1B,IAAW,IAAA,IACX,GAAU,KACV,EAAS,KAGR,IAAW,IAAA,KACV,EAAiB,IAAI,CAAM,GAAM,EAAY,YAAc,IAAW,OACxD,EAAU,EAAgB,EACnC,CACN,GAAI,GACJ,MAAO,GACP,QAAS,EAAa,EAAS,CAAW,EAC1C,MAAO,CACR,EAEM,CAAE,GAAI,GAAO,MAAO,GAAO,MAAO,CAAM,CAChD,CAGA,MAAO,CACN,GAAI,GACJ,MAAO,GACP,MAAO,IAAI,EACV,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACrD,EAAW,cACX,CACC,QAAS,CAAE,SAAU,EAAI,SAAU,UAAW,EAAI,SAAU,EAC5D,cAAe,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CACxE,CACD,CACD,CACD,QAAU,CACT,EAAQ,CACT,CACD,CA8CA,eAAsB,EACrB,EACA,EACA,EACa,CACb,IAAM,EAAY,GAAkB,EAIhC,EACJ,GAAI,CACH,EAAO,KAAK,UAAU,CAAI,CAC3B,OAAS,EAAO,CACf,MAAM,IAAI,EACT,0CAA0C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,IAC/F,EAAW,cACX,CACC,QAAS,CAAE,WAAU,WAAU,EAC/B,cAAe,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CACxE,CACD,CACD,CAGA,IAAM,EAAc,EAAe,CAAI,EACjC,EAAU,EAAS,EAAU,EAAO,SAAS,EAC7C,EAAU,EAAa,EAAW,CAAM,EACxC,EAAc,EAAmB,EAAO,KAAK,EAC7C,EAAgB,EAAY,SAAW,EAE7C,GAAI,EAAO,MAAO,CACjB,IAAM,GAAU,EAAc,KAAA,CAAM,QAAQ,CAAC,EAE7C,EAAI,GADU,EAAc,IAAS,KAAO,KAC/B,6BAA6B,EAAU,KAAK,EAAS,IAAI,EAAO,KAAM,EAAI,CACxF,CAEA,IAAM,EAAsB,CAC3B,WACA,OACA,cACA,UACA,YACA,UACA,QACD,EAMA,IAAK,IAAI,EAAU,GAAK,IAAW,CAClC,IAAM,EAAS,MAAM,EAAa,EAAK,EAAa,EAAS,CAAa,EAE1E,GAAI,EAAO,GAAI,OAAO,EAAO,MAE7B,GAAI,CAAC,EAAO,MAAO,MAAM,EAAO,MAE5B,EAAO,OACV,EACC,eAAe,EAAU,mBAAmB,EAAO,QAAQ,cAAc,EAAU,EAAE,GAAG,EAAc,KAAK,EAAO,MAAM,UACxH,EACD,EAGD,GAAI,CACH,MAAM,EAAM,EAAO,QAAS,EAAO,MAAM,CAC1C,MAAQ,CAEP,MAAM,IAAI,EAAa,4BAA6B,EAAW,QAAS,CACvE,QAAS,CAAE,WAAU,YAAW,aAAY,EAC5C,cAAe,EAAO,KACvB,CAAC,CACF,CACD,CACD,CChVA,SAAgB,EAAgB,EAA0D,CACzF,OACC,OAAO,GAAe,UACtB,EAAE,aAAsB,aACxB,OAAO,EAAW,KAAQ,UAC1B,OAAO,EAAW,MAAS,UAE7B,CC/BA,MAAa,EAAqB,IAChC,GAAa,GAAA,CACZ,QAAQ,MAAO,GAAG,CAAC,CACnB,QAAQ,MAAO,GAAG,CAAC,CACnB,MAAM,GAAG,CAAC,CACV,IAAK,GAAY,EAAQ,KAAK,CAAC,CAAC,CAChC,OAAQ,GAAY,IAAY,EAAE,EAGxB,EAAU,GACtB,EAAkB,EAAK,SAAS,CAAC,CAAC,IAAM,GAM5B,GAAiB,GAC7B,EAAkB,EAAK,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EASvC,EACZ,GACyC,CACzC,IAAM,EAAS,IAAI,IAEnB,IAAK,IAAM,KAAQ,EAAO,CACzB,IAAM,EAAO,EAAO,CAAI,EAClB,EAAS,EAAO,IAAI,CAAI,EAC1B,EAAQ,EAAO,KAAK,CAAI,EACvB,EAAO,IAAI,EAAM,CAAC,CAAI,CAAC,CAC7B,CAEA,OAAO,MAAM,KAAK,GAAS,CAAC,EAAM,MAAc,CAAE,OAAM,MAAO,CAAQ,EAAE,CAC1E,EAMa,GAAiB,GAAyB,CACtD,IAAM,EAAO,EACX,QAAQ,gBAAiB,GAAG,CAAC,CAC7B,QAAQ,OAAQ,GAAG,CAAC,CACpB,KAAK,CAAC,CACN,QAAQ,OAAQ,EAAE,EAEpB,OAAO,IAAS,GAAK,QAAU,CAChC,EC7Da,GAAkC,MAC9C,EACA,EAAwD,OAEjD,EAAa,EAAmB,CAAe,EAI1C,EAAmB,MAC/B,EACA,EACA,EAAwD,OACrC,CAEnB,GAAI,OAAO,SAAa,KAAe,OAAO,KAAS,IACtD,MAAM,IAAI,EACT,8HACA,EAAW,aACX,CACC,QAAS,CACR,YAAa,OAAO,OAAW,IAAc,gBAAkB,UAC/D,kBAAmB,OAAO,SAAa,IACvC,cAAe,OAAO,KAAS,GAChC,CACD,CACD,EAGD,GAAI,CAEH,MAAM,GAAqB,MADE,EAAa,EAAmB,CAAe,EACjC,CAAc,CAC1D,OAAS,EAAK,CAKb,MAHI,aAAe,EACZ,EAED,IAAI,EACT,iDACA,EAAW,cACX,CACC,QAAS,CAAE,cAAe,aAAe,MAAQ,EAAI,QAAU,OAAO,CAAG,CAAE,EAC3E,cAAe,aAAe,MAAQ,EAAM,IAAA,EAC7C,CACD,CACD,CACD,EAiBa,GAAyB,MACrC,EACA,EACA,EAAwD,OACrC,CACnB,IAAM,EAAS,EAAiB,CAAiB,EAGjD,GAAI,EAAO,SAAW,EAAG,CACxB,MAAM,EAAiB,CAAC,EAAG,EAAc,CAAe,EACxD,MACD,CAIA,IAAM,GAAc,EAAO,KAAM,GAAU,EAAM,OAAS,EAAE,GAAK,EAAO,GAAA,CAAI,KAE5E,IAAK,GAAM,CAAE,OAAM,WAAW,EAC7B,MAAM,EACL,IAAS,GAAK,EAAQ,EAAM,IAAK,IAAU,CAAE,GAAG,EAAM,UAAW,GAAc,CAAI,CAAE,EAAE,EACvF,IAAS,GAAK,EAAe,GAAc,CAAI,EAC/C,IAAS,EAAa,EAAkB,IACzC,CAEF,EAYM,EAAuB,GAC5B,EACE,QAAQ,MAAO,GAAG,CAAC,CACnB,QAAQ,MAAO,GAAG,CAAC,CACnB,MAAM,GAAG,CAAC,CACV,IAAK,GAAY,EAAQ,KAAK,CAAC,CAAC,CAChC,OACC,GACA,IAAY,IAAM,IAAY,KAAO,IAAY,MAAQ,CAAC,cAAc,KAAK,CAAO,CACtF,CAAC,CACA,KAAK,GAAG,EAOL,GAAgB,GACrB,IAAS,IAAS,OAAO,GAAS,UAAY,EAAK,KAAK,CAAC,CAAC,YAAY,IAAM,OAmBvE,GAAuB,GAA2C,CACvE,IAAM,EAAkC,CAAC,EAsDzC,OApDA,EAAU,QAAS,GAAS,CAC3B,IAAM,EAAU,EAAkB,EAAM,UAAU,GAAK,GACjD,EAAU,EAAkB,EAAM,UAAU,GAAK,GACjD,EAAW,EAAoB,GAAG,IAAU,GAAS,EAC3D,GAAI,IAAa,GAAI,CACpB,EAAU,CAAC,CAAC,KACX,qCAAqC,IAAU,EAAQ,oDACxD,EACA,MACD,CACA,IAAM,EAAe,EAAkB,EAAM,WAAW,EAClD,EAAY,EAAe,EAAoB,CAAY,EAAI,GAC/D,EAAW,IAAc,GAAkC,EAA7B,GAAG,EAAU,GAAG,IAI9C,EAAO,EAAmB,EAAM,MAAM,EAC5C,GAAI,OAAO,GAAS,UAAY,IAAS,GAAI,CAC5C,EAAU,CAAC,CAAC,KAAK,kBAAkB,EAAS,gCAAgC,EAC5E,MACD,CAIA,IAAM,EAAW,EAAkC,EAAM,UAAU,EAEnE,GAAI,GAAa,EAAmB,EAAM,iBAAiB,CAAC,EAI3D,GAAI,CACH,EAAe,KAAK,CACnB,WACA,QAAS,EAAqB,CAAI,EAClC,KAAM,EACN,YACA,GAAI,EAAW,CAAE,UAAS,EAAI,CAAC,CAChC,CAAC,CACF,OAAS,EAAK,CACb,EAAU,CAAC,CAAC,KAAK,kBAAkB,EAAS,0BAA2B,CAAG,CAC3E,MAEA,EAAe,KAAK,CACnB,WACA,QAAS,EACT,KAAM,EACN,YACA,GAAI,EAAW,CAAE,UAAS,EAAI,CAAC,CAChC,CAAC,CAEH,CAAC,EAEM,CACR,EAiBM,EAAmB,KAAO,KAoCxB,MAnCe,QAAQ,IAC7B,EAAK,IAAI,KAAO,IAAS,CACxB,GAAI,CACH,IAAM,EACL,OAAO,YAAgB,KAAe,OAAO,YAAY,SAAY,WAClE,YAAY,QAAQ,GAAsB,EAC1C,IAAA,GACE,EAAW,MAAM,MAAM,EAAK,SAAU,CAAE,QAAO,CAAC,EACtD,GAAI,CAAC,EAAS,GAEb,OADA,EAAU,CAAC,CAAC,KAAK,6CAA6C,EAAK,UAAU,EACtE,KAGR,IAAM,EAAc,MAAM,EAAS,YAAY,EAEzC,EAAW,EAAoB,EAAK,QAAQ,EAClD,GAAI,IAAa,GAEhB,OADA,EAAU,CAAC,CAAC,KAAK,6CAA6C,EAAK,UAAU,EACtE,KAER,IAAM,EAAY,EAAK,UAAY,EAAoB,EAAK,SAAS,EAAI,GACzE,MAAO,CACN,SAAU,EACV,QAAS,IAAI,WAAW,CAAW,EACnC,KAAM,IAAc,GAAkC,EAA7B,GAAG,EAAU,GAAG,IAEzC,WACD,CACD,OAAS,EAAO,CAEf,OADA,EAAU,CAAC,CAAC,MAAM,4CAA4C,EAAK,WAAY,CAAK,EAC7E,IACR,CACD,CAAC,CACF,EAAA,CAEe,OAAQ,GAA0B,IAAM,IAAI,EActD,EAAe,MACpB,EACA,IAC8B,CAC9B,IAAM,EAAiB,GAAoB,CAAS,EAEpD,GAAI,EAAiB,CACpB,IAAM,EAAa,MAAM,QAAQ,CAAe,EAAI,EAAkB,CAAC,CAAe,EACtF,EAAe,KAAK,GAAI,MAAM,EAAiB,CAAU,CAAE,CAC5D,CAEA,IAAM,EAAQ,IAAI,IAClB,OAAO,EAAe,IAAK,GAAS,CACnC,IAAM,EAAO,EAAkB,EAAK,KAAM,CAAK,EAI/C,OAHA,EAAM,IAAI,CAAI,EACV,IAAS,EAAK,KAAa,GAC/B,EAAU,CAAC,CAAC,KAAK,2BAA2B,EAAK,KAAK,kBAAkB,EAAK,GAAG,EACzE,CAAE,GAAG,EAAM,OAAM,SAAU,EAAK,MAAM,EAAK,YAAY,GAAG,EAAI,CAAC,CAAE,EACzE,CAAC,CACF,EAGA,eAAe,GAAqB,EAAwB,EAAgC,CAC3F,GAAM,CAAE,MAAK,WAAY,MAAM,OAAO,UAOhC,EAAsC,CAAC,EACvC,EAAQ,IAAI,IAClB,EAAM,QAAS,GAAS,CACvB,IAAM,EAAO,EAAkB,EAAK,KAAM,CAAK,EAC/C,EAAM,IAAI,CAAI,EACV,IAAS,EAAK,MACjB,EAAU,CAAC,CAAC,KAAK,2BAA2B,EAAK,KAAK,kBAAkB,EAAK,GAAG,EAEjF,EAAQ,GAAQ,OAAO,EAAK,SAAY,SAAW,EAAQ,EAAK,OAAO,EAAI,EAAK,OACjF,CAAC,EAID,IAAM,EAAS,MAAM,IAAI,SAAqB,EAAS,IAAW,CACjE,EAAI,EAAS,CAAE,MAAO,CAAE,GAAI,EAAK,IAAU,EAAM,EAAO,CAAG,EAAI,EAAQ,CAAI,CAAE,CAC9E,CAAC,EAGD,GAAS,IADQ,KAAK,CAAC,CAAkB,EAAG,CAAE,KAAM,iBAAkB,CAC1D,EAAG,GAAG,EAAQ,KAAK,CAChC,CAMA,SAAS,EAAkB,EAAc,EAAoC,CAC5E,GAAI,CAAC,EAAM,IAAI,CAAI,EAAG,OAAO,EAC7B,IAAM,EAAQ,EAAK,YAAY,GAAG,EAC5B,EAAM,EAAK,YAAY,GAAG,EAC1B,EAAU,EAAM,EAAQ,EAAM,EAAK,OACnC,EAAO,EAAK,MAAM,EAAG,CAAO,EAC5B,EAAM,EAAK,MAAM,CAAO,EAC9B,IAAK,IAAI,EAAI,GAAK,IAAK,CACtB,IAAM,EAAY,GAAG,EAAK,GAAG,IAAI,IACjC,GAAI,CAAC,EAAM,IAAI,CAAS,EAAG,OAAO,CACnC,CACD,CAGA,SAAS,GAAS,EAAY,EAAkB,CAC/C,GAAI,OAAO,SAAa,IACvB,MAAM,IAAI,EACT,+DACA,EAAW,aACX,CACC,QAAS,CAAE,SAAU,WAAY,YAAa,UAAW,CAC1D,CACD,EAGD,IAAM,EAAM,IAAI,gBAAgB,CAAI,EAC9B,EAAI,SAAS,cAAc,GAAG,EACpC,EAAE,KAAO,EACT,EAAE,SAAW,EAEb,SAAS,KAAK,YAAY,CAAC,EAC3B,EAAE,MAAM,EACR,EAAE,OAAO,EAGT,eAAiB,IAAI,gBAAgB,CAAG,EAAG,GAAM,CAClD"}
@@ -0,0 +1,3 @@
1
+ const e={NETWORK_ERROR:`NETWORK_ERROR`,AUTH_ERROR:`AUTH_ERROR`,VALIDATION_ERROR:`VALIDATION_ERROR`,COMPUTATION_ERROR:`COMPUTATION_ERROR`,TIMEOUT_ERROR:`TIMEOUT_ERROR`,CORS_ERROR:`CORS_ERROR`,NOT_FOUND:`NOT_FOUND`,RATE_LIMIT:`RATE_LIMIT`,INVALID_RESPONSE:`INVALID_RESPONSE`,UNKNOWN_ERROR:`UNKNOWN_ERROR`,INVALID_STATE:`INVALID_STATE`,INVALID_INPUT:`INVALID_INPUT`,INVALID_CONFIG:`INVALID_CONFIG`,BROWSER_ONLY:`BROWSER_ONLY`,ENVIRONMENT_ERROR:`ENVIRONMENT_ERROR`,ENCODING_ERROR:`ENCODING_ERROR`,MALFORMED_DEFAULT:`MALFORMED_DEFAULT`,SUPERSEDED:`SUPERSEDED`,ABORTED:`ABORTED`,QUEUE_FULL:`QUEUE_FULL`,QUEUE_TIMEOUT:`QUEUE_TIMEOUT`,DEFINITION_NOT_CACHED:`DEFINITION_NOT_CACHED`};var t=class t extends Error{code;statusCode;context;originalError;constructor(t,n=e.UNKNOWN_ERROR,r){super(t),this.name=`ComputeError`,this.code=n,this.statusCode=r?.statusCode,this.context=r?.context,this.originalError=r?.originalError,r?.originalError&&(this.cause=r.originalError)}static missingValues(n,r,i){return new t(`Input "${n}" has no values defined${r?` (expected ${r})`:``}`,e.INVALID_INPUT,{context:{inputName:n,expectedType:r,...i}})}static unknownParamType(n,r,i){return new t(`Unknown paramType: ${n}`,e.VALIDATION_ERROR,{context:{receivedParamType:n,paramName:r,...i}})}},n=class{debug(){}info(){}warn(){}error(){}},r=class{debug(e,...t){console.debug(e,...t)}info(e,...t){console.info(e,...t)}warn(e,...t){console.warn(e,...t)}error(e,...t){console.error(e,...t)}};let i=new n;function a(){return i}function o(r){if(r===null){i=new n;return}let a=[`debug`,`info`,`warn`,`error`].filter(e=>typeof r[e]!=`function`);if(a.length>0)throw new t(`Logger is missing required method(s): ${a.join(`, `)}. A logger must implement debug, info, warn and error.`,e.INVALID_CONFIG,{context:{missingMethods:a}});i=r}function s(){o(new r)}function c(e,t){if(!e||typeof e!=`object`)return;let n=e;if(l(n,t))return n[t];let r=f(n).get(t.toLowerCase());return r===void 0?void 0:n[r]}const l=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);function u(e,t){if(!e||typeof e!=`object`)return!1;let n=e;return l(n,t)?!0:f(n).has(t.toLowerCase())}const d=new WeakMap;function f(e){let t=Object.keys(e),n=d.get(e);if(n&&n.keyCount===t.length)return n.map;let r=new Map;for(let e of t){let t=e.toLowerCase();r.has(t)||r.set(t,e)}return d.set(e,{keyCount:t.length,map:r}),r}function p(n,r){let i=n?.trim()??``;if(!i)throw new t(`serverUrl is required`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});if(!/^https?:\/\//i.test(i))throw new t(`Invalid serverUrl: "${i}". Must start with "http://" or "https://". For example: "http://localhost:5000" or "https://example.com"`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});let a;try{a=new URL(i)}catch(r){throw new t(`Invalid serverUrl: "${i}". Must be a valid URL. Received error: ${r instanceof Error?r.message:String(r)}`,e.INVALID_CONFIG,{context:{receivedServerUrl:n},originalError:r instanceof Error?r:void 0})}if(a.username!==``||a.password!==``)throw new t(`Invalid serverUrl: "${i}". Must not embed credentials (user:pass@host) — fetch rejects credentialed URLs at request time. Pass the API key separately.`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});if(i.includes(`?`)||i.includes(`#`))throw new t(`Invalid serverUrl: "${i}". Must not contain a query string or fragment — endpoint paths are appended to this URL (e.g. "\${serverUrl}/version"), and a "?" or "#" suffix would corrupt every request path.`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});let o=a.hostname.toLowerCase().replace(/\.+$/,``);if((r?.blockedHosts??[`compute.rhino3d.com`]).map(e=>e.toLowerCase().replace(/\.+$/,``)).includes(o))throw new t(`serverUrl must be set to your Compute server URL. The shared public endpoint is not allowed.`,e.INVALID_CONFIG,{context:{receivedServerUrl:n}});return i.replace(/\/+$/,``)}function m(e,t){return`${t.replace(/\/+$/,``)}/${e.replace(/^\/+/,``)}`}function h(e){try{let t=new URL(e).hostname.toLowerCase();return t===`localhost`||t===`127.0.0.1`||t===`[::1]`}catch{return/(localhost|127\.0\.0\.1|\[::1\])/i.test(e)}}const g=new Set;function _(e){return{...e.headers,...e.authToken&&{Authorization:e.authToken},...e.apiKey&&{[e.apiKeyHeader??`RhinoComputeKey`]:e.apiKey}}}function v(e,t){let n={..._(t),"X-Request-ID":e,"Content-Type":`application/json`};return!t.apiKey&&!t.authToken&&!g.has(t.serverUrl)&&!h(t.serverUrl)&&(g.add(t.serverUrl),a().warn(`⚠️ [Compute] Request [${e}] targets remote server (${t.serverUrl}) but no API key or auth token is configured. Requests may fail or be rate-limited. (warned once per server)`)),n}function y(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}const b=/ECONNREFUSED|ERR_CONNECTION_REFUSED|connection refused/i,x=/ENOTFOUND|EAI_AGAIN|getaddrinfo|ERR_NAME_NOT_RESOLVED|dns/i,ee=/TimeoutError|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|ENETUNREACH|abort|timed? ?out/i;function te(e){if(e.online)return null;if(e.status!==void 0){if(e.status===401||e.status===403)return{verdict:`unauthorized`,retryable:!1,summary:`The server rejected the liveness probe with HTTP ${e.status} — check the API key.`};let t=e.status>=500;return{verdict:`http_error`,retryable:t,summary:t?`The server answered HTTP ${e.status} — it may still be starting up.`:`The server answered HTTP ${e.status}, which will not change on retry.`}}let t=e.error??``;return b.test(t)?{verdict:`refused`,retryable:!1,summary:`Connection refused — the host is reachable but nothing is listening on that port. Rhino.Compute is not running, or the URL names the wrong port.`}:x.test(t)?{verdict:`dns`,retryable:!0,summary:`The server hostname could not be resolved — check the URL and DNS.`}:ee.test(t)?{verdict:`timeout`,retryable:!0,summary:`No response before the timeout — the machine may be powered off, blocked by a firewall, or still booting.`}:{verdict:`unknown`,retryable:!0,summary:t?`The connection failed: ${t}`:`The connection failed.`}}function S(){let e=globalThis.Buffer;return typeof e==`function`?e:void 0}function C(e){let t=S();return t?t.from(e,`utf-8`).toString(`base64`):E(new TextEncoder().encode(e))}function w(e){if(!e)return null;let t=e.replace(/[\t\n\f\r ]/g,``);t.length%4==0&&(t=t.replace(/={1,2}$/,``));let n=t.length%4;if(n===1||t.length<64||!/^[A-Za-z0-9+/]*$/.test(t))return null;let r=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`.indexOf(t.charAt(t.length-1));return n===2&&r&15||n===3&&r&3?null:n===0?t:t+`==`.slice(0,4-n)}function T(n){let r=n.replace(/[\t\n\f\r ]/g,``);if(r.length%4==0&&(r=r.replace(/={1,2}$/,``)),r.length%4==1||!/^[A-Za-z0-9+/]*$/.test(r))throw new t(`Invalid base64 input.`,e.ENCODING_ERROR,{context:{inputLength:n.length}});let i=S();if(i)return new Uint8Array(i.from(r,`base64`));if(typeof globalThis.atob==`function`){let e=globalThis.atob(r),t=new Uint8Array(e.length);for(let n=0;n<e.length;n++)t[n]=e.charCodeAt(n)&255;return t}throw new t(`Base64 decoding not supported in this environment.`,e.ENVIRONMENT_ERROR,{context:{environmentInfo:`atob or Buffer not available`}})}function ne(e){let t=S();if(t)return t.byteLength(e,`utf-8`);let n=0;for(let t=0;t<e.length;t++){let r=e.charCodeAt(t);r<128?n+=1:r<2048?n+=2:r>=55296&&r<=56319&&t+1<e.length&&(e.charCodeAt(t+1)&64512)==56320?(n+=4,t++):n+=3}return n}function E(n){let r=S();if(r)return r.from(n).toString(`base64`);if(typeof globalThis.btoa==`function`){let e=32766,t=[];for(let r=0;r<n.length;r+=e)t.push(globalThis.btoa(String.fromCharCode.apply(null,n.subarray(r,r+e))));return t.join(``)}throw new t(`Base64 encoding not supported in this environment.`,e.ENVIRONMENT_ERROR,{context:{environmentInfo:`btoa or Buffer not available`}})}function D(e,t){t&&a().debug(e)}function O(e){if(!e)return null;let t={raw:e},n=!1;for(let r of e.split(`,`)){let[e,...i]=r.trim().split(`;`),a=i.find(e=>e.trim().toLowerCase().startsWith(`dur`));if(!a)continue;let o=Number(a.split(`=`)[1]);if(!Number.isFinite(o))continue;let s=e.trim().toLowerCase();(s===`decode`||s===`solve`||s===`encode`)&&(t[s]=o,n=!0)}return n?t:null}function k(e,t,n,r){if(!n)return;let i=O(e.headers.get(`Server-Timing`));if(i)try{n(i,t)}catch(e){r&&D(` onServerTiming callback threw: ${e}`,!0)}}const A=new WeakMap;function j(e,t){typeof e==`object`&&e&&A.set(e,t)}function re(e){if(typeof e==`object`&&e)return A.get(e)}const M=4096;function ie(e){return e.length<=M?e:`${e.slice(0,M)}… [truncated ${e.length-M} chars]`}function N(n,r,i,a,o,s,c,l,u){let{status:d,statusText:f}=n,p={};n.headers.forEach((e,t)=>{p[t]=e});let m=s.trim(),h=m?` — ${m.slice(0,200)}${m.length>200?`…`:``}`:``,g=l??s,_={url:r,requestId:i,method:`POST`,requestSize:a,serverUrl:o,responseBody:g?ie(g):void 0,responseHeaders:p},v={400:{message:`Bad request: ${f}${h}`,code:e.VALIDATION_ERROR},401:{message:`HTTP ${d}: ${f}${h}`,code:e.AUTH_ERROR},403:{message:`HTTP ${d}: ${f}${h}`,code:e.AUTH_ERROR},404:{message:`Endpoint not found: ${r}`,code:e.NOT_FOUND},413:{message:`Request too large: ${(a/1024).toFixed(2)}KB`,code:e.VALIDATION_ERROR},429:{message:`Rate limit exceeded${h}`,code:e.RATE_LIMIT},500:{message:`Server error: ${f}${h}`,code:e.COMPUTATION_ERROR},502:{message:`Bad gateway: ${f}${h}`,code:e.NETWORK_ERROR},503:{message:`Service unavailable: ${f}${h}`,code:e.NETWORK_ERROR},504:{message:`Service unavailable: ${f}${h}`,code:e.NETWORK_ERROR}}[d]||{message:`HTTP ${d}: ${f}${h}`,code:e.UNKNOWN_ERROR},y=P(c,u)??v.code;throw new t(v.message,y,{statusCode:d,context:_})}function P(e,t){if(e&&t)return t[e]}async function F(n,r,i,a,o,s,l,u,d){let f=Math.round(performance.now()-s);if(!n.ok){let e=await n.text(),t=e;l&&(D(`❌ Request [${i}] failed with HTTP ${n.status} in ${f}ms`,!0),D(` URL: ${r}`,!0),D(` Status: ${n.status} ${n.statusText}`,!0),t&&D(` Response body: ${t.substring(0,500)}${t.length>500?`...`:``}`,!0));let s;try{let e=JSON.parse(t);typeof e?.code==`string`&&(s=e.code)}catch{}if(n.status===500)try{let e=JSON.parse(t),r=c(e,`values`),a=c(e,`errors`),o=c(e,`warnings`);if(r&&(a||o))return j(e,t.length),l&&(D(`⚠️ Request [${i}] completed with solver errors in ${f}ms`,!0),a&&a.length>0&&D(` Errors: ${JSON.stringify(a,null,2)}`,!0),o&&o.length>0&&D(` Warnings: ${JSON.stringify(o,null,2)}`,!0)),k(n,i,u,l),e;let s=typeof e?.message==`string`&&e.message||typeof e?.Message==`string`&&e.Message||``,d=typeof e?.ExceptionType==`string`&&e.ExceptionType||``,p=e?.stackTrace??e?.StackTrace,m=Array.isArray(p)?p.join(`
2
+ `):p||``;s?t=`${d?`${d}: `:``}${s}${m?`\n${m}`:``}`:e?.error&&(t=typeof e.error==`string`?e.error:JSON.stringify(e.error,null,2))}catch(e){l&&D(` Failed to parse error body as JSON: ${e}`,!0)}N(n,r,i,a,o,t,s,e,d)}D(`✅ Request [${i}] completed in ${f}ms`,l),k(n,i,u,l);try{let e=await n.text(),t=JSON.parse(e);return j(t,e.length),t}catch(a){if(a instanceof Error&&(a.name===`AbortError`||a.name===`TimeoutError`))throw a;let o=(n.headers.get(`Content-Type`)??``).toLowerCase();throw o!==``&&!o.includes(`json`)?new t(`Server returned a non-JSON response (Content-Type: ${o}) — check the server URL / proxy configuration`,e.INVALID_RESPONSE,{statusCode:n.status,context:{url:r,requestId:i,contentType:o},originalError:a instanceof Error?a:Error(String(a))}):new t(`Failed to parse JSON response`,e.NETWORK_ERROR,{statusCode:n.status,context:{url:r,requestId:i},originalError:a instanceof Error?a:Error(String(a))})}}const I={attempts:0,baseDelayMs:500,maxDelayMs:3e4,retryOn429:!0},L=new Set([502,503,504]);function R(e){return e?{attempts:e.attempts??I.attempts,baseDelayMs:e.baseDelayMs??I.baseDelayMs,maxDelayMs:e.maxDelayMs??I.maxDelayMs,retryOn429:e.retryOn429??I.retryOn429}:I}function z(e){if(!e)return null;let t=Number(e);if(Number.isFinite(t)&&t>=0)return t*1e3;let n=Date.parse(e);if(Number.isFinite(n)){let e=n-Date.now();return e>0?e:0}return null}function B(e,t){let n=t.baseDelayMs*2**e,r=Math.random()*t.baseDelayMs;return Math.min(n+r,t.maxDelayMs)}function V(e,t){return new Promise((n,r)=>{if(t?.aborted){r(new DOMException(`Aborted`,`AbortError`));return}let i=setTimeout(()=>{t?.removeEventListener(`abort`,a),n()},e),a=()=>{clearTimeout(i),r(new DOMException(`Aborted`,`AbortError`))};t?.addEventListener(`abort`,a,{once:!0})})}function H(e,t){let n=()=>{},r=typeof t==`number`&&t>0;if(!e&&!r)return{signal:void 0,cleanup:n};if(e&&!r)return{signal:e,cleanup:n};let i=typeof AbortSignal<`u`&&typeof AbortSignal.timeout==`function`;if(!e){if(i)return{signal:AbortSignal.timeout(t),cleanup:n};let e=new AbortController,r=setTimeout(()=>e.abort(),t);return{signal:e.signal,cleanup:()=>clearTimeout(r)}}let a=new AbortController,o,s;if(i)o=AbortSignal.timeout(t);else{let e=new AbortController;s=setTimeout(()=>e.abort(),t),o=e.signal}let c=[e,o],l=function(){a.abort(this.reason)};for(let e of c){if(e.aborted){a.abort(e.reason);break}e.addEventListener(`abort`,l,{once:!0})}return{signal:a.signal,cleanup:()=>{s!==void 0&&clearTimeout(s);for(let e of c)e.removeEventListener(`abort`,l)}}}async function U(n,r,i,a){let{signal:o,cleanup:s}=H(n.config.signal,n.config.timeoutMs),c=performance.now();try{let s=await fetch(n.fullUrl,{method:`POST`,body:n.body,headers:n.headers,signal:o});if((L.has(s.status)||r.retryOn429&&s.status===429)&&i<a-1){let a=z(s.headers.get(`Retry-After`)),o=a===null?B(i,r):Math.min(a,Math.max(r.maxDelayMs,6e4));return await s.text().catch(()=>{}),{ok:!1,retry:!0,delayMs:o,cause:new t(`HTTP ${s.status} ${s.statusText} (will retry)`,s.status===429?e.RATE_LIMIT:e.NETWORK_ERROR,{statusCode:s.status,context:{requestId:n.requestId}})}}return{ok:!0,value:await F(s,n.fullUrl,n.requestId,n.requestSize,n.config.serverUrl,c,n.config.debug,n.config.onServerTiming,n.config.serverErrorCodes)}}catch(o){if(o instanceof Error&&(o.name===`AbortError`||o.name===`TimeoutError`)){if(n.config.signal?.aborted===!0)return{ok:!1,retry:!1,cause:new t(`Request aborted by caller`,e.ABORTED,{context:{endpoint:n.endpoint,requestId:n.requestId,requestSize:n.requestSize},originalError:o})};let s=typeof n.config.timeoutMs==`number`&&n.config.timeoutMs>0?new t(`Request timed out after ${n.config.timeoutMs}ms`,e.TIMEOUT_ERROR,{context:{serverUrl:n.config.serverUrl,timeoutMs:n.config.timeoutMs,url:n.fullUrl,requestId:n.requestId,endpoint:n.endpoint,requestSize:n.requestSize},originalError:o}):new t(`Request aborted by the runtime (${o.name}): ${o.message}`,e.NETWORK_ERROR,{context:{serverUrl:n.config.serverUrl,url:n.fullUrl,requestId:n.requestId,endpoint:n.endpoint,requestSize:n.requestSize},originalError:o});return i<a-1?{ok:!1,retry:!0,delayMs:B(i,r),cause:s}:{ok:!1,retry:!1,cause:s}}if(o instanceof TypeError){let s=typeof window<`u`&&window.document!==void 0,c={serverUrl:n.config.serverUrl,url:n.fullUrl,requestId:n.requestId,endpoint:n.endpoint,requestSize:n.requestSize},l=s?new t(`Request failed: ${o.message} — in a browser this usually means a CORS misconfiguration on the server (or a network failure; the browser does not distinguish them)`,e.CORS_ERROR,{context:c,originalError:o}):new t(`Network error: ${o.message}`,e.NETWORK_ERROR,{context:c,originalError:o});return i<a-1?{ok:!1,retry:!0,delayMs:B(i,r),cause:l}:{ok:!1,retry:!1,cause:l}}if(o instanceof t){let t=o.statusCode;return(o.code===e.NETWORK_ERROR&&t!==void 0&&t>=200&&t<300||t!==void 0&&(L.has(t)||r.retryOn429&&t===429))&&i<a-1?{ok:!1,retry:!0,delayMs:B(i,r),cause:o}:{ok:!1,retry:!1,cause:o}}return{ok:!1,retry:!1,cause:new t(o instanceof Error?o.message:String(o),e.UNKNOWN_ERROR,{context:{endpoint:n.endpoint,requestId:n.requestId},originalError:o instanceof Error?o:Error(String(o))})}}finally{s()}}async function W(n,r,i){let a=y(),o;try{o=JSON.stringify(r)}catch(r){throw new t(`Request body is not JSON-serializable: ${r instanceof Error?r.message:String(r)}`,e.INVALID_INPUT,{context:{endpoint:n,requestId:a},originalError:r instanceof Error?r:Error(String(r))})}let s=ne(o),c=m(n,i.serverUrl),l=v(a,i),u=R(i.retry),d=u.attempts+1;if(i.debug){let e=(s/1024).toFixed(2);D(`${s>1e5?`⚠️`:`🚀`} Starting compute request [${a}]: ${n} (${e}KB)`,!0)}let f={endpoint:n,body:o,requestSize:s,fullUrl:c,requestId:a,headers:l,config:i};for(let r=0;;r++){let o=await U(f,u,r,d);if(o.ok)return o.value;if(!o.retry)throw o.cause;i.debug&&D(`🔁 Request [${a}] retrying after ${o.delayMs}ms (attempt ${r+2}/${d}): ${o.cause.message}`,!0);try{await V(o.delayMs,i.signal)}catch{throw new t(`Request aborted by caller`,e.ABORTED,{context:{endpoint:n,requestId:a,requestSize:s},originalError:o.cause})}}}function G(e){return typeof e==`object`&&!(e instanceof Uint8Array)&&typeof e.key==`string`&&typeof e.load==`function`}const K=e=>(e??``).replace(/::/g,`/`).replace(/\\/g,`/`).split(`/`).map(e=>e.trim()).filter(e=>e!==``),q=e=>K(e.subFolder)[0]??``,J=e=>K(e.subFolder).slice(1).join(`/`),Y=e=>{let t=new Map;for(let n of e){let e=q(n),r=t.get(e);r?r.push(n):t.set(e,[n])}return Array.from(t,([e,t])=>({root:e,files:t}))},ae=e=>{let t=e.replace(/[/\\:*?"<>|]/g,`_`).replace(/\s+/g,` `).trim().replace(/^\.+/,``);return t===``?`files`:t},oe=async(e,t=null)=>Q(e,t),X=async(n,r,i=null)=>{if(typeof document>`u`||typeof Blob>`u`)throw new t(`File download functionality is only available in browser environments. This function requires the DOM API (document, Blob).`,e.BROWSER_ONLY,{context:{environment:typeof window<`u`?`browser (SSR)`:`Node.js`,documentAvailable:typeof document<`u`,blobAvailable:typeof Blob<`u`}});try{await de(await Q(n,i),r)}catch(n){throw n instanceof t?n:new t(`Failed to download files from compute response`,e.INVALID_STATE,{context:{originalError:n instanceof Error?n.message:String(n)},originalError:n instanceof Error?n:void 0})}},se=async(e,t,n=null)=>{let r=Y(e);if(r.length===0){await X([],t,n);return}let i=(r.find(e=>e.root===``)??r[0]).root;for(let{root:e,files:a}of r)await X(e===``?a:a.map(e=>({...e,subFolder:J(e)})),e===``?t:ae(e),e===i?n:null)},Z=e=>e.replace(/::/g,`/`).replace(/\\/g,`/`).split(`/`).map(e=>e.trim()).filter(e=>e!==``&&e!==`.`&&e!==`..`&&!/^[a-zA-Z]:$/.test(e)).join(`/`),ce=e=>e===!0||typeof e==`string`&&e.trim().toLowerCase()===`true`,le=e=>{let t=[];return e.forEach(e=>{let n=c(e,`fileName`)??``,r=c(e,`fileType`)??``,i=Z(`${n}${r}`);if(i===``){a().warn(`Skipping file with unusable name "${n}${r}": no safe archive path remains after sanitization.`);return}let o=c(e,`subFolder`),s=o?Z(o):``,l=s===``?i:`${s}/${i}`,u=c(e,`data`);if(typeof u!=`string`||u===``){a().warn(`Skipping file "${l}": item carries no usable data.`);return}let d=c(e,`metadata`);if(ce(c(e,`isBase64Encoded`)))try{t.push({fileName:i,content:T(u),path:l,subFolder:s,...d?{metadata:d}:{}})}catch(e){a().warn(`Skipping file "${l}": base64 decode failed.`,e)}else t.push({fileName:i,content:u,path:l,subFolder:s,...d?{metadata:d}:{}})}),t},ue=async e=>(await Promise.all(e.map(async e=>{try{let t=typeof AbortSignal<`u`&&typeof AbortSignal.timeout==`function`?AbortSignal.timeout(3e4):void 0,n=await fetch(e.filePath,{signal:t});if(!n.ok)return a().warn(`Failed to fetch additional file from URL: ${e.filePath}`),null;let r=await n.arrayBuffer(),i=Z(e.fileName);if(i===``)return a().warn(`Skipping fetched file with unusable name: ${e.fileName}`),null;let o=e.subFolder?Z(e.subFolder):``;return{fileName:i,content:new Uint8Array(r),path:o===``?i:`${o}/${i}`,subFolder:o}}catch(t){return a().error(`Error fetching additional file from URL: ${e.filePath}`,t),null}}))).filter(e=>e!==null),Q=async(e,t)=>{let n=le(e);if(t){let e=Array.isArray(t)?t:[t];n.push(...await ue(e))}let r=new Set;return n.map(e=>{let t=$(e.path,r);return r.add(t),t===e.path?e:(a().warn(`Duplicate archive path "${e.path}" — storing as "${t}".`),{...e,path:t,fileName:t.slice(t.lastIndexOf(`/`)+1)})})};async function de(e,t){let{zip:n,strToU8:r}=await import(`fflate`),i={},o=new Set;e.forEach(e=>{let t=$(e.path,o);o.add(t),t!==e.path&&a().warn(`Duplicate archive path "${e.path}" — storing as "${t}".`),i[t]=typeof e.content==`string`?r(e.content):e.content});let s=await new Promise((e,t)=>{n(i,{level:6},(n,r)=>n?t(n):e(r))});fe(new Blob([s],{type:`application/zip`}),`${t}.zip`)}function $(e,t){if(!t.has(e))return e;let n=e.lastIndexOf(`/`),r=e.lastIndexOf(`.`),i=r>n?r:e.length,a=e.slice(0,i),o=e.slice(i);for(let e=2;;e++){let n=`${a}-${e}${o}`;if(!t.has(n))return n}}function fe(n,r){if(typeof document>`u`)throw new t(`saveFile requires a browser environment with DOM API access.`,e.BROWSER_ONLY,{context:{function:`saveFile`,requiredAPI:`document`}});let i=URL.createObjectURL(n),a=document.createElement(`a`);a.href=i,a.download=r,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(i),1e4)}Object.defineProperty(exports,"C",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"S",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"_",{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return K}}),Object.defineProperty(exports,"b",{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return re}}),Object.defineProperty(exports,"d",{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,"f",{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,"g",{enumerable:!0,get:function(){return`compute.rhino3d.com`}}),Object.defineProperty(exports,"h",{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return Y}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,"m",{enumerable:!0,get:function(){return te}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return se}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return G}}),Object.defineProperty(exports,"p",{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return oe}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return W}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return X}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return E}}),Object.defineProperty(exports,"v",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"w",{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,"x",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"y",{enumerable:!0,get:function(){return c}});
3
+ //# sourceMappingURL=core-zk0-pvdL.cjs.map