@selvajs/solve 0.2.0-beta.1 → 0.2.0-beta.4

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/README.md CHANGED
@@ -14,10 +14,9 @@ client/ form state machine, auto/manual decision, throttle, result memo, drive
14
14
  server/ solve pipeline (tree build → solve → serialize → envelope), caches, single-flight
15
15
  ```
16
16
 
17
- `client/` and `server/` both depend on `shared/`, and **never on each other** — enforced three ways:
18
- no root barrel, `no-restricted-imports` in this package's eslint config, and a bundle test that
19
- checks the shipped `dist/client.js` rather than the source
20
- ([`src/__tests__/client-bundle-boundary.test.ts`](./src/__tests__/client-bundle-boundary.test.ts)).
17
+ `client/` and `server/` both depend on `shared/`, and **never on each other**. See
18
+ [`src/server/README.md`](./src/server/README.md#node-only-and-that-is-load-bearing) for how that's
19
+ enforced.
21
20
 
22
21
  See [`src/client/README.md`](./src/client/README.md) for the session, the driver seam, and how to
23
22
  write a transport; [`src/server/README.md`](./src/server/README.md) for the pipeline and the cache
@@ -28,19 +27,19 @@ tiers.
28
27
  - **No UI framework.** No Svelte, no runes, no DOM. `client/` is a state machine, not a component.
29
28
  - **No renderer.** No `three`. `SolveResult<TMesh>` keeps meshes opaque — the app that parses a
30
29
  response into `THREE.Object3D[]` is the only place that knows the concrete type. This is what lets
31
- a headless CLI solve without dragging in a parse layer. Where the result memo genuinely needs to
32
- clone and dispose meshes, the rules are injected as a `MeshPolicy` (three.js implementation:
33
- `meshPolicy` in `@selvajs/visualization/parse`).
30
+ a headless CLI solve without dragging in a parse layer. See
31
+ [`src/client/README.md`](./src/client/README.md#mesh-ownership-is-injected-not-known-here) for how
32
+ the result memo handles mesh ownership without knowing what a mesh is.
34
33
  - **No authorization, orgs, projects, or share links.** App policy.
35
34
  - **No HTTP.** `client/` stops at a `SolveFn`; `server/` stops at a `SolveOutcome`. Mapping either to
36
35
  a status code is the app's job.
37
36
 
38
37
  ## No root barrel — on purpose
39
38
 
40
- `@selvajs/solve` exports `./shared`, `./client` and `./server`. There is deliberately **no `.`
41
- export**: a root barrel re-exporting both halves would let a browser bundle reach server code — and
42
- server credentials — through one innocent-looking import, defeating every other guard. Adding one is
43
- a boundary change, not a convenience.
39
+ `@selvajs/solve` exports `./shared`, `./client` and `./server`. There is deliberately no `.` export:
40
+ a root barrel re-exporting both halves would let a browser bundle reach server code — and server
41
+ credentials — through one innocent-looking import, defeating every other guard. Adding one is a
42
+ boundary change, not a convenience.
44
43
 
45
44
  ```ts
46
45
  import type { SolveResult } from '@selvajs/solve/shared';
@@ -54,6 +53,3 @@ import { runSolvePipeline } from '@selvajs/solve/server';
54
53
  `@selvajs/server/compute` keeps only the HTTP request policy it owns (limits, rate limiting, the
55
54
  SSRF guard, remote-definition fetch) and does not re-export any of this — the two packages are
56
55
  independent.
