@selvajs/solve 2.0.0-beta.10 → 2.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,2 +1,874 @@
1
- import{n as e,r as t,t as n}from"./outcome-BOrAeRAQ.js";import{getDefaultValue as r,getInputItems as i}from"@selvajs/schemas";import{GrasshopperResponseProcessor as a}from"@selvajs/compute/grasshopper";function o(e,t){return`external:${e}:${t}`}function s(e){let{scopeKey:t,inputId:n,value:r}=e;t&&n&&typeof sessionStorage<`u`&&sessionStorage.setItem(o(t,n),JSON.stringify(r))}function c(e){let{scopeKey:t,inputId:n}=e;if(!t||!n||typeof sessionStorage>`u`)return;let r=sessionStorage.getItem(o(t,n));if(r!==null)try{return JSON.parse(r)}catch{return}}function l(e){let{scopeKey:t,inputId:n}=e;t&&n&&typeof sessionStorage<`u`&&sessionStorage.removeItem(o(t,n))}function u(e){return i(e).filter(e=>e.source?.kind===`client`).map(e=>({paramId:e.paramId,displayName:e.displayName??e.paramId}))}const d=Object.freeze({solveId:null,running:!1,diagnostics:[],progress:/* @__PURE__ */ new Map,ended:null,other:[],lastSeq:0});function f(e,t){let r=t.solveId===e.solveId?e:{...d,solveId:t.solveId,running:!0};if(t.seq<=r.lastSeq)return e;let i={...r,lastSeq:t.seq},a=t.payload??{};switch(t.type){case`solveStarted`:return{...i,running:!0};case`solveEnded`:return{...i,running:!1,ended:{kind:typeof a.kind==`string`?a.kind:`ok`}};case`diagnostic`:{let[e]=n({diagnostics:[a]})?.diagnostics??[];return e?{...i,diagnostics:[...i.diagnostics,e]}:i}case`progress`:{let e=typeof a.source==`string`?a.source:``,t=new Map(i.progress),n=p(a),r=i.progress.get(e)?.fraction;return r!==void 0&&n.fraction!==void 0&&n.fraction<r&&(n.fraction=r),t.set(e,n),{...i,progress:t}}default:return{...i,other:[...i.other,t]}}}function p(e){let t=e=>typeof e==`number`&&Number.isFinite(e)?e:void 0,n=t(e.fraction),r=t(e.done),i=t(e.total),a=t(e.nextFraction),o=t(e.stepMs),s=e=>Math.min(1,Math.max(0,e));return{...typeof e.source==`string`?{source:e.source}:{},...n===void 0?{}:{fraction:s(n)},...r===void 0?{}:{done:r},...i===void 0?{}:{total:i},...typeof e.label==`string`?{label:e.label}:{},...a===void 0?{}:{nextFraction:s(a)},...o!==void 0&&o>0?{stepMs:o}:{}}}function m(e,t,n){let i=new Set(u(e).map(e=>e.paramId)),a={};for(let o of e.inputs)if(i.has(o.id)){let e=n({scopeKey:t,inputId:o.id});e!==void 0&&(a[o.id]=e)}else a[o.id]=o.default??r(o.paramType);for(let t of e.outputs)a[t.id]=null;return a}function h(e){let t=e===!1;return{hasPendingChanges:t,hasNeverSolved:t}}function g(e,t,n,r){return e.values[t]=n,r===!1?(e.pendingValues[t]=n,e.hasPendingChanges=!0,{state:e,shouldSolve:!1}):{state:e,shouldSolve:!0}}const _=/* @__PURE__ */ new Set([`dynamicValueList`,`file`]);function v(e,t,n,r,i){if(!t||!(r in t)||typeof i==`object`&&i||!Object.is(t[r],i)||!Object.is(n[r],i))return!1;let a=e?.inputs?.find(e=>e.id===r)?.paramType;return!_.has(a??``)}function y(e,t){if(!e?.inputs)return t;let n={};for(let r of e.inputs)r.id in t&&(n[r.id]=t[r.id]);return n}function b(e,t){return e.error=``,e.computeErrors=t.errors??[],e.computeWarnings=t.warnings??[],e.diagnostics=t.diagnostics??[],e.blocked=t.blocked??!1,e.blocked?(e.awaitingAck=!1,e.heldResult=null,S(e,t)):w(e.diagnostics)?(e.awaitingAck=!0,e.heldResult=t,e.pendingValues={},e.hasPendingChanges=!1,e):(e.awaitingAck=!1,e.heldResult=null,S(e,t))}function x(e){let t={};for(let n of e?.outputs??[])t[n.id]=null;return t}function S(e,t){e.meshes=t.meshes??[];let{meshes:n,...r}=t;return e.lastResult=r,Object.assign(e.values,t.outputs),e.pendingValues={},e.hasPendingChanges=!1,e.hasNeverSolved=!1,e}function C(e,t){return t?`solving`:e.awaitingAck?`review`:e.blocked?`blocked`:`idle`}function w(e){return(e??[]).some(e=>e.isGate&&e.level!==`remark`)}function T(e){let t=e.schema,n=null,r=h(t?.instanceSolve),i={values:m(t,e.scopeKey,c),error:``,computeErrors:[],computeWarnings:[],diagnostics:[],blocked:!1,awaitingAck:!1,heldResult:null,meshes:[],pendingValues:{},hasPendingChanges:r.hasPendingChanges,hasNeverSolved:r.hasNeverSolved,lastResult:null},a=/* @__PURE__ */ new Set,o=()=>a.forEach(e=>e()),s=/* @__PURE__ */ new Set,l=[],u={status:`aborted`,diagnostics:[]};function p(){return new Promise(e=>l.push(e))}function _(e){let t=l;l=[],t.forEach(t=>t(e)),s.forEach(t=>t(e))}function w(){let e=l;l=[],e.forEach(e=>e(u))}function T(e){let t=i.diagnostics;if(e.aborted)return _({status:`aborted`,diagnostics:t});if(e.blocked)return _({status:`blocked`,diagnostics:t});_({status:`applied`,result:i.lastResult,diagnostics:t})}let E=d,D=e.driver.onEvent?.(e=>{let t=f(E,e);t!==E&&(E=t,o())});function O(){let e=p();return k(),e}function k(){n={...y(t,i.values)},e.driver.solve(n),o()}return{get values(){return i.values},get error(){return i.error},get computeErrors(){return i.computeErrors},get computeWarnings(){return i.computeWarnings},get diagnostics(){return i.diagnostics},get blocked(){return i.blocked},get awaitingAck(){return i.awaitingAck},acknowledge(){if(!i.awaitingAck)return;let e=i.heldResult;i.awaitingAck=!1,i.heldResult=null,e&&S(i,e),o(),e&&T(e)},discard(){i.awaitingAck&&(i.awaitingAck=!1,i.heldResult=null,o(),_({status:`discarded`,diagnostics:i.diagnostics}))},get live(){return E},get phase(){return C(i,e.driver.isSolving)},abort(){e.driver.cancel()},get meshes(){return i.meshes},get lastResult(){return i.lastResult},get hasPendingChanges(){return i.hasPendingChanges},get hasNeverSolved(){return i.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(e,r,a=!1){if(!a&&v(t,n,i.values,e,r))return;let{shouldSolve:s}=g(i,e,r,t?.instanceSolve);s||a?(a&&!s&&(i.pendingValues={},i.hasPendingChanges=!1),k()):o()},solve:O,ensureSolved(){return e.driver.isSolving||e.driver.hasQueued||i.awaitingAck?p():i.hasPendingChanges||i.hasNeverSolved||i.error||!i.lastResult?O():i.blocked?Promise.resolve({status:`blocked`,diagnostics:i.diagnostics}):Promise.resolve({status:`applied`,result:i.lastResult,diagnostics:i.diagnostics})},onSettled(e){return s.add(e),()=>s.delete(e)},loadValues(e){Object.assign(i.values,e),t?.instanceSolve===!1?(i.hasPendingChanges=!0,o()):k()},rebuild(r,a){w(),t=r,n=null,e.driver.clearCache?.(),i.meshes=[],i.lastResult=null,i.error=``,i.computeErrors=[],i.computeWarnings=[],i.diagnostics=[],i.blocked=!1,i.awaitingAck=!1,i.heldResult=null,i.pendingValues={},i.values=m(r,a,c);let s=h(r?.instanceSolve);i.hasPendingChanges=s.hasPendingChanges,i.hasNeverSolved=s.hasNeverSolved,r&&Object.keys(i.values).length>0&&r.instanceSolve!==!1?k():o()},report(r){r.aborted&&(n=null),b(i,r.blocked||r.aborted?{...r,outputs:x(t),meshes:[]}:r),o(),!(i.awaitingAck||e.driver.hasQueued)&&T(r)},reportError(t){n=null,i.error=t,o(),e.driver.hasQueued||_({status:`failed`,error:t})},reportAborted(){n=null,o(),e.driver.hasQueued||_(u)},subscribe(e){return a.add(e),()=>a.delete(e)},notify:o,dispose(){D?.(),a.clear(),s.clear(),w()}}}function E(e,t){let{runDeadlineMs:n,onChange:r}=t,i=!1,a=null,o=null;function s(e){i!==e&&(i=e,r?.())}function c(){o?.abort(),o=null}async function l(t){c(),o=new AbortController;let{signal:r}=o,i=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${n}ms — aborting`),o?.abort(new DOMException(`Run deadline exceeded`,`TimeoutError`))},n);s(!0);try{await e(t,r)}catch(e){(!(e instanceof Error)||e.name!==`AbortError`&&e.name!==`TimeoutError`)&&console.error(`[Solve/throttle] unhandled error in run:`,e)}finally{if(clearTimeout(i),s(!1),o=null,a!==null){let e=a;a=null,l(e)}}}function u(e){i?(a!==null&&console.debug(`[Solve/throttle] superseded pending run (latest-wins)`),a=e):l(e)}function d(){a=null,c()}return{trigger:u,get isRunning(){return i},get hasPending(){return a!==null},cancel:d}}function D(e){return O(e)}function O(e){if(typeof e!=`object`||!e)return JSON.stringify(e)??`null`;if(Array.isArray(e))return`[${e.map(O).join(`,`)}]`;let t=e;return`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${O(t[e])}`).join(`,`)}}`}function k(e={}){let{max:t=16,meshPolicy:n}=e,r=/* @__PURE__ */ new Map;function i(e){e.meshes?.length&&n?.release(e.meshes)}function a(e){return!n||!e.meshes?.length?e:{...e,meshes:n.clone(e.meshes)}}function o(e){let t=r.get(e);t!==void 0&&(r.delete(e),i(t))}return{get(e){let n=D(e),i=r.get(n);if(i!==void 0)return r.delete(n),r.set(n,i),console.info(`[Solve/memo] HIT — served from client memo (${r.size}/${t})`),a(i)},set(e,n){let i=D(e);for(o(i),r.set(i,a(n));r.size>t;){let e=r.keys().next().value;if(e===void 0)break;o(e),console.info(`[Solve/memo] evicted LRU entry (cap ${t})`)}},clear(){r.size>0&&console.info(`[Solve/memo] cleared ${r.size} entries (definition changed)`),r.forEach(i),r.clear()}}}function A(e,t,n){let{meshPolicy:r,solveDeadlineMs:i,onChange:a,events:o}=n,s=k({meshPolicy:r});function c(e){e.reason?.name===`TimeoutError`?t().reportError(`The solve took longer than ${Math.round(i/1e3)} s and was stopped.`):t().reportAborted()}let l=E(async(n,r)=>{let i=s.get(n);if(i!==void 0){if(r.aborted)return c(r);t().report(i)}else try{let i=await e(n,r);if(r.aborted)return console.debug(`[Solve/driver] solve completed after abort — result discarded`),c(r);let a={...i,values:n};s.set(n,a),t().report(a)}catch(e){if(r.aborted)return console.debug(`[Solve/driver] solve aborted (cancelled or timed out)`),c(r);console.warn(`[Solve/driver] solve failed:`,e),t().reportError(e instanceof Error?e.message:String(e))}},{runDeadlineMs:i,onChange:a});return{solve(e){l.trigger(e)},cancel(){l.cancel(),o?.cancelCurrent?.()},get isSolving(){return l.isRunning},get hasQueued(){return l.hasPending},clearCache(){s.clear()},...o?{onEvent:e=>o.subscribe(e)}:{}}}function j(){return typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID().replace(/-/g,``):Array.from({length:32},()=>Math.floor(Math.random()*16).toString(16)).join(``)}function M(e){let t=j(),n=/* @__PURE__ */ new Set,r=e.fetch??((...e)=>fetch(...e)),i=null,a=null;function o(){if(i||typeof EventSource>`u`)return;let r=`${e.endpoint}${e.endpoint.includes(`?`)?`&`:`?`}streamId=${t}`;i=new EventSource(r),i.addEventListener(`solve`,e=>{let t;try{t=JSON.parse(e.data)}catch{return}t.type===`solveStarted`?a=t.solveId:t.type===`solveEnded`&&a===t.solveId&&(a=null);for(let e of n)e(t)})}return o(),{streamId:t,get currentSolveId(){return a},subscribe(e){return n.add(e),o(),()=>{n.delete(e)}},cancelCurrent(){let t=a;t&&r(e.cancelEndpoint(t),{method:`POST`,keepalive:!0}).catch(()=>{})},close(){i?.close(),i=null,n.clear(),a=null}}}function N(e){let t={};if(!e)return t;for(let n of e.split(`,`)){let[e,...r]=n.split(`;`).map(e=>e.trim()),i=r.find(e=>e.startsWith(`dur=`));if(!e||!i)continue;let a=Number(i.slice(4));Number.isFinite(a)&&(t[e]=a)}return t}const P=e=>{throw Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},F=()=>{throw Error(`Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.`)};function I(r){let i=r.debug??!1,o=(...e)=>{i&&console.info(...e)},s=0;return async(c,l)=>{let u=s-Date.now();if(u>0)throw Error(`Rate limit reached. Try again in ${Math.ceil(u/1e3)}s.`);let d=performance.now(),f=r.inputs(),p=r.outputs(),m=r.channel?.(),h=r.versionId?.(),g=r.streamId?.(),_=JSON.stringify({inputs:f,values:c,definitionUrl:r.definitionUrl(),...h?{versionId:h}:m===`draft`?{channel:`draft`}:{},...g?{streamId:g}:{}}),v;try{v=await fetch(r.endpoint,{method:`POST`,headers:{"Content-Type":`application/json`},body:_,signal:l})}catch(e){if(l.aborted)return o(`[Compute] solve aborted during fetch — empty result discarded`),{outputs:{}};throw Error(`Request blocked — your session may have expired. Reload the page to sign in again.`,{cause:e})}if(l.aborted)return o(`[Compute] solve aborted after headers — empty result discarded`),{outputs:{}};let y=v.redirected&&new URL(v.url).pathname===`/login`;if((v.status===401||y)&&(r.onSessionExpired??F)(),!v.ok){let e=await v.text().catch(()=>``),t={};try{t=JSON.parse(e)}catch{e&&console.warn(`[Compute] non-JSON error body (HTTP ${v.status}):`,e.slice(0,300))}if(v.status===503||v.status===504)throw Error(t.message||`Compute server is offline or unreachable. Please try again later.`);if(v.status===429){let e=Number(v.headers.get(`Retry-After`))||Number(t.retryAfter)||5;s=Date.now()+e*1e3,(r.onRateLimited??P)(e)}throw Error(t.message||`Compute error (HTTP ${v.status})`)}let b=performance.now()-d,x=performance.now(),S=await v.text();if(l.aborted)return o(`[Compute] solve aborted mid-download — empty result discarded`),{outputs:{}};let C=performance.now()-x,w=S.length,T;try{T=JSON.parse(S)}catch(e){throw Error(`Received an invalid response from the server. Reload the page and try again.`,{cause:e})}let E=new a(T),D=n(T.selva?.outcome)??t(T.errors??[],T.warnings??[]),O=D.blocked||D.aborted,k=r.meshes&&!O?await r.meshes.extract(E.response,{debug:i}):[],A={};for(let e of O?[]:p){let t=E.getValue({byId:e.id},{parseValues:!0});A[e.id]=t??(e.nickname?E.getValue({byName:e.nickname},{parseValues:!0}):void 0)}if(i){let e=JSON.stringify(c).length,t=N(v.headers.get(`Server-Timing`)),n=t.total??0,r=Math.max(0,b-n),i=w/1048576,a=i>=1?`${i.toFixed(2)} MB`:`${(w/1024).toFixed(0)} KB`,s=C>0?(i/(C/1e3)).toFixed(1):`∞`,l=performance.memory?.usedJSHeapSize,u=l===void 0?``:` | heap=${(l/1048576).toFixed(0)} MB`;if(o(`[Compute] req=${(_.length/1024).toFixed(0)}KB (values ${(e/1024).toFixed(0)}KB) | ttfb=${b.toFixed(0)}ms (network≈${r.toFixed(0)} + server ${n.toFixed(0)}) | download=${C.toFixed(0)}ms (${a} @ ${s} MB/s)${u}`),t.selva_cache!==void 0){let e=t.selva_cache?`HIT — served without calling compute`:t.rhino_cache?`HIT — Rhino.Compute's result cache, solved nothing`:`miss — solved on Rhino.Compute`;o(`[Compute] └─ cache: ${e}`)}}return e({outputs:A,meshes:k,source:T},D)}}export{d as EMPTY_LIVE_SOLVE,b as applySolveResult,g as applyValueChange,m as buildInitialValues,l as clearExternalValue,E as createAsyncThrottle,I as createComputeFetchSolveFn,A as createRequestResponseDriver,M as createSolveEventStream,k as createSolveMemo,T as createSolveSession,n as decodeOutcome,e as finalizeResult,u as getExternalInputs,h as makeInitialFlags,y as pickInputValues,c as readExternalValue,f as reduceLiveSolve,C as solvePhase,D as stableInputKey,s as writeExternalValue};
1
+ import { n as finalizeResult, r as outcomeFromComputeMessages, t as decodeOutcome } from "./outcome-ai_9fm43.js";
2
+ import { getDefaultValue, getInputItems } from "@selvajs/schemas";
3
+ import { GrasshopperResponseProcessor } from "@selvajs/compute/grasshopper";
4
+ // src/client/external-storage.ts
5
+ const STORAGE_PREFIX = "external";
6
+ function makeKey(scopeKey, inputId) {
7
+ return `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;
8
+ }
9
+ function writeExternalValue(args) {
10
+ const { scopeKey, inputId, value } = args;
11
+ if (!scopeKey || !inputId) return;
12
+ if (typeof sessionStorage === "undefined") return;
13
+ sessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));
14
+ }
15
+ function readExternalValue(ref) {
16
+ const { scopeKey, inputId } = ref;
17
+ if (!scopeKey || !inputId) return void 0;
18
+ if (typeof sessionStorage === "undefined") return void 0;
19
+ const raw = sessionStorage.getItem(makeKey(scopeKey, inputId));
20
+ if (raw === null) return void 0;
21
+ try {
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ return;
25
+ }
26
+ }
27
+ function clearExternalValue(ref) {
28
+ const { scopeKey, inputId } = ref;
29
+ if (!scopeKey || !inputId) return;
30
+ if (typeof sessionStorage === "undefined") return;
31
+ sessionStorage.removeItem(makeKey(scopeKey, inputId));
32
+ }
33
+ function getExternalInputs(schema) {
34
+ return getInputItems(schema).filter((item) => item.source?.kind === "client").map((item) => ({
35
+ paramId: item.paramId,
36
+ displayName: item.displayName ?? item.paramId
37
+ }));
38
+ }
39
+ // src/client/events/live-solve.ts
40
+ const EMPTY_LIVE_SOLVE = Object.freeze({
41
+ solveId: null,
42
+ running: false,
43
+ diagnostics: [],
44
+ progress: /* @__PURE__ */ new Map(),
45
+ ended: null,
46
+ other: [],
47
+ lastSeq: 0
48
+ });
49
+ function reduceLiveSolve(state, event) {
50
+ const base = event.solveId === state.solveId ? state : {
51
+ ...EMPTY_LIVE_SOLVE,
52
+ solveId: event.solveId,
53
+ running: true
54
+ };
55
+ if (event.seq <= base.lastSeq) return state;
56
+ const next = {
57
+ ...base,
58
+ lastSeq: event.seq
59
+ };
60
+ const payload = event.payload ?? {};
61
+ switch (event.type) {
62
+ case "solveStarted": return {
63
+ ...next,
64
+ running: true
65
+ };
66
+ case "solveEnded": return {
67
+ ...next,
68
+ running: false,
69
+ ended: { kind: typeof payload.kind === "string" ? payload.kind : "ok" }
70
+ };
71
+ case "diagnostic": {
72
+ const [diagnostic] = decodeOutcome({ diagnostics: [payload] })?.diagnostics ?? [];
73
+ return diagnostic ? {
74
+ ...next,
75
+ diagnostics: [...next.diagnostics, diagnostic]
76
+ } : next;
77
+ }
78
+ case "progress": {
79
+ const source = typeof payload.source === "string" ? payload.source : "";
80
+ const progress = new Map(next.progress);
81
+ const report = readProgress(payload);
82
+ const previous = next.progress.get(source)?.fraction;
83
+ if (previous !== void 0 && report.fraction !== void 0 && report.fraction < previous) report.fraction = previous;
84
+ progress.set(source, report);
85
+ return {
86
+ ...next,
87
+ progress
88
+ };
89
+ }
90
+ default: return {
91
+ ...next,
92
+ other: [...next.other, event]
93
+ };
94
+ }
95
+ }
96
+ function readProgress(payload) {
97
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
98
+ const fraction = num(payload.fraction);
99
+ const done = num(payload.done);
100
+ const total = num(payload.total);
101
+ const nextFraction = num(payload.nextFraction);
102
+ const stepMs = num(payload.stepMs);
103
+ const clamp = (v) => Math.min(1, Math.max(0, v));
104
+ return {
105
+ ...typeof payload.source === "string" ? { source: payload.source } : {},
106
+ ...fraction !== void 0 ? { fraction: clamp(fraction) } : {},
107
+ ...done !== void 0 ? { done } : {},
108
+ ...total !== void 0 ? { total } : {},
109
+ ...typeof payload.label === "string" ? { label: payload.label } : {},
110
+ ...nextFraction !== void 0 ? { nextFraction: clamp(nextFraction) } : {},
111
+ ...stepMs !== void 0 && stepMs > 0 ? { stepMs } : {}
112
+ };
113
+ }
114
+ // src/client/session/solve-session-core.ts
115
+ /**
116
+ * Client-sourced inputs are hydrated from `read` and left absent when no stored value
117
+ * exists, so the missing-inputs panel can detect them. Other inputs fall back to their
118
+ * declared default, then the paramType default.
119
+ */
120
+ function buildInitialValues(schema, scopeKey, read) {
121
+ const clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));
122
+ const values = {};
123
+ for (const input of schema.inputs) {
124
+ if (clientSet.has(input.id)) {
125
+ const stored = read({
126
+ scopeKey,
127
+ inputId: input.id
128
+ });
129
+ if (stored !== void 0) values[input.id] = stored;
130
+ continue;
131
+ }
132
+ values[input.id] = input.default ?? getDefaultValue(input.paramType);
133
+ }
134
+ for (const output of schema.outputs) values[output.id] = null;
135
+ return values;
136
+ }
137
+ /** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */
138
+ function makeInitialFlags(instanceSolve) {
139
+ const manual = instanceSolve === false;
140
+ return {
141
+ hasPendingChanges: manual,
142
+ hasNeverSolved: manual
143
+ };
144
+ }
145
+ /** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */
146
+ function applyValueChange(state, id, value, instanceSolve) {
147
+ state.values[id] = value;
148
+ if (instanceSolve === false) {
149
+ state.pendingValues[id] = value;
150
+ state.hasPendingChanges = true;
151
+ return {
152
+ state,
153
+ shouldSolve: false
154
+ };
155
+ }
156
+ return {
157
+ state,
158
+ shouldSolve: true
159
+ };
160
+ }
161
+ const RESEND_UNCHANGED = /* @__PURE__ */ new Set(["dynamicValueList", "file"]);
162
+ /**
163
+ * Whether setting `value` would solve again for nothing: it is what the last solve sent and what
164
+ * the session holds, and nothing failed or aborted since (`lastSent` is null then). A field that
165
+ * reports on input and again on blur would otherwise solve twice, and Grasshopper re-applies every
166
+ * dynamic list on each update, so the repeat is a real solve. Objects never count as repeats.
167
+ */
168
+ function isRepeatOfLastSolve(schema, lastSent, values, id, value) {
169
+ if (!lastSent || !(id in lastSent)) return false;
170
+ if (value !== null && typeof value === "object") return false;
171
+ if (!Object.is(lastSent[id], value) || !Object.is(values[id], value)) return false;
172
+ const paramType = schema?.inputs?.find((input) => input.id === id)?.paramType;
173
+ return !RESEND_UNCHANGED.has(paramType ?? "");
174
+ }
175
+ /**
176
+ * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value
177
+ * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB
178
+ * options-list output showed this can re-upload a multi-MB payload no backend reads.
179
+ */
180
+ function pickInputValues(schema, values) {
181
+ if (!schema?.inputs) return values;
182
+ const picked = {};
183
+ for (const input of schema.inputs) if (input.id in values) picked[input.id] = values[input.id];
184
+ return picked;
185
+ }
186
+ function applySolveResult(state, result) {
187
+ state.error = "";
188
+ state.computeErrors = result.errors ?? [];
189
+ state.computeWarnings = result.warnings ?? [];
190
+ state.diagnostics = result.diagnostics ?? [];
191
+ state.blocked = result.blocked ?? false;
192
+ if (state.blocked) {
193
+ state.awaitingAck = false;
194
+ state.heldResult = null;
195
+ return commitSolveResult(state, result);
196
+ }
197
+ if (needsAcknowledgement(state.diagnostics)) {
198
+ state.awaitingAck = true;
199
+ state.heldResult = result;
200
+ state.pendingValues = {};
201
+ state.hasPendingChanges = false;
202
+ return state;
203
+ }
204
+ state.awaitingAck = false;
205
+ state.heldResult = null;
206
+ return commitSolveResult(state, result);
207
+ }
208
+ /**
209
+ * Every schema output set to null. A blocked solve reports `outputs: {}`, and merging that into
210
+ * `values` would leave the previous solve's outputs behind.
211
+ */
212
+ function blankOutputs(schema) {
213
+ const blank = {};
214
+ for (const output of schema?.outputs ?? []) blank[output.id] = null;
215
+ return blank;
216
+ }
217
+ /** Writes a result into the state. Shared by the clean path and by `acknowledge()`. */
218
+ function commitSolveResult(state, result) {
219
+ state.meshes = result.meshes ?? [];
220
+ const { meshes: _meshes, ...retained } = result;
221
+ state.lastResult = retained;
222
+ Object.assign(state.values, result.outputs);
223
+ state.pendingValues = {};
224
+ state.hasPendingChanges = false;
225
+ state.hasNeverSolved = false;
226
+ return state;
227
+ }
228
+ /**
229
+ * One value for what a host should show. Solving wins: a new solve supersedes a held or refused
230
+ * result. `isSolving` is passed in because the driver owns it.
231
+ */
232
+ function solvePhase(state, isSolving) {
233
+ if (isSolving) return "solving";
234
+ if (state.awaitingAck) return "review";
235
+ if (state.blocked) return "blocked";
236
+ return "idle";
237
+ }
238
+ /**
239
+ * Whether this solve's messages still need the user's acknowledgement.
240
+ *
241
+ * Only a Selva Message component gates. Grasshopper raises warnings constantly for reasons that
242
+ * concern whoever is editing the definition, not whoever is using the app — a component
243
+ * complaining about its own inputs is not something an end user can act on, and interrupting
244
+ * them for it trains them to dismiss the one message that mattered.
245
+ *
246
+ * Every solve that gates asks again, by design — an acknowledgement covers the solve in front of
247
+ * the user, never a future one. Under `instanceSolve` with a live warning that means a dialog
248
+ * per solve, which is the intended checkpoint, not an oversight.
249
+ *
250
+ * Without `diagnostics` (a transport that reports no structure) nothing gates: the messages are
251
+ * still shown, just without interrupting.
252
+ */
253
+ function needsAcknowledgement(diagnostics) {
254
+ return (diagnostics ?? []).some((d) => d.isGate && d.level !== "remark");
255
+ }
256
+ // src/client/session/solve-session.ts
257
+ function createSolveSession(args) {
258
+ let currentSchema = args.schema;
259
+ let lastSent = null;
260
+ const flags = makeInitialFlags(currentSchema?.instanceSolve);
261
+ const state = {
262
+ values: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),
263
+ error: "",
264
+ computeErrors: [],
265
+ computeWarnings: [],
266
+ diagnostics: [],
267
+ blocked: false,
268
+ awaitingAck: false,
269
+ heldResult: null,
270
+ meshes: [],
271
+ pendingValues: {},
272
+ hasPendingChanges: flags.hasPendingChanges,
273
+ hasNeverSolved: flags.hasNeverSolved,
274
+ lastResult: null
275
+ };
276
+ const listeners = /* @__PURE__ */ new Set();
277
+ const emit = () => listeners.forEach((l) => l());
278
+ const settledListeners = /* @__PURE__ */ new Set();
279
+ let waiters = [];
280
+ const ABORTED = {
281
+ status: "aborted",
282
+ diagnostics: []
283
+ };
284
+ function nextSettlement() {
285
+ return new Promise((resolve) => waiters.push(resolve));
286
+ }
287
+ function settle(settlement) {
288
+ const waiting = waiters;
289
+ waiters = [];
290
+ waiting.forEach((resolve) => resolve(settlement));
291
+ settledListeners.forEach((listener) => listener(settlement));
292
+ }
293
+ /** Resolves waiting callers without telling listeners: no solve ended, the session moved on. */
294
+ function abandon() {
295
+ const waiting = waiters;
296
+ waiters = [];
297
+ waiting.forEach((resolve) => resolve(ABORTED));
298
+ }
299
+ function settleResult(result) {
300
+ const diagnostics = state.diagnostics;
301
+ if (result.aborted) return settle({
302
+ status: "aborted",
303
+ diagnostics
304
+ });
305
+ if (result.blocked) return settle({
306
+ status: "blocked",
307
+ diagnostics
308
+ });
309
+ settle({
310
+ status: "applied",
311
+ result: state.lastResult,
312
+ diagnostics
313
+ });
314
+ }
315
+ let live = EMPTY_LIVE_SOLVE;
316
+ const stopEvents = args.driver.onEvent?.((event) => {
317
+ const next = reduceLiveSolve(live, event);
318
+ if (next === live) return;
319
+ live = next;
320
+ emit();
321
+ });
322
+ function solve() {
323
+ const settled = nextSettlement();
324
+ dispatch();
325
+ return settled;
326
+ }
327
+ function dispatch() {
328
+ lastSent = { ...pickInputValues(currentSchema, state.values) };
329
+ args.driver.solve(lastSent);
330
+ emit();
331
+ }
332
+ return {
333
+ get values() {
334
+ return state.values;
335
+ },
336
+ get error() {
337
+ return state.error;
338
+ },
339
+ get computeErrors() {
340
+ return state.computeErrors;
341
+ },
342
+ get computeWarnings() {
343
+ return state.computeWarnings;
344
+ },
345
+ get diagnostics() {
346
+ return state.diagnostics;
347
+ },
348
+ get blocked() {
349
+ return state.blocked;
350
+ },
351
+ get awaitingAck() {
352
+ return state.awaitingAck;
353
+ },
354
+ acknowledge() {
355
+ if (!state.awaitingAck) return;
356
+ const held = state.heldResult;
357
+ state.awaitingAck = false;
358
+ state.heldResult = null;
359
+ if (held) commitSolveResult(state, held);
360
+ emit();
361
+ if (held) settleResult(held);
362
+ },
363
+ discard() {
364
+ if (!state.awaitingAck) return;
365
+ state.awaitingAck = false;
366
+ state.heldResult = null;
367
+ emit();
368
+ settle({
369
+ status: "discarded",
370
+ diagnostics: state.diagnostics
371
+ });
372
+ },
373
+ get live() {
374
+ return live;
375
+ },
376
+ get phase() {
377
+ return solvePhase(state, args.driver.isSolving);
378
+ },
379
+ abort() {
380
+ args.driver.cancel();
381
+ },
382
+ get meshes() {
383
+ return state.meshes;
384
+ },
385
+ get lastResult() {
386
+ return state.lastResult;
387
+ },
388
+ get hasPendingChanges() {
389
+ return state.hasPendingChanges;
390
+ },
391
+ get hasNeverSolved() {
392
+ return state.hasNeverSolved;
393
+ },
394
+ get isSolving() {
395
+ return args.driver.isSolving;
396
+ },
397
+ setValue(id, value, forceSolve = false) {
398
+ if (!forceSolve && isRepeatOfLastSolve(currentSchema, lastSent, state.values, id, value)) return;
399
+ const { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);
400
+ if (shouldSolve || forceSolve) {
401
+ if (forceSolve && !shouldSolve) {
402
+ state.pendingValues = {};
403
+ state.hasPendingChanges = false;
404
+ }
405
+ dispatch();
406
+ } else emit();
407
+ },
408
+ solve,
409
+ ensureSolved() {
410
+ if (args.driver.isSolving || args.driver.hasQueued || state.awaitingAck) return nextSettlement();
411
+ if (state.hasPendingChanges || state.hasNeverSolved || state.error || !state.lastResult) return solve();
412
+ if (state.blocked) return Promise.resolve({
413
+ status: "blocked",
414
+ diagnostics: state.diagnostics
415
+ });
416
+ return Promise.resolve({
417
+ status: "applied",
418
+ result: state.lastResult,
419
+ diagnostics: state.diagnostics
420
+ });
421
+ },
422
+ onSettled(listener) {
423
+ settledListeners.add(listener);
424
+ return () => settledListeners.delete(listener);
425
+ },
426
+ loadValues(incoming) {
427
+ Object.assign(state.values, incoming);
428
+ if (currentSchema?.instanceSolve !== false) dispatch();
429
+ else {
430
+ state.hasPendingChanges = true;
431
+ emit();
432
+ }
433
+ },
434
+ rebuild(schema, scopeKey) {
435
+ abandon();
436
+ currentSchema = schema;
437
+ lastSent = null;
438
+ args.driver.clearCache?.();
439
+ state.meshes = [];
440
+ state.lastResult = null;
441
+ state.error = "";
442
+ state.computeErrors = [];
443
+ state.computeWarnings = [];
444
+ state.diagnostics = [];
445
+ state.blocked = false;
446
+ state.awaitingAck = false;
447
+ state.heldResult = null;
448
+ state.pendingValues = {};
449
+ state.values = buildInitialValues(schema, scopeKey, readExternalValue);
450
+ const f = makeInitialFlags(schema?.instanceSolve);
451
+ state.hasPendingChanges = f.hasPendingChanges;
452
+ state.hasNeverSolved = f.hasNeverSolved;
453
+ if (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) dispatch();
454
+ else emit();
455
+ },
456
+ report(result) {
457
+ if (result.aborted) lastSent = null;
458
+ applySolveResult(state, result.blocked || result.aborted ? {
459
+ ...result,
460
+ outputs: blankOutputs(currentSchema),
461
+ meshes: []
462
+ } : result);
463
+ emit();
464
+ if (state.awaitingAck || args.driver.hasQueued) return;
465
+ settleResult(result);
466
+ },
467
+ reportError(message) {
468
+ lastSent = null;
469
+ state.error = message;
470
+ emit();
471
+ if (!args.driver.hasQueued) settle({
472
+ status: "failed",
473
+ error: message
474
+ });
475
+ },
476
+ reportAborted() {
477
+ lastSent = null;
478
+ emit();
479
+ if (!args.driver.hasQueued) settle(ABORTED);
480
+ },
481
+ subscribe(listener) {
482
+ listeners.add(listener);
483
+ return () => listeners.delete(listener);
484
+ },
485
+ notify: emit,
486
+ dispose() {
487
+ stopEvents?.();
488
+ listeners.clear();
489
+ settledListeners.clear();
490
+ abandon();
491
+ }
492
+ };
493
+ }
494
+ // src/client/async-throttle.ts
495
+ function createAsyncThrottle(run, options) {
496
+ const { runDeadlineMs, onChange } = options;
497
+ let isRunning = false;
498
+ let pendingValues = null;
499
+ let currentAbortController = null;
500
+ function setRunning(next) {
501
+ if (isRunning === next) return;
502
+ isRunning = next;
503
+ onChange?.();
504
+ }
505
+ function abortCurrent() {
506
+ currentAbortController?.abort();
507
+ currentAbortController = null;
508
+ }
509
+ async function execute(values) {
510
+ abortCurrent();
511
+ currentAbortController = new AbortController();
512
+ const { signal } = currentAbortController;
513
+ const deadlineId = setTimeout(() => {
514
+ console.warn(`[Solve/throttle] run exceeded ${runDeadlineMs}ms — aborting`);
515
+ currentAbortController?.abort(new DOMException("Run deadline exceeded", "TimeoutError"));
516
+ }, runDeadlineMs);
517
+ setRunning(true);
518
+ try {
519
+ await run(values, signal);
520
+ } catch (err) {
521
+ if (!(err instanceof Error) || err.name !== "AbortError" && err.name !== "TimeoutError") console.error("[Solve/throttle] unhandled error in run:", err);
522
+ } finally {
523
+ clearTimeout(deadlineId);
524
+ setRunning(false);
525
+ currentAbortController = null;
526
+ if (pendingValues !== null) {
527
+ const next = pendingValues;
528
+ pendingValues = null;
529
+ execute(next);
530
+ }
531
+ }
532
+ }
533
+ function trigger(values) {
534
+ if (isRunning) {
535
+ if (pendingValues !== null) console.debug("[Solve/throttle] superseded pending run (latest-wins)");
536
+ pendingValues = values;
537
+ } else execute(values);
538
+ }
539
+ function cancel() {
540
+ pendingValues = null;
541
+ abortCurrent();
542
+ }
543
+ return {
544
+ trigger,
545
+ get isRunning() {
546
+ return isRunning;
547
+ },
548
+ get hasPending() {
549
+ return pendingValues !== null;
550
+ },
551
+ cancel
552
+ };
553
+ }
554
+ // src/client/solve-memo.ts
555
+ /**
556
+ * Deterministic string key for a set of input values. Sorts object keys at every level so
557
+ * two logically-equal inputs built in different key order collide.
558
+ */
559
+ function stableInputKey(values) {
560
+ return serialize(values);
561
+ }
562
+ function serialize(value) {
563
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
564
+ if (Array.isArray(value)) return `[${value.map(serialize).join(",")}]`;
565
+ const obj = value;
566
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(",")}}`;
567
+ }
568
+ function createSolveMemo(options = {}) {
569
+ const { max = 16, meshPolicy } = options;
570
+ const entries = /* @__PURE__ */ new Map();
571
+ function release(result) {
572
+ if (result.meshes?.length) meshPolicy?.release(result.meshes);
573
+ }
574
+ function copy(result) {
575
+ if (!meshPolicy || !result.meshes?.length) return result;
576
+ return {
577
+ ...result,
578
+ meshes: meshPolicy.clone(result.meshes)
579
+ };
580
+ }
581
+ function evict(key) {
582
+ const entry = entries.get(key);
583
+ if (entry === void 0) return;
584
+ entries.delete(key);
585
+ release(entry);
586
+ }
587
+ return {
588
+ get(values) {
589
+ const key = stableInputKey(values);
590
+ const hit = entries.get(key);
591
+ if (hit === void 0) return void 0;
592
+ entries.delete(key);
593
+ entries.set(key, hit);
594
+ console.info(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);
595
+ return copy(hit);
596
+ },
597
+ set(values, result) {
598
+ const key = stableInputKey(values);
599
+ evict(key);
600
+ entries.set(key, copy(result));
601
+ while (entries.size > max) {
602
+ const oldest = entries.keys().next().value;
603
+ if (oldest === void 0) break;
604
+ evict(oldest);
605
+ console.info(`[Solve/memo] evicted LRU entry (cap ${max})`);
606
+ }
607
+ },
608
+ clear() {
609
+ if (entries.size > 0) console.info(`[Solve/memo] cleared ${entries.size} entries (definition changed)`);
610
+ entries.forEach(release);
611
+ entries.clear();
612
+ }
613
+ };
614
+ }
615
+ // src/client/drivers/request-response.ts
616
+ /**
617
+ * `getReporter` is lazy (`() => session`) because the session and driver reference each
618
+ * other — the host builds the driver first, then the session, then can hand this back.
619
+ */
620
+ function createRequestResponseDriver(onSolve, getReporter, options) {
621
+ const { meshPolicy, solveDeadlineMs, onChange, events } = options;
622
+ const memo = createSolveMemo({ meshPolicy });
623
+ function reportStopped(signal) {
624
+ if (signal.reason?.name === "TimeoutError") getReporter().reportError(`The solve took longer than ${Math.round(solveDeadlineMs / 1e3)} s and was stopped.`);
625
+ else getReporter().reportAborted();
626
+ }
627
+ const throttle = createAsyncThrottle(async (values, signal) => {
628
+ const cached = memo.get(values);
629
+ if (cached !== void 0) {
630
+ if (signal.aborted) return reportStopped(signal);
631
+ getReporter().report(cached);
632
+ return;
633
+ }
634
+ try {
635
+ const result = await onSolve(values, signal);
636
+ if (signal.aborted) {
637
+ console.debug("[Solve/driver] solve completed after abort — result discarded");
638
+ return reportStopped(signal);
639
+ }
640
+ const stamped = {
641
+ ...result,
642
+ values
643
+ };
644
+ memo.set(values, stamped);
645
+ getReporter().report(stamped);
646
+ } catch (err) {
647
+ if (signal.aborted) {
648
+ console.debug("[Solve/driver] solve aborted (cancelled or timed out)");
649
+ return reportStopped(signal);
650
+ }
651
+ console.warn("[Solve/driver] solve failed:", err);
652
+ getReporter().reportError(err instanceof Error ? err.message : String(err));
653
+ }
654
+ }, {
655
+ runDeadlineMs: solveDeadlineMs,
656
+ onChange
657
+ });
658
+ return {
659
+ solve(values) {
660
+ throttle.trigger(values);
661
+ },
662
+ cancel() {
663
+ throttle.cancel();
664
+ events?.cancelCurrent?.();
665
+ },
666
+ get isSolving() {
667
+ return throttle.isRunning;
668
+ },
669
+ get hasQueued() {
670
+ return throttle.hasPending;
671
+ },
672
+ clearCache() {
673
+ memo.clear();
674
+ },
675
+ ...events ? { onEvent: (listener) => events.subscribe(listener) } : {}
676
+ };
677
+ }
678
+ // src/client/events/solve-event-stream.ts
679
+ function mintStreamId() {
680
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID().replace(/-/g, "");
681
+ return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
682
+ }
683
+ function createSolveEventStream(options) {
684
+ const streamId = mintStreamId();
685
+ const listeners = /* @__PURE__ */ new Set();
686
+ const doFetch = options.fetch ?? ((...args) => fetch(...args));
687
+ let source = null;
688
+ let currentSolveId = null;
689
+ function open() {
690
+ if (source || typeof EventSource === "undefined") return;
691
+ const url = `${options.endpoint}${options.endpoint.includes("?") ? "&" : "?"}streamId=${streamId}`;
692
+ source = new EventSource(url);
693
+ source.addEventListener("solve", (raw) => {
694
+ let event;
695
+ try {
696
+ event = JSON.parse(raw.data);
697
+ } catch {
698
+ return;
699
+ }
700
+ if (event.type === "solveStarted") currentSolveId = event.solveId;
701
+ else if (event.type === "solveEnded" && currentSolveId === event.solveId) currentSolveId = null;
702
+ for (const listener of listeners) listener(event);
703
+ });
704
+ }
705
+ open();
706
+ return {
707
+ streamId,
708
+ get currentSolveId() {
709
+ return currentSolveId;
710
+ },
711
+ subscribe(listener) {
712
+ listeners.add(listener);
713
+ open();
714
+ return () => {
715
+ listeners.delete(listener);
716
+ };
717
+ },
718
+ cancelCurrent() {
719
+ const solveId = currentSolveId;
720
+ if (!solveId) return;
721
+ doFetch(options.cancelEndpoint(solveId), {
722
+ method: "POST",
723
+ keepalive: true
724
+ }).catch(() => {});
725
+ },
726
+ close() {
727
+ source?.close();
728
+ source = null;
729
+ listeners.clear();
730
+ currentSolveId = null;
731
+ }
732
+ };
733
+ }
734
+ // src/client/compute-fetch-solve-fn.ts
735
+ /**
736
+ * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a
737
+ * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +
738
+ * `SolveEngine` produce server-side) and unpacks the result.
739
+ *
740
+ * Ports the resilience behavior an interactive solve endpoint needs regardless
741
+ * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response
742
+ * guarding, and abort handling at every await point — so a new consumer gets it
743
+ * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:
744
+ * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a
745
+ * viewer-enabled consumer supplies its own `meshes.extract` hook (typically
746
+ * `getThreeObjectsFromComputeResponse` from `@selvajs/visualization/parse`).
747
+ */
748
+ function parseServerTiming(header) {
749
+ const out = {};
750
+ if (!header) return out;
751
+ for (const part of header.split(",")) {
752
+ const [name, ...params] = part.split(";").map((s) => s.trim());
753
+ const dur = params.find((p) => p.startsWith("dur="));
754
+ if (!name || !dur) continue;
755
+ const ms = Number(dur.slice(4));
756
+ if (Number.isFinite(ms)) out[name] = ms;
757
+ }
758
+ return out;
759
+ }
760
+ const defaultOnRateLimited = (retryAfterSeconds) => {
761
+ throw new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);
762
+ };
763
+ const defaultOnSessionExpired = () => {
764
+ throw new Error("Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.");
765
+ };
766
+ function createComputeFetchSolveFn(opts) {
767
+ const debug = opts.debug ?? false;
768
+ const log = (...args) => {
769
+ if (debug) console.info(...args);
770
+ };
771
+ let cooldownUntil = 0;
772
+ return async (values, signal) => {
773
+ const remainingMs = cooldownUntil - Date.now();
774
+ if (remainingMs > 0) throw new Error(`Rate limit reached. Try again in ${Math.ceil(remainingMs / 1e3)}s.`);
775
+ const solveStart = performance.now();
776
+ const inputs = opts.inputs();
777
+ const outputs = opts.outputs();
778
+ const channel = opts.channel?.();
779
+ const versionId = opts.versionId?.();
780
+ const streamId = opts.streamId?.();
781
+ const payload = JSON.stringify({
782
+ inputs,
783
+ values,
784
+ definitionUrl: opts.definitionUrl(),
785
+ ...versionId ? { versionId } : channel === "draft" ? { channel: "draft" } : {},
786
+ ...streamId ? { streamId } : {}
787
+ });
788
+ let res;
789
+ try {
790
+ res = await fetch(opts.endpoint, {
791
+ method: "POST",
792
+ headers: { "Content-Type": "application/json" },
793
+ body: payload,
794
+ signal
795
+ });
796
+ } catch (err) {
797
+ if (signal.aborted) {
798
+ log("[Compute] solve aborted during fetch — empty result discarded");
799
+ return { outputs: {} };
800
+ }
801
+ throw new Error("Request blocked — your session may have expired. Reload the page to sign in again.", { cause: err });
802
+ }
803
+ if (signal.aborted) {
804
+ log("[Compute] solve aborted after headers — empty result discarded");
805
+ return { outputs: {} };
806
+ }
807
+ const redirectedToLogin = res.redirected && new URL(res.url).pathname === "/login";
808
+ if (res.status === 401 || redirectedToLogin) (opts.onSessionExpired ?? defaultOnSessionExpired)();
809
+ if (!res.ok) {
810
+ const errorBody = await res.text().catch(() => "");
811
+ let d = {};
812
+ try {
813
+ d = JSON.parse(errorBody);
814
+ } catch {
815
+ if (errorBody) console.warn(`[Compute] non-JSON error body (HTTP ${res.status}):`, errorBody.slice(0, 300));
816
+ }
817
+ if (res.status === 503 || res.status === 504) throw new Error(d.message || "Compute server is offline or unreachable. Please try again later.");
818
+ if (res.status === 429) {
819
+ const retryAfter = Number(res.headers.get("Retry-After")) || Number(d.retryAfter) || 5;
820
+ cooldownUntil = Date.now() + retryAfter * 1e3;
821
+ (opts.onRateLimited ?? defaultOnRateLimited)(retryAfter);
822
+ }
823
+ throw new Error(d.message || `Compute error (HTTP ${res.status})`);
824
+ }
825
+ const ttfbMs = performance.now() - solveStart;
826
+ const downloadStart = performance.now();
827
+ const bodyText = await res.text();
828
+ if (signal.aborted) {
829
+ log("[Compute] solve aborted mid-download — empty result discarded");
830
+ return { outputs: {} };
831
+ }
832
+ const downloadMs = performance.now() - downloadStart;
833
+ const bytes = bodyText.length;
834
+ let solved;
835
+ try {
836
+ solved = JSON.parse(bodyText);
837
+ } catch (err) {
838
+ throw new Error("Received an invalid response from the server. Reload the page and try again.", { cause: err });
839
+ }
840
+ const processor = new GrasshopperResponseProcessor(solved);
841
+ const outcome = decodeOutcome(solved.selva?.outcome) ?? outcomeFromComputeMessages(solved.errors ?? [], solved.warnings ?? []);
842
+ const rejected = outcome.blocked || outcome.aborted;
843
+ const meshes = opts.meshes && !rejected ? await opts.meshes.extract(processor.response, { debug }) : [];
844
+ const resultOutputs = {};
845
+ for (const o of rejected ? [] : outputs) {
846
+ const byId = processor.getValue({ byId: o.id }, { parseValues: true });
847
+ resultOutputs[o.id] = byId ?? (o.nickname ? processor.getValue({ byName: o.nickname }, { parseValues: true }) : void 0);
848
+ }
849
+ if (debug) {
850
+ const valuesBytes = JSON.stringify(values).length;
851
+ const serverTiming = parseServerTiming(res.headers.get("Server-Timing"));
852
+ const serverTotal = serverTiming.total ?? 0;
853
+ const networkMs = Math.max(0, ttfbMs - serverTotal);
854
+ const sizeMB = bytes / 1048576;
855
+ const size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bytes / 1024).toFixed(0)} KB`;
856
+ const mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1e3)).toFixed(1) : "∞";
857
+ const heapMB = performance.memory?.usedJSHeapSize;
858
+ const heap = heapMB !== void 0 ? ` | heap=${(heapMB / 1048576).toFixed(0)} MB` : "";
859
+ log(`[Compute] req=${(payload.length / 1024).toFixed(0)}KB (values ${(valuesBytes / 1024).toFixed(0)}KB) | ttfb=${ttfbMs.toFixed(0)}ms (network≈${networkMs.toFixed(0)} + server ${serverTotal.toFixed(0)}) | download=${downloadMs.toFixed(0)}ms (${size} @ ${mbps} MB/s)${heap}`);
860
+ if (serverTiming.selva_cache !== void 0) {
861
+ const cacheMsg = serverTiming.selva_cache ? "HIT — served without calling compute" : serverTiming.rhino_cache ? "HIT — Rhino.Compute's result cache, solved nothing" : "miss — solved on Rhino.Compute";
862
+ log(`[Compute] └─ cache: ${cacheMsg}`);
863
+ }
864
+ }
865
+ return finalizeResult({
866
+ outputs: resultOutputs,
867
+ meshes,
868
+ source: solved
869
+ }, outcome);
870
+ };
871
+ }
872
+ export { EMPTY_LIVE_SOLVE, applySolveResult, applyValueChange, buildInitialValues, clearExternalValue, createAsyncThrottle, createComputeFetchSolveFn, createRequestResponseDriver, createSolveEventStream, createSolveMemo, createSolveSession, decodeOutcome, finalizeResult, getExternalInputs, makeInitialFlags, pickInputValues, readExternalValue, reduceLiveSolve, solvePhase, stableInputKey, writeExternalValue };
873
+
2
874
  //# sourceMappingURL=client.js.map