57
-
58
- In a Svelte app use `useSolveSession` from `@selvajs/ui` rather than `createSolveSession` directly —
59
- the raw factory returns correct values that never re-render.
package/dist/client.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _schemas = require('@selvajs/schemas');var V="external";function g(n,r){return`${V}:${n}:${r}`}function P(n){let{scopeKey:r,inputId:s,value:e}=n;!r||!s||typeof sessionStorage>"u"||sessionStorage.setItem(g(r,s),JSON.stringify(e))}function f(n){let{scopeKey:r,inputId:s}=n;if(!r||!s||typeof sessionStorage>"u")return;let e=sessionStorage.getItem(g(r,s));if(e!==null)try{return JSON.parse(e)}catch (e2){return}}function C(n){let{scopeKey:r,inputId:s}=n;!r||!s||typeof sessionStorage>"u"||sessionStorage.removeItem(g(r,s))}function h(n){return _schemas.getInputItems.call(void 0, n).filter(r=>_optionalChain([r, 'access', _ => _.source, 'optionalAccess', _2 => _2.kind])==="client").map(r=>({paramId:r.paramId,displayName:_nullishCoalesce(r.displayName, () => (r.paramId))}))}function v(n,r,s){let e=new Set(h(n).map(t=>t.paramId)),a={};for(let t of n.inputs){if(e.has(t.id)){let i=s({scopeKey:r,inputId:t.id});i!==void 0&&(a[t.id]=i);continue}a[t.id]=_nullishCoalesce(t.default, () => (_schemas.getDefaultValue.call(void 0, t.paramType)))}for(let t of n.outputs)a[t.id]=null;return a}function m(n){let r=n===!1;return{hasPendingChanges:r,hasNeverSolved:r}}function S(n,r,s,e){return n.values[r]=s,e===!1?(n.pendingValues[r]=s,n.hasPendingChanges=!0,{state:n,shouldSolve:!1}):{state:n,shouldSolve:!0}}function y(n,r){if(!_optionalChain([n, 'optionalAccess', _3 => _3.inputs]))return r;let s={};for(let e of n.inputs)e.id in r&&(s[e.id]=r[e.id]);return s}function x(n,r){return n.error="",n.computeErrors=_nullishCoalesce(r.errors, () => ([])),n.computeWarnings=_nullishCoalesce(r.warnings, () => ([])),n.meshes=_nullishCoalesce(r.meshes, () => ([])),Object.assign(n.values,r.outputs),n.pendingValues={},n.hasPendingChanges=!1,n.hasNeverSolved=!1,n}function A(n){let r=n.schema,s=m(_optionalChain([r, 'optionalAccess', _4 => _4.instanceSolve])),e={values:v(r,n.scopeKey,f),error:"",computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:s.hasPendingChanges,hasNeverSolved:s.hasNeverSolved},a=new Set,t=()=>a.forEach(o=>o());function i(){n.driver.solve({...y(r,e.values)}),t()}return{get values(){return e.values},get error(){return e.error},get computeErrors(){return e.computeErrors},get computeWarnings(){return e.computeWarnings},get meshes(){return e.meshes},get hasPendingChanges(){return e.hasPendingChanges},get hasNeverSolved(){return e.hasNeverSolved},get isSolving(){return n.driver.isSolving},setValue(o,l,u=!1){let{shouldSolve:c}=S(e,o,l,_optionalChain([r, 'optionalAccess', _5 => _5.instanceSolve]));c||u?(u&&!c&&(e.pendingValues={},e.hasPendingChanges=!1),i()):t()},solve(){i()},loadValues(o){Object.assign(e.values,o),_optionalChain([r, 'optionalAccess', _6 => _6.instanceSolve])!==!1?i():(e.hasPendingChanges=!0,t())},rebuild(o,l){r=o,_optionalChain([n, 'access', _7 => _7.driver, 'access', _8 => _8.clearCache, 'optionalCall', _9 => _9()]),e.meshes=[],e.error="",e.computeErrors=[],e.computeWarnings=[],e.pendingValues={},e.values=v(o,l,f);let u=m(_optionalChain([o, 'optionalAccess', _10 => _10.instanceSolve]));e.hasPendingChanges=u.hasPendingChanges,e.hasNeverSolved=u.hasNeverSolved,o&&Object.keys(e.values).length>0&&o.instanceSolve!==!1?i():t()},report(o){x(e,o),t()},reportError(o){e.error=o,t()},subscribe(o){return a.add(o),()=>a.delete(o)},notify:t}}function R(n,r={}){let{timeout:s=6e4,onChange:e}=r,a=!1,t=null,i=null;function o(d){a!==d&&(a=d,_optionalChain([e, 'optionalCall', _11 => _11()]))}function l(){_optionalChain([i, 'optionalAccess', _12 => _12.abort, 'call', _13 => _13()]),i=null}async function u(d){l(),i=new AbortController;let{signal:M}=i,k=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${s}ms \u2014 aborting`),_optionalChain([i, 'optionalAccess', _14 => _14.abort, 'call', _15 => _15()])},s);o(!0);try{await n(d,M)}catch(p){(!(p instanceof Error)||p.name!=="AbortError"&&p.name!=="TimeoutError")&&console.error("[Solve/throttle] unhandled error in run:",p)}finally{if(clearTimeout(k),o(!1),i=null,t!==null){let p=t;t=null,u(p)}}}function c(d){a?(t!==null&&console.debug("[Solve/throttle] superseded pending run (latest-wins)"),t=d):u(d)}function I(){t=null,l()}return{trigger:c,get isRunning(){return a},get hasPending(){return t!==null},cancel:I}}function b(n){return T(n)}function T(n){if(n===null||typeof n!="object")return _nullishCoalesce(JSON.stringify(n), () => ("null"));if(Array.isArray(n))return`[${n.map(T).join(",")}]`;let r=n;return`{${Object.keys(r).sort().map(e=>`${JSON.stringify(e)}:${T(r[e])}`).join(",")}}`}function E(n={}){let{max:r=16,meshPolicy:s}=n,e=new Map;function a(o){_optionalChain([o, 'access', _16 => _16.meshes, 'optionalAccess', _17 => _17.length])&&_optionalChain([s, 'optionalAccess', _18 => _18.release, 'call', _19 => _19(o.meshes)])}function t(o){return!s||!_optionalChain([o, 'access', _20 => _20.meshes, 'optionalAccess', _21 => _21.length])?o:{...o,meshes:s.clone(o.meshes)}}function i(o){let l=e.get(o);l!==void 0&&(e.delete(o),a(l))}return{get(o){let l=b(o),u=e.get(l);if(u!==void 0)return e.delete(l),e.set(l,u),console.info(`[Solve/memo] HIT \u2014 served from client memo (${e.size}/${r})`),t(u)},set(o,l){let u=b(o);for(i(u),e.set(u,t(l));e.size>r;){let c=e.keys().next().value;if(c===void 0)break;i(c),console.info(`[Solve/memo] evicted LRU entry (cap ${r})`)}},clear(){e.size>0&&console.info(`[Solve/memo] cleared ${e.size} entries (definition changed)`),e.forEach(a),e.clear()}}}function O(n,r,s={}){let{meshPolicy:e,...a}=s,t=E({meshPolicy:e}),i=R(async(o,l)=>{let u=t.get(o);if(u!==void 0){if(l.aborted)return;r().report(u);return}try{let c=await n(o,l);if(l.aborted){console.debug("[Solve/driver] solve completed after abort \u2014 result discarded");return}t.set(o,c),r().report(c)}catch(c){if(l.aborted){console.debug("[Solve/driver] solve aborted (superseded, cancelled, or timed out)");return}console.warn("[Solve/driver] solve failed:",c),r().reportError(c instanceof Error?c.message:String(c))}},a);return{solve(o){i.trigger(o)},cancel(){i.cancel()},get isSolving(){return i.isRunning},clearCache(){t.clear()}}}exports.applySolveResult = x; exports.applyValueChange = S; exports.buildInitialValues = v; exports.clearExternalValue = C; exports.createAsyncThrottle = R; exports.createRequestResponseDriver = O; exports.createSolveMemo = E; exports.createSolveSession = A; exports.getExternalInputs = h; exports.makeInitialFlags = m; exports.pickInputValues = y; exports.readExternalValue = f; exports.stableInputKey = b; exports.writeExternalValue = P;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _schemas = require('@selvajs/schemas');var H="external";function k(e,r){return`${H}:${e}:${r}`}function X(e){let{scopeKey:r,inputId:s,value:n}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.setItem(k(r,s),JSON.stringify(n))}function R(e){let{scopeKey:r,inputId:s}=e;if(!r||!s||typeof sessionStorage>"u")return;let n=sessionStorage.getItem(k(r,s));if(n!==null)try{return JSON.parse(n)}catch (e2){return}}function Y(e){let{scopeKey:r,inputId:s}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.removeItem(k(r,s))}function I(e){return _schemas.getInputItems.call(void 0, e).filter(r=>_optionalChain([r, 'access', _2 => _2.source, 'optionalAccess', _3 => _3.kind])==="client").map(r=>({paramId:r.paramId,displayName:_nullishCoalesce(r.displayName, () => (r.paramId))}))}function b(e,r,s){let n=new Set(I(e).map(t=>t.paramId)),l={};for(let t of e.inputs){if(n.has(t.id)){let i=s({scopeKey:r,inputId:t.id});i!==void 0&&(l[t.id]=i);continue}l[t.id]=_nullishCoalesce(t.default, () => (_schemas.getDefaultValue.call(void 0, t.paramType)))}for(let t of e.outputs)l[t.id]=null;return l}function w(e){let r=e===!1;return{hasPendingChanges:r,hasNeverSolved:r}}function C(e,r,s,n){return e.values[r]=s,n===!1?(e.pendingValues[r]=s,e.hasPendingChanges=!0,{state:e,shouldSolve:!1}):{state:e,shouldSolve:!0}}function P(e,r){if(!_optionalChain([e, 'optionalAccess', _4 => _4.inputs]))return r;let s={};for(let n of e.inputs)n.id in r&&(s[n.id]=r[n.id]);return s}function V(e,r){return e.error="",e.computeErrors=_nullishCoalesce(r.errors, () => ([])),e.computeWarnings=_nullishCoalesce(r.warnings, () => ([])),e.meshes=_nullishCoalesce(r.meshes, () => ([])),Object.assign(e.values,r.outputs),e.pendingValues={},e.hasPendingChanges=!1,e.hasNeverSolved=!1,e}function Z(e){let r=e.schema,s=w(_optionalChain([r, 'optionalAccess', _5 => _5.instanceSolve])),n={values:b(r,e.scopeKey,R),error:"",computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:s.hasPendingChanges,hasNeverSolved:s.hasNeverSolved},l=new Set,t=()=>l.forEach(o=>o());function i(){e.driver.solve({...P(r,n.values)}),t()}return{get values(){return n.values},get error(){return n.error},get computeErrors(){return n.computeErrors},get computeWarnings(){return n.computeWarnings},get meshes(){return n.meshes},get hasPendingChanges(){return n.hasPendingChanges},get hasNeverSolved(){return n.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(o,a,u=!1){let{shouldSolve:c}=C(n,o,a,_optionalChain([r, 'optionalAccess', _6 => _6.instanceSolve]));c||u?(u&&!c&&(n.pendingValues={},n.hasPendingChanges=!1),i()):t()},solve(){i()},loadValues(o){Object.assign(n.values,o),_optionalChain([r, 'optionalAccess', _7 => _7.instanceSolve])!==!1?i():(n.hasPendingChanges=!0,t())},rebuild(o,a){r=o,_optionalChain([e, 'access', _8 => _8.driver, 'access', _9 => _9.clearCache, 'optionalCall', _10 => _10()]),n.meshes=[],n.error="",n.computeErrors=[],n.computeWarnings=[],n.pendingValues={},n.values=b(o,a,R);let u=w(_optionalChain([o, 'optionalAccess', _11 => _11.instanceSolve]));n.hasPendingChanges=u.hasPendingChanges,n.hasNeverSolved=u.hasNeverSolved,o&&Object.keys(n.values).length>0&&o.instanceSolve!==!1?i():t()},report(o){V(n,o),t()},reportError(o){n.error=o,t()},subscribe(o){return l.add(o),()=>l.delete(o)},notify:t}}function F(e,r={}){let{timeout:s=6e4,onChange:n}=r,l=!1,t=null,i=null;function o(m){l!==m&&(l=m,_optionalChain([n, 'optionalCall', _12 => _12()]))}function a(){_optionalChain([i, 'optionalAccess', _13 => _13.abort, 'call', _14 => _14()]),i=null}async function u(m){a(),i=new AbortController;let{signal:S}=i,y=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${s}ms \u2014 aborting`),_optionalChain([i, 'optionalAccess', _15 => _15.abort, 'call', _16 => _16()])},s);o(!0);try{await e(m,S)}catch(f){(!(f instanceof Error)||f.name!=="AbortError"&&f.name!=="TimeoutError")&&console.error("[Solve/throttle] unhandled error in run:",f)}finally{if(clearTimeout(y),o(!1),i=null,t!==null){let f=t;t=null,u(f)}}}function c(m){l?(t!==null&&console.debug("[Solve/throttle] superseded pending run (latest-wins)"),t=m):u(m)}function v(){t=null,a()}return{trigger:c,get isRunning(){return l},get hasPending(){return t!==null},cancel:v}}function O(e){return $(e)}function $(e){if(e===null||typeof e!="object")return _nullishCoalesce(JSON.stringify(e), () => ("null"));if(Array.isArray(e))return`[${e.map($).join(",")}]`;let r=e;return`{${Object.keys(r).sort().map(n=>`${JSON.stringify(n)}:${$(r[n])}`).join(",")}}`}function j(e={}){let{max:r=16,meshPolicy:s}=e,n=new Map;function l(o){_optionalChain([o, 'access', _17 => _17.meshes, 'optionalAccess', _18 => _18.length])&&_optionalChain([s, 'optionalAccess', _19 => _19.release, 'call', _20 => _20(o.meshes)])}function t(o){return!s||!_optionalChain([o, 'access', _21 => _21.meshes, 'optionalAccess', _22 => _22.length])?o:{...o,meshes:s.clone(o.meshes)}}function i(o){let a=n.get(o);a!==void 0&&(n.delete(o),l(a))}return{get(o){let a=O(o),u=n.get(a);if(u!==void 0)return n.delete(a),n.set(a,u),console.info(`[Solve/memo] HIT \u2014 served from client memo (${n.size}/${r})`),t(u)},set(o,a){let u=O(o);for(i(u),n.set(u,t(a));n.size>r;){let c=n.keys().next().value;if(c===void 0)break;i(c),console.info(`[Solve/memo] evicted LRU entry (cap ${r})`)}},clear(){n.size>0&&console.info(`[Solve/memo] cleared ${n.size} entries (definition changed)`),n.forEach(l),n.clear()}}}function ee(e,r,s={}){let{meshPolicy:n,...l}=s,t=j({meshPolicy:n}),i=F(async(o,a)=>{let u=t.get(o);if(u!==void 0){if(a.aborted)return;r().report(u);return}try{let c=await e(o,a);if(a.aborted){console.debug("[Solve/driver] solve completed after abort \u2014 result discarded");return}let v={...c,values:o};t.set(o,v),r().report(v)}catch(c){if(a.aborted){console.debug("[Solve/driver] solve aborted (superseded, cancelled, or timed out)");return}console.warn("[Solve/driver] solve failed:",c),r().reportError(c instanceof Error?c.message:String(c))}},l);return{solve(o){i.trigger(o)},cancel(){i.cancel()},get isSolving(){return i.isRunning},clearCache(){t.clear()}}}var _compute = require('@selvajs/compute');function re(e){let r={};if(!e)return r;for(let s of e.split(",")){let[n,...l]=s.split(";").map(o=>o.trim()),t=l.find(o=>o.startsWith("dur="));if(!n||!t)continue;let i=Number(t.slice(4));Number.isFinite(i)&&(r[n]=i)}return r}var oe=e=>{throw new Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},te=()=>{throw new Error("Your session has expired. Sign in again in a new tab, then re-run \u2014 your inputs are preserved.")};function se(e){let r=_nullishCoalesce(e.debug, () => (!1)),s=(...i)=>{r&&console.info(...i)},n=null;function l(){if(!e.meshes)throw new Error("createComputeFetchSolveFn: meshes option not configured.");return n||(n=e.meshes.loadRhino()),n}let t=0;return async(i,o)=>{let a=t-Date.now();if(a>0)throw new Error(`Rate limit reached. Try again in ${Math.ceil(a/1e3)}s.`);let u=performance.now(),c=e.inputs(),v=e.outputs(),m=_optionalChain([e, 'access', _23 => _23.channel, 'optionalCall', _24 => _24()]),S=_optionalChain([e, 'access', _25 => _25.versionId, 'optionalCall', _26 => _26()]),y=JSON.stringify({inputs:c,values:i,definitionUrl:e.definitionUrl(),...S?{versionId:S}:m==="draft"?{channel:"draft"}:{}}),f=JSON.stringify(i).length,p;try{p=await fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:y,signal:o})}catch(d){if(o.aborted)return s("[Compute] solve aborted during fetch \u2014 empty result discarded"),{outputs:{}};throw new Error("Request blocked \u2014 your session may have expired. Reload the page to sign in again.",{cause:d})}if(o.aborted)return s("[Compute] solve aborted after headers \u2014 empty result discarded"),{outputs:{}};let J=p.redirected&&new URL(p.url).pathname==="/login";if((p.status===401||J)&&(_nullishCoalesce(e.onSessionExpired, () => (te)))(),!p.ok){if(p.status===503)throw new Error("Compute server is offline or unreachable. Please try again later.");let d=await p.text().catch(()=>""),h={};try{h=JSON.parse(d)}catch (e3){d&&console.warn(`[Compute] non-JSON error body (HTTP ${p.status}):`,d.slice(0,300))}if(p.status===429){let x=Number(p.headers.get("Retry-After"))||Number(h.retryAfter)||5;t=Date.now()+x*1e3,(_nullishCoalesce(e.onRateLimited, () => (oe)))(x)}throw new Error(h.message||`Compute error (HTTP ${p.status})`)}let A=performance.now()-u,B=performance.now(),N=await p.text();if(o.aborted)return s("[Compute] solve aborted mid-download \u2014 empty result discarded"),{outputs:{}};let T=performance.now()-B,U=N.length,g;try{g=JSON.parse(N)}catch(d){throw new Error("Received an invalid response from the server. Reload the page and try again.",{cause:d})}let M=new (0, _compute.GrasshopperResponseProcessor)(g,!1),z=e.meshes?await e.meshes.extract(M.response,{debug:r,rhino:await l()}):[],D={};for(let d of v){let h=M.getValue({byId:d.id},{parseValues:!0});D[d.id]=_nullishCoalesce(h, () => ((d.nickname?M.getValue({byName:d.nickname},{parseValues:!0}):void 0)))}if(r){let d=re(p.headers.get("Server-Timing")),h=_nullishCoalesce(d.total, () => (0)),x=Math.max(0,A-h),E=U/(1024*1024),q=E>=1?`${E.toFixed(2)} MB`:`${(U/1024).toFixed(0)} KB`,L=T>0?(E/(T/1e3)).toFixed(1):"\u221E",K=_optionalChain([performance, 'access', _27 => _27.memory, 'optionalAccess', _28 => _28.usedJSHeapSize]),W=K!==void 0?` | heap=${(K/(1024*1024)).toFixed(0)} MB`:"";if(s(`[Compute] req=${(y.length/1024).toFixed(0)}KB (values ${(f/1024).toFixed(0)}KB) | ttfb=${A.toFixed(0)}ms (network\u2248${x.toFixed(0)} + server ${h.toFixed(0)}) | download=${T.toFixed(0)}ms (${q} @ ${L} MB/s)${W}`),d.selva_cache!==void 0){let _=d.selva_cache?"HIT \u2014 served without calling compute":"miss \u2014 solved on Rhino.Compute";s(`[Compute] \u2514\u2500 cache: ${_}`)}}return{outputs:D,meshes:z,errors:_nullishCoalesce(g.errors, () => ([])),warnings:_nullishCoalesce(g.warnings, () => ([])),source:g}}}exports.applySolveResult = V; exports.applyValueChange = C; exports.buildInitialValues = b; exports.clearExternalValue = Y; exports.createAsyncThrottle = F; exports.createComputeFetchSolveFn = se; exports.createRequestResponseDriver = ee; exports.createSolveMemo = j; exports.createSolveSession = Z; exports.getExternalInputs = I; exports.makeInitialFlags = w; exports.pickInputValues = P; exports.readExternalValue = R; exports.stableInputKey = O; exports.writeExternalValue = X;
2
2
  //# sourceMappingURL=client.cjs.map
package/dist/client.d.cts CHANGED
@@ -1,28 +1,26 @@
1
1
  import { UISchema } from '@selvajs/schemas';
2
- import { a as SolveResult, S as SolveFn } from './solve-fn-DGzPCsDu.cjs';
2
+ import { a as SolveResult, S as SolveFn } from './solve-fn-0hPOtZVD.cjs';
3
+ import { GrasshopperComputeResponse } from '@selvajs/compute';
3
4
 
4
5
  /**
5
- * The transport behind a Solve Session. Never returns outputs directly — results come
6
- * back via the session's `report()`, which lets push transports (a WebSocket streaming
7
- * mesh frames on its own schedule) satisfy the same interface as request/response HTTP.
6
+ * Transport behind a solve session. `solve()` never returns outputs directly — results
7
+ * go through the session's `report()` instead, so a push transport (e.g. a WebSocket
8
+ * streaming mesh frames on its own schedule) can satisfy the same interface as
9
+ * request/response HTTP.
8
10
  */
9
11
  interface SolveDriver {
10
12
  solve(values: Record<string, unknown>): void;
11
13
  cancel(): void;
12
14
  readonly isSolving: boolean;
13
15
  /**
14
- * Drops any cached solve results the driver holds. Optional — only drivers with a
15
- * client-side memo (the request/response driver) implement it. Called on rebuild so a
16
- * definition swap can't serve a stale result from a prior definition's input space.
16
+ * Drops cached solve results. Optional — only the request/response driver has a
17
+ * client-side memo to clear. Call on rebuild: a definition swap must not serve a
18
+ * stale result from the prior definition's input space.
17
19
  */
18
20
  clearCache?(): void;
19
21
  }
20
- /**
21
- * The slice of a SolveSession a driver feeds completed/failed solves back into.
22
- *
23
- * `TMesh` defaults to `unknown` because nothing in this package inspects meshes; a host that
24
- * knows its concrete mesh type (a three.js viewer) narrows it at its own seam.
25
- */
22
+ /** `TMesh` defaults to `unknown`: nothing here inspects meshes. A host with a concrete
23
+ * mesh type (e.g. a three.js viewer) narrows it at its own seam. */
26
24
  interface SolveReporter<TMesh = unknown> {
27
25
  report(result: SolveResult<TMesh>): void;
28
26
  reportError(message: string): void;
@@ -58,38 +56,28 @@ interface SolveSessionArgs {
58
56
  declare function createSolveSession(args: SolveSessionArgs): SolveSession;
59
57
 
60
58
  /**
61
- * Deterministic string key for a set of input values. Object keys are sorted at every
62
- * level so two logically-equal inputs (built in different key order) collide, matching the
63
- * server's stable-input keying intent. Values are plain JSON — the projected solve inputs
64
- * never contain functions or cycles.
59
+ * Deterministic string key for a set of input values. Sorts object keys at every level so
60
+ * two logically-equal inputs built in different key order collide.
65
61
  */
66
62
  declare function stableInputKey(values: Record<string, unknown>): string;
67
63
  /**
68
- * Omit for mesh-free results, or for meshes no consumer takes ownership of: the memo then
69
- * stores and serves them by reference, which is correct precisely because nothing frees
70
- * them behind its back.
71
- *
72
- * @property clone - Called on the way in AND on every hit, so neither the stored entry nor
73
- * a previously served one can be disposed by the other's owner.
74
- * @property release - Must NOT free anything shared with live content — see
75
- * `releaseSceneObjects` in `@selvajs/visualization/parse`, which skips materials for
76
- * exactly that reason.
64
+ * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then
65
+ * stores and serves them by reference, which is safe only because nothing frees them behind
66
+ * its back.
77
67
  */
78
68
  interface MeshPolicy<TMesh> {
69
+ /** Called on the way in and on every hit, so the stored entry and a served copy are never the same instance. */
79
70
  clone(meshes: TMesh[]): TMesh[];
71
+ /** Must not free anything shared with live content (e.g. materials another object still uses). */
80
72
  release(meshes: TMesh[]): void;
81
73
  }
82
74
  interface SolveMemoOptions<TMesh> {
83
- /**
84
- * Entry cap (entries, not bytes). Solve results can be large, so the default is
85
- * deliberately small — this targets the tight slider-scrub loop, not a durable cache.
86
- */
75
+ /** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */
87
76
  max?: number;
88
77
  meshPolicy?: MeshPolicy<TMesh>;
89
78
  }
90
79
  interface SolveMemo<TMesh = unknown> {
91
80
  get(values: Record<string, unknown>): SolveResult<TMesh> | undefined;
92
- /** Evicts the LRU tail if `set` pushes the map past its cap. */
93
81
  set(values: Record<string, unknown>, result: SolveResult<TMesh>): void;
94
82
  clear(): void;
95
83
  }
@@ -179,4 +167,55 @@ interface AsyncThrottle<T> {
179
167
  }
180
168
  declare function createAsyncThrottle<T>(run: (values: T, signal: AbortSignal) => Promise<void>, options?: AsyncThrottleOptions): AsyncThrottle<T>;
181
169
 
182
- export { type AsyncThrottle, type ExternalInput, type ExternalReader, type ExternalValueRef, type MeshPolicy, type RequestResponseDriverOptions, type SolveDriver, SolveFn, type SolveMemo, type SolveMemoOptions, type SolveReporter, SolveResult, type SolveSession, type SolveSessionArgs, type SolveSessionState, applySolveResult, applyValueChange, buildInitialValues, clearExternalValue, createAsyncThrottle, createRequestResponseDriver, createSolveMemo, createSolveSession, getExternalInputs, makeInitialFlags, pickInputValues, readExternalValue, stableInputKey, writeExternalValue };
170
+ /**
171
+ * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a
172
+ * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +
173
+ * `SolveEngine` produce server-side) and unpacks the result.
174
+ *
175
+ * Ports the resilience behavior an interactive solve endpoint needs regardless
176
+ * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response
177
+ * guarding, and abort handling at every await point — so a new consumer gets it
178
+ * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:
179
+ * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a
180
+ * viewer-enabled consumer supplies its own `meshes.extract` hook (typically
181
+ * `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).
182
+ */
183
+
184
+ interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {
185
+ /** The compute endpoint to POST to, e.g. `/api/compute`. */
186
+ endpoint: string;
187
+ /** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */
188
+ definitionUrl: () => string;
189
+ /** Schema input params, sent as the request body's `inputs`. */
190
+ inputs: () => Array<{
191
+ id: string;
192
+ }>;
193
+ /** Schema output params — drives byId/byName output extraction after a solve. */
194
+ outputs: () => Array<{
195
+ id: string;
196
+ nickname?: string;
197
+ }>;
198
+ channel?: () => 'live' | 'draft' | undefined;
199
+ versionId?: () => string | null | undefined;
200
+ /**
201
+ * Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`
202
+ * and no rhino3dm module is ever loaded. `loadRhino` is called lazily on the
203
+ * first solve that needs meshes and cached for the lifetime of this `SolveFn`.
204
+ */
205
+ meshes?: {
206
+ loadRhino: () => Promise<TRhino>;
207
+ extract: (response: GrasshopperComputeResponse, opts: {
208
+ debug: boolean;
209
+ rhino: TRhino;
210
+ }) => TMesh[] | Promise<TMesh[]>;
211
+ };
212
+ /** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */
213
+ debug?: boolean;
214
+ /** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */
215
+ onRateLimited?: (retryAfterSeconds: number) => void;
216
+ /** Called on a 401 or a login-page redirect. Default: throws an actionable "reload to sign in again" Error. */
217
+ onSessionExpired?: () => void;
218
+ }
219
+ declare function createComputeFetchSolveFn<TMesh = unknown, TRhino = unknown>(opts: ComputeFetchSolveFnOptions<TMesh, TRhino>): SolveFn<TMesh, GrasshopperComputeResponse>;
220
+
221
+ export { type AsyncThrottle, type ComputeFetchSolveFnOptions, type ExternalInput, type ExternalReader, type ExternalValueRef, type MeshPolicy, type RequestResponseDriverOptions, type SolveDriver, SolveFn, type SolveMemo, type SolveMemoOptions, type SolveReporter, SolveResult, type SolveSession, type SolveSessionArgs, type SolveSessionState, applySolveResult, applyValueChange, buildInitialValues, clearExternalValue, createAsyncThrottle, createComputeFetchSolveFn, createRequestResponseDriver, createSolveMemo, createSolveSession, getExternalInputs, makeInitialFlags, pickInputValues, readExternalValue, stableInputKey, writeExternalValue };
package/dist/client.d.ts CHANGED
@@ -1,28 +1,26 @@
1
1
  import { UISchema } from '@selvajs/schemas';
2
- import { a as SolveResult, S as SolveFn } from './solve-fn-DGzPCsDu.js';
2
+ import { a as SolveResult, S as SolveFn } from './solve-fn-0hPOtZVD.js';
3
+ import { GrasshopperComputeResponse } from '@selvajs/compute';
3
4
 
4
5
  /**
5
- * The transport behind a Solve Session. Never returns outputs directly — results come
6
- * back via the session's `report()`, which lets push transports (a WebSocket streaming
7
- * mesh frames on its own schedule) satisfy the same interface as request/response HTTP.
6
+ * Transport behind a solve session. `solve()` never returns outputs directly — results
7
+ * go through the session's `report()` instead, so a push transport (e.g. a WebSocket
8
+ * streaming mesh frames on its own schedule) can satisfy the same interface as
9
+ * request/response HTTP.
8
10
  */
9
11
  interface SolveDriver {
10
12
  solve(values: Record<string, unknown>): void;
11
13
  cancel(): void;
12
14
  readonly isSolving: boolean;
13
15
  /**
14
- * Drops any cached solve results the driver holds. Optional — only drivers with a
15
- * client-side memo (the request/response driver) implement it. Called on rebuild so a
16
- * definition swap can't serve a stale result from a prior definition's input space.
16
+ * Drops cached solve results. Optional — only the request/response driver has a
17
+ * client-side memo to clear. Call on rebuild: a definition swap must not serve a
18
+ * stale result from the prior definition's input space.
17
19
  */
18
20
  clearCache?(): void;
19
21
  }
20
- /**
21
- * The slice of a SolveSession a driver feeds completed/failed solves back into.
22
- *
23
- * `TMesh` defaults to `unknown` because nothing in this package inspects meshes; a host that
24
- * knows its concrete mesh type (a three.js viewer) narrows it at its own seam.
25
- */
22
+ /** `TMesh` defaults to `unknown`: nothing here inspects meshes. A host with a concrete
23
+ * mesh type (e.g. a three.js viewer) narrows it at its own seam. */
26
24
  interface SolveReporter<TMesh = unknown> {
27
25
  report(result: SolveResult<TMesh>): void;
28
26
  reportError(message: string): void;
@@ -58,38 +56,28 @@ interface SolveSessionArgs {
58
56
  declare function createSolveSession(args: SolveSessionArgs): SolveSession;
59
57
 
60
58
  /**
61
- * Deterministic string key for a set of input values. Object keys are sorted at every
62
- * level so two logically-equal inputs (built in different key order) collide, matching the
63
- * server's stable-input keying intent. Values are plain JSON — the projected solve inputs
64
- * never contain functions or cycles.
59
+ * Deterministic string key for a set of input values. Sorts object keys at every level so
60
+ * two logically-equal inputs built in different key order collide.
65
61
  */
66
62
  declare function stableInputKey(values: Record<string, unknown>): string;
67
63
  /**
68
- * Omit for mesh-free results, or for meshes no consumer takes ownership of: the memo then
69
- * stores and serves them by reference, which is correct precisely because nothing frees
70
- * them behind its back.
71
- *
72
- * @property clone - Called on the way in AND on every hit, so neither the stored entry nor
73
- * a previously served one can be disposed by the other's owner.
74
- * @property release - Must NOT free anything shared with live content — see
75
- * `releaseSceneObjects` in `@selvajs/visualization/parse`, which skips materials for
76
- * exactly that reason.
64
+ * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then
65
+ * stores and serves them by reference, which is safe only because nothing frees them behind
66
+ * its back.
77
67
  */
78
68
  interface MeshPolicy<TMesh> {
69
+ /** Called on the way in and on every hit, so the stored entry and a served copy are never the same instance. */
79
70
  clone(meshes: TMesh[]): TMesh[];
71
+ /** Must not free anything shared with live content (e.g. materials another object still uses). */
80
72
  release(meshes: TMesh[]): void;
81
73
  }
82
74
  interface SolveMemoOptions<TMesh> {
83
- /**
84
- * Entry cap (entries, not bytes). Solve results can be large, so the default is
85
- * deliberately small — this targets the tight slider-scrub loop, not a durable cache.
86
- */
75
+ /** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */
87
76
  max?: number;
88
77
  meshPolicy?: MeshPolicy<TMesh>;
89
78
  }
90
79
  interface SolveMemo<TMesh = unknown> {
91
80
  get(values: Record<string, unknown>): SolveResult<TMesh> | undefined;
92
- /** Evicts the LRU tail if `set` pushes the map past its cap. */
93
81
  set(values: Record<string, unknown>, result: SolveResult<TMesh>): void;
94
82
  clear(): void;
95
83
  }
@@ -179,4 +167,55 @@ interface AsyncThrottle<T> {
179
167
  }
180
168
  declare function createAsyncThrottle<T>(run: (values: T, signal: AbortSignal) => Promise<void>, options?: AsyncThrottleOptions): AsyncThrottle<T>;
181
169
 
182
- export { type AsyncThrottle, type ExternalInput, type ExternalReader, type ExternalValueRef, type MeshPolicy, type RequestResponseDriverOptions, type SolveDriver, SolveFn, type SolveMemo, type SolveMemoOptions, type SolveReporter, SolveResult, type SolveSession, type SolveSessionArgs, type SolveSessionState, applySolveResult, applyValueChange, buildInitialValues, clearExternalValue, createAsyncThrottle, createRequestResponseDriver, createSolveMemo, createSolveSession, getExternalInputs, makeInitialFlags, pickInputValues, readExternalValue, stableInputKey, writeExternalValue };
170
+ /**
171
+ * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a
172
+ * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +
173
+ * `SolveEngine` produce server-side) and unpacks the result.
174
+ *
175
+ * Ports the resilience behavior an interactive solve endpoint needs regardless
176
+ * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response
177
+ * guarding, and abort handling at every await point — so a new consumer gets it
178
+ * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:
179
+ * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a
180
+ * viewer-enabled consumer supplies its own `meshes.extract` hook (typically
181
+ * `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).
182
+ */
183
+
184
+ interface ComputeFetchSolveFnOptions<TMesh = unknown, TRhino = unknown> {
185
+ /** The compute endpoint to POST to, e.g. `/api/compute`. */
186
+ endpoint: string;
187
+ /** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */
188
+ definitionUrl: () => string;
189
+ /** Schema input params, sent as the request body's `inputs`. */
190
+ inputs: () => Array<{
191
+ id: string;
192
+ }>;
193
+ /** Schema output params — drives byId/byName output extraction after a solve. */
194
+ outputs: () => Array<{
195
+ id: string;
196
+ nickname?: string;
197
+ }>;
198
+ channel?: () => 'live' | 'draft' | undefined;
199
+ versionId?: () => string | null | undefined;
200
+ /**
201
+ * Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`
202
+ * and no rhino3dm module is ever loaded. `loadRhino` is called lazily on the
203
+ * first solve that needs meshes and cached for the lifetime of this `SolveFn`.
204
+ */
205
+ meshes?: {
206
+ loadRhino: () => Promise<TRhino>;
207
+ extract: (response: GrasshopperComputeResponse, opts: {
208
+ debug: boolean;
209
+ rhino: TRhino;
210
+ }) => TMesh[] | Promise<TMesh[]>;
211
+ };
212
+ /** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */
213
+ debug?: boolean;
214
+ /** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */
215
+ onRateLimited?: (retryAfterSeconds: number) => void;
216
+ /** Called on a 401 or a login-page redirect. Default: throws an actionable "reload to sign in again" Error. */
217
+ onSessionExpired?: () => void;
218
+ }
219
+ declare function createComputeFetchSolveFn<TMesh = unknown, TRhino = unknown>(opts: ComputeFetchSolveFnOptions<TMesh, TRhino>): SolveFn<TMesh, GrasshopperComputeResponse>;
220
+
221
+ export { type AsyncThrottle, type ComputeFetchSolveFnOptions, type ExternalInput, type ExternalReader, type ExternalValueRef, type MeshPolicy, type RequestResponseDriverOptions, type SolveDriver, SolveFn, type SolveMemo, type SolveMemoOptions, type SolveReporter, SolveResult, type SolveSession, type SolveSessionArgs, type SolveSessionState, applySolveResult, applyValueChange, buildInitialValues, clearExternalValue, createAsyncThrottle, createComputeFetchSolveFn, createRequestResponseDriver, createSolveMemo, createSolveSession, getExternalInputs, makeInitialFlags, pickInputValues, readExternalValue, stableInputKey, writeExternalValue };
package/dist/client.js CHANGED
@@ -1,2 +1,2 @@
1
- import{getInputItems as w}from"@selvajs/schemas";var V="external";function g(n,r){return`${V}:${n}:${r}`}function P(n){let{scopeKey:r,inputId:s,value:e}=n;!r||!s||typeof sessionStorage>"u"||sessionStorage.setItem(g(r,s),JSON.stringify(e))}function f(n){let{scopeKey:r,inputId:s}=n;if(!r||!s||typeof sessionStorage>"u")return;let e=sessionStorage.getItem(g(r,s));if(e!==null)try{return JSON.parse(e)}catch{return}}function C(n){let{scopeKey:r,inputId:s}=n;!r||!s||typeof sessionStorage>"u"||sessionStorage.removeItem(g(r,s))}function h(n){return w(n).filter(r=>r.source?.kind==="client").map(r=>({paramId:r.paramId,displayName:r.displayName??r.paramId}))}import{getDefaultValue as j}from"@selvajs/schemas";function v(n,r,s){let e=new Set(h(n).map(t=>t.paramId)),a={};for(let t of n.inputs){if(e.has(t.id)){let i=s({scopeKey:r,inputId:t.id});i!==void 0&&(a[t.id]=i);continue}a[t.id]=t.default??j(t.paramType)}for(let t of n.outputs)a[t.id]=null;return a}function m(n){let r=n===!1;return{hasPendingChanges:r,hasNeverSolved:r}}function S(n,r,s,e){return n.values[r]=s,e===!1?(n.pendingValues[r]=s,n.hasPendingChanges=!0,{state:n,shouldSolve:!1}):{state:n,shouldSolve:!0}}function y(n,r){if(!n?.inputs)return r;let s={};for(let e of n.inputs)e.id in r&&(s[e.id]=r[e.id]);return s}function x(n,r){return n.error="",n.computeErrors=r.errors??[],n.computeWarnings=r.warnings??[],n.meshes=r.meshes??[],Object.assign(n.values,r.outputs),n.pendingValues={},n.hasPendingChanges=!1,n.hasNeverSolved=!1,n}function A(n){let r=n.schema,s=m(r?.instanceSolve),e={values:v(r,n.scopeKey,f),error:"",computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:s.hasPendingChanges,hasNeverSolved:s.hasNeverSolved},a=new Set,t=()=>a.forEach(o=>o());function i(){n.driver.solve({...y(r,e.values)}),t()}return{get values(){return e.values},get error(){return e.error},get computeErrors(){return e.computeErrors},get computeWarnings(){return e.computeWarnings},get meshes(){return e.meshes},get hasPendingChanges(){return e.hasPendingChanges},get hasNeverSolved(){return e.hasNeverSolved},get isSolving(){return n.driver.isSolving},setValue(o,l,u=!1){let{shouldSolve:c}=S(e,o,l,r?.instanceSolve);c||u?(u&&!c&&(e.pendingValues={},e.hasPendingChanges=!1),i()):t()},solve(){i()},loadValues(o){Object.assign(e.values,o),r?.instanceSolve!==!1?i():(e.hasPendingChanges=!0,t())},rebuild(o,l){r=o,n.driver.clearCache?.(),e.meshes=[],e.error="",e.computeErrors=[],e.computeWarnings=[],e.pendingValues={},e.values=v(o,l,f);let u=m(o?.instanceSolve);e.hasPendingChanges=u.hasPendingChanges,e.hasNeverSolved=u.hasNeverSolved,o&&Object.keys(e.values).length>0&&o.instanceSolve!==!1?i():t()},report(o){x(e,o),t()},reportError(o){e.error=o,t()},subscribe(o){return a.add(o),()=>a.delete(o)},notify:t}}function R(n,r={}){let{timeout:s=6e4,onChange:e}=r,a=!1,t=null,i=null;function o(d){a!==d&&(a=d,e?.())}function l(){i?.abort(),i=null}async function u(d){l(),i=new AbortController;let{signal:M}=i,k=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${s}ms \u2014 aborting`),i?.abort()},s);o(!0);try{await n(d,M)}catch(p){(!(p instanceof Error)||p.name!=="AbortError"&&p.name!=="TimeoutError")&&console.error("[Solve/throttle] unhandled error in run:",p)}finally{if(clearTimeout(k),o(!1),i=null,t!==null){let p=t;t=null,u(p)}}}function c(d){a?(t!==null&&console.debug("[Solve/throttle] superseded pending run (latest-wins)"),t=d):u(d)}function I(){t=null,l()}return{trigger:c,get isRunning(){return a},get hasPending(){return t!==null},cancel:I}}function b(n){return T(n)}function T(n){if(n===null||typeof n!="object")return JSON.stringify(n)??"null";if(Array.isArray(n))return`[${n.map(T).join(",")}]`;let r=n;return`{${Object.keys(r).sort().map(e=>`${JSON.stringify(e)}:${T(r[e])}`).join(",")}}`}function E(n={}){let{max:r=16,meshPolicy:s}=n,e=new Map;function a(o){o.meshes?.length&&s?.release(o.meshes)}function t(o){return!s||!o.meshes?.length?o:{...o,meshes:s.clone(o.meshes)}}function i(o){let l=e.get(o);l!==void 0&&(e.delete(o),a(l))}return{get(o){let l=b(o),u=e.get(l);if(u!==void 0)return e.delete(l),e.set(l,u),console.info(`[Solve/memo] HIT \u2014 served from client memo (${e.size}/${r})`),t(u)},set(o,l){let u=b(o);for(i(u),e.set(u,t(l));e.size>r;){let c=e.keys().next().value;if(c===void 0)break;i(c),console.info(`[Solve/memo] evicted LRU entry (cap ${r})`)}},clear(){e.size>0&&console.info(`[Solve/memo] cleared ${e.size} entries (definition changed)`),e.forEach(a),e.clear()}}}function O(n,r,s={}){let{meshPolicy:e,...a}=s,t=E({meshPolicy:e}),i=R(async(o,l)=>{let u=t.get(o);if(u!==void 0){if(l.aborted)return;r().report(u);return}try{let c=await n(o,l);if(l.aborted){console.debug("[Solve/driver] solve completed after abort \u2014 result discarded");return}t.set(o,c),r().report(c)}catch(c){if(l.aborted){console.debug("[Solve/driver] solve aborted (superseded, cancelled, or timed out)");return}console.warn("[Solve/driver] solve failed:",c),r().reportError(c instanceof Error?c.message:String(c))}},a);return{solve(o){i.trigger(o)},cancel(){i.cancel()},get isSolving(){return i.isRunning},clearCache(){t.clear()}}}export{x as applySolveResult,S as applyValueChange,v as buildInitialValues,C as clearExternalValue,R as createAsyncThrottle,O as createRequestResponseDriver,E as createSolveMemo,A as createSolveSession,h as getExternalInputs,m as makeInitialFlags,y as pickInputValues,f as readExternalValue,b as stableInputKey,P as writeExternalValue};
1
+ import{getInputItems as G}from"@selvajs/schemas";var H="external";function k(e,r){return`${H}:${e}:${r}`}function X(e){let{scopeKey:r,inputId:s,value:n}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.setItem(k(r,s),JSON.stringify(n))}function R(e){let{scopeKey:r,inputId:s}=e;if(!r||!s||typeof sessionStorage>"u")return;let n=sessionStorage.getItem(k(r,s));if(n!==null)try{return JSON.parse(n)}catch{return}}function Y(e){let{scopeKey:r,inputId:s}=e;!r||!s||typeof sessionStorage>"u"||sessionStorage.removeItem(k(r,s))}function I(e){return G(e).filter(r=>r.source?.kind==="client").map(r=>({paramId:r.paramId,displayName:r.displayName??r.paramId}))}import{getDefaultValue as Q}from"@selvajs/schemas";function b(e,r,s){let n=new Set(I(e).map(t=>t.paramId)),l={};for(let t of e.inputs){if(n.has(t.id)){let i=s({scopeKey:r,inputId:t.id});i!==void 0&&(l[t.id]=i);continue}l[t.id]=t.default??Q(t.paramType)}for(let t of e.outputs)l[t.id]=null;return l}function w(e){let r=e===!1;return{hasPendingChanges:r,hasNeverSolved:r}}function C(e,r,s,n){return e.values[r]=s,n===!1?(e.pendingValues[r]=s,e.hasPendingChanges=!0,{state:e,shouldSolve:!1}):{state:e,shouldSolve:!0}}function P(e,r){if(!e?.inputs)return r;let s={};for(let n of e.inputs)n.id in r&&(s[n.id]=r[n.id]);return s}function V(e,r){return e.error="",e.computeErrors=r.errors??[],e.computeWarnings=r.warnings??[],e.meshes=r.meshes??[],Object.assign(e.values,r.outputs),e.pendingValues={},e.hasPendingChanges=!1,e.hasNeverSolved=!1,e}function Z(e){let r=e.schema,s=w(r?.instanceSolve),n={values:b(r,e.scopeKey,R),error:"",computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:s.hasPendingChanges,hasNeverSolved:s.hasNeverSolved},l=new Set,t=()=>l.forEach(o=>o());function i(){e.driver.solve({...P(r,n.values)}),t()}return{get values(){return n.values},get error(){return n.error},get computeErrors(){return n.computeErrors},get computeWarnings(){return n.computeWarnings},get meshes(){return n.meshes},get hasPendingChanges(){return n.hasPendingChanges},get hasNeverSolved(){return n.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(o,a,u=!1){let{shouldSolve:c}=C(n,o,a,r?.instanceSolve);c||u?(u&&!c&&(n.pendingValues={},n.hasPendingChanges=!1),i()):t()},solve(){i()},loadValues(o){Object.assign(n.values,o),r?.instanceSolve!==!1?i():(n.hasPendingChanges=!0,t())},rebuild(o,a){r=o,e.driver.clearCache?.(),n.meshes=[],n.error="",n.computeErrors=[],n.computeWarnings=[],n.pendingValues={},n.values=b(o,a,R);let u=w(o?.instanceSolve);n.hasPendingChanges=u.hasPendingChanges,n.hasNeverSolved=u.hasNeverSolved,o&&Object.keys(n.values).length>0&&o.instanceSolve!==!1?i():t()},report(o){V(n,o),t()},reportError(o){n.error=o,t()},subscribe(o){return l.add(o),()=>l.delete(o)},notify:t}}function F(e,r={}){let{timeout:s=6e4,onChange:n}=r,l=!1,t=null,i=null;function o(m){l!==m&&(l=m,n?.())}function a(){i?.abort(),i=null}async function u(m){a(),i=new AbortController;let{signal:S}=i,y=setTimeout(()=>{console.warn(`[Solve/throttle] run exceeded ${s}ms \u2014 aborting`),i?.abort()},s);o(!0);try{await e(m,S)}catch(f){(!(f instanceof Error)||f.name!=="AbortError"&&f.name!=="TimeoutError")&&console.error("[Solve/throttle] unhandled error in run:",f)}finally{if(clearTimeout(y),o(!1),i=null,t!==null){let f=t;t=null,u(f)}}}function c(m){l?(t!==null&&console.debug("[Solve/throttle] superseded pending run (latest-wins)"),t=m):u(m)}function v(){t=null,a()}return{trigger:c,get isRunning(){return l},get hasPending(){return t!==null},cancel:v}}function O(e){return $(e)}function $(e){if(e===null||typeof e!="object")return JSON.stringify(e)??"null";if(Array.isArray(e))return`[${e.map($).join(",")}]`;let r=e;return`{${Object.keys(r).sort().map(n=>`${JSON.stringify(n)}:${$(r[n])}`).join(",")}}`}function j(e={}){let{max:r=16,meshPolicy:s}=e,n=new Map;function l(o){o.meshes?.length&&s?.release(o.meshes)}function t(o){return!s||!o.meshes?.length?o:{...o,meshes:s.clone(o.meshes)}}function i(o){let a=n.get(o);a!==void 0&&(n.delete(o),l(a))}return{get(o){let a=O(o),u=n.get(a);if(u!==void 0)return n.delete(a),n.set(a,u),console.info(`[Solve/memo] HIT \u2014 served from client memo (${n.size}/${r})`),t(u)},set(o,a){let u=O(o);for(i(u),n.set(u,t(a));n.size>r;){let c=n.keys().next().value;if(c===void 0)break;i(c),console.info(`[Solve/memo] evicted LRU entry (cap ${r})`)}},clear(){n.size>0&&console.info(`[Solve/memo] cleared ${n.size} entries (definition changed)`),n.forEach(l),n.clear()}}}function ee(e,r,s={}){let{meshPolicy:n,...l}=s,t=j({meshPolicy:n}),i=F(async(o,a)=>{let u=t.get(o);if(u!==void 0){if(a.aborted)return;r().report(u);return}try{let c=await e(o,a);if(a.aborted){console.debug("[Solve/driver] solve completed after abort \u2014 result discarded");return}let v={...c,values:o};t.set(o,v),r().report(v)}catch(c){if(a.aborted){console.debug("[Solve/driver] solve aborted (superseded, cancelled, or timed out)");return}console.warn("[Solve/driver] solve failed:",c),r().reportError(c instanceof Error?c.message:String(c))}},l);return{solve(o){i.trigger(o)},cancel(){i.cancel()},get isSolving(){return i.isRunning},clearCache(){t.clear()}}}import{GrasshopperResponseProcessor as ne}from"@selvajs/compute";function re(e){let r={};if(!e)return r;for(let s of e.split(",")){let[n,...l]=s.split(";").map(o=>o.trim()),t=l.find(o=>o.startsWith("dur="));if(!n||!t)continue;let i=Number(t.slice(4));Number.isFinite(i)&&(r[n]=i)}return r}var oe=e=>{throw new Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},te=()=>{throw new Error("Your session has expired. Sign in again in a new tab, then re-run \u2014 your inputs are preserved.")};function se(e){let r=e.debug??!1,s=(...i)=>{r&&console.info(...i)},n=null;function l(){if(!e.meshes)throw new Error("createComputeFetchSolveFn: meshes option not configured.");return n||(n=e.meshes.loadRhino()),n}let t=0;return async(i,o)=>{let a=t-Date.now();if(a>0)throw new Error(`Rate limit reached. Try again in ${Math.ceil(a/1e3)}s.`);let u=performance.now(),c=e.inputs(),v=e.outputs(),m=e.channel?.(),S=e.versionId?.(),y=JSON.stringify({inputs:c,values:i,definitionUrl:e.definitionUrl(),...S?{versionId:S}:m==="draft"?{channel:"draft"}:{}}),f=JSON.stringify(i).length,p;try{p=await fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:y,signal:o})}catch(d){if(o.aborted)return s("[Compute] solve aborted during fetch \u2014 empty result discarded"),{outputs:{}};throw new Error("Request blocked \u2014 your session may have expired. Reload the page to sign in again.",{cause:d})}if(o.aborted)return s("[Compute] solve aborted after headers \u2014 empty result discarded"),{outputs:{}};let J=p.redirected&&new URL(p.url).pathname==="/login";if((p.status===401||J)&&(e.onSessionExpired??te)(),!p.ok){if(p.status===503)throw new Error("Compute server is offline or unreachable. Please try again later.");let d=await p.text().catch(()=>""),h={};try{h=JSON.parse(d)}catch{d&&console.warn(`[Compute] non-JSON error body (HTTP ${p.status}):`,d.slice(0,300))}if(p.status===429){let x=Number(p.headers.get("Retry-After"))||Number(h.retryAfter)||5;t=Date.now()+x*1e3,(e.onRateLimited??oe)(x)}throw new Error(h.message||`Compute error (HTTP ${p.status})`)}let A=performance.now()-u,B=performance.now(),N=await p.text();if(o.aborted)return s("[Compute] solve aborted mid-download \u2014 empty result discarded"),{outputs:{}};let T=performance.now()-B,U=N.length,g;try{g=JSON.parse(N)}catch(d){throw new Error("Received an invalid response from the server. Reload the page and try again.",{cause:d})}let M=new ne(g,!1),z=e.meshes?await e.meshes.extract(M.response,{debug:r,rhino:await l()}):[],D={};for(let d of v){let h=M.getValue({byId:d.id},{parseValues:!0});D[d.id]=h??(d.nickname?M.getValue({byName:d.nickname},{parseValues:!0}):void 0)}if(r){let d=re(p.headers.get("Server-Timing")),h=d.total??0,x=Math.max(0,A-h),E=U/(1024*1024),q=E>=1?`${E.toFixed(2)} MB`:`${(U/1024).toFixed(0)} KB`,L=T>0?(E/(T/1e3)).toFixed(1):"\u221E",K=performance.memory?.usedJSHeapSize,W=K!==void 0?` | heap=${(K/(1024*1024)).toFixed(0)} MB`:"";if(s(`[Compute] req=${(y.length/1024).toFixed(0)}KB (values ${(f/1024).toFixed(0)}KB) | ttfb=${A.toFixed(0)}ms (network\u2248${x.toFixed(0)} + server ${h.toFixed(0)}) | download=${T.toFixed(0)}ms (${q} @ ${L} MB/s)${W}`),d.selva_cache!==void 0){let _=d.selva_cache?"HIT \u2014 served without calling compute":"miss \u2014 solved on Rhino.Compute";s(`[Compute] \u2514\u2500 cache: ${_}`)}}return{outputs:D,meshes:z,errors:g.errors??[],warnings:g.warnings??[],source:g}}}export{V as applySolveResult,C as applyValueChange,b as buildInitialValues,Y as clearExternalValue,F as createAsyncThrottle,se as createComputeFetchSolveFn,ee as createRequestResponseDriver,j as createSolveMemo,Z as createSolveSession,I as getExternalInputs,w as makeInitialFlags,P as pickInputValues,R as readExternalValue,O as stableInputKey,X as writeExternalValue};
2
2
  //# sourceMappingURL=client.js.map