@selvajs/solve 1.1.1 → 1.2.0

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as SolveResult, t as SolveFn } from "./solve-fn-lsWj61m5.js";
2
- import { UISchema } from "@selvajs/schemas";
1
+ import { c as SolveResult, n as decodeOutcome, o as SolveDiagnostic, r as finalizeResult, s as SolveFn, t as SolveOutcome } from "./outcome-ePNabW7y.js";
2
+ import { ProgressPayload, SolveEvent, UISchema } from "@selvajs/schemas";
3
3
  import { GrasshopperComputeResponse } from "@selvajs/compute/grasshopper";
4
4
  //#region src/client/drivers/driver.d.ts
5
5
  /**
@@ -27,6 +27,21 @@ interface SolveDriver {
27
27
  * stale result from the prior definition's input space.
28
28
  */
29
29
  clearCache?(): void;
30
+ /**
31
+ * Live events from the running solve, when the transport carries them (a WebSocket frame
32
+ * locally, an SSE stream in cloud mode). Returns the unsubscribe. A driver without a live
33
+ * channel leaves this undefined and the session simply never sees events.
34
+ */
35
+ onEvent?(listener: (event: SolveEvent) => void): () => void;
36
+ }
37
+ /**
38
+ * Where a request/response driver gets its live events from. The stream is owned by the host
39
+ * (it outlives individual solves), so the driver only subscribes and forwards.
40
+ */
41
+ interface SolveEventSource {
42
+ subscribe(listener: (event: SolveEvent) => void): () => void;
43
+ /** Asks the server to abort the solve currently in flight, if the source knows which one that is. */
44
+ cancelCurrent?(): void;
30
45
  }
31
46
  /** `TMesh` defaults to `unknown`: nothing here inspects meshes. A host with a concrete
32
47
  * mesh type (e.g. a three.js viewer) narrows it at its own seam. */
@@ -35,6 +50,28 @@ interface SolveReporter<TMesh = unknown> {
35
50
  reportError(message: string): void;
36
51
  }
37
52
  //#endregion
53
+ //#region src/client/events/live-solve.d.ts
54
+ interface LiveSolveState {
55
+ /** The solve these events belong to; null before the first event. */
56
+ readonly solveId: string | null;
57
+ /** Between that solve's first event and its `solveEnded`. */
58
+ readonly running: boolean;
59
+ /** `diagnostic` events in arrival order. */
60
+ readonly diagnostics: readonly SolveDiagnostic[];
61
+ /** The newest `progress` per source. A report without a source is keyed by `''`. */
62
+ readonly progress: ReadonlyMap<string, ProgressPayload>;
63
+ /** `solveEnded`'s payload once it arrives: `ok`, `blocked`, `aborted` or `error`. */
64
+ readonly ended: {
65
+ readonly kind: string;
66
+ } | null;
67
+ /** Events of kinds this package does not know, oldest first. */
68
+ readonly other: readonly SolveEvent[];
69
+ /** Highest `seq` applied for this solve. A late or repeated delivery at or below it is ignored. */
70
+ readonly lastSeq: number;
71
+ }
72
+ export declare const EMPTY_LIVE_SOLVE: LiveSolveState;
73
+ export declare function reduceLiveSolve(state: LiveSolveState, event: SolveEvent): LiveSolveState;
74
+ //#endregion
38
75
  //#region src/client/external-storage.d.ts
39
76
  interface ExternalValueRef {
40
77
  scopeKey: string;
@@ -51,7 +88,7 @@ interface ExternalInput {
51
88
  }
52
89
  export declare function getExternalInputs(schema: UISchema): ExternalInput[];
53
90
  //#endregion
54
- //#region src/client/solve-session-core.d.ts
91
+ //#region src/client/session/solve-session-core.d.ts
55
92
  /**
56
93
  * The slice of a reported result the session keeps addressable, so a host can commit exactly
57
94
  * what the viewer is showing — `source` and `values` included, already memo-correct.
@@ -68,6 +105,29 @@ interface SolveSessionState {
68
105
  error: string;
69
106
  computeErrors: string[];
70
107
  computeWarnings: string[];
108
+ /**
109
+ * The same messages with their level, source and `isGate`. What decides whether a solve
110
+ * interrupts the user; the two string arrays above stay the plain display lists.
111
+ */
112
+ diagnostics: SolveDiagnostic[];
113
+ /**
114
+ * The last solve was refused by a guard in the definition. Separate from `computeErrors`
115
+ * being non-empty: a solve can report errors and still return usable outputs, and the UI
116
+ * needs to tell "solved, with complaints" from "would not solve".
117
+ */
118
+ blocked: boolean;
119
+ /**
120
+ * The last solve raised messages the user has not acknowledged yet. While true the result
121
+ * is withheld: `meshes` and the solve's outputs stay at their previous values until
122
+ * `acknowledge()` releases them.
123
+ */
124
+ awaitingAck: boolean;
125
+ /**
126
+ * The withheld result, kept whole so `acknowledge()` can apply it without a re-solve.
127
+ * Null whenever `awaitingAck` is false. A newer solve replaces it: the user is always
128
+ * confirming the most recent result, never a stale one.
129
+ */
130
+ heldResult: SolveResult | null;
71
131
  meshes: unknown[];
72
132
  /** Values changed since the last solve, in manual (instanceSolve === false) mode. */
73
133
  pendingValues: Record<string, unknown>;
@@ -107,13 +167,66 @@ export declare function applyValueChange(state: SolveSessionState, id: string, v
107
167
  */
108
168
  export declare function pickInputValues(schema: UISchema | undefined, values: Record<string, unknown>): Record<string, unknown>;
109
169
  export declare function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState;
170
+ type SolvePhase = 'idle' | 'solving' | 'review' | 'blocked';
171
+ /**
172
+ * One value for what a host should show. Solving wins: a new solve supersedes a held or refused
173
+ * result. `isSolving` is passed in because the driver owns it.
174
+ */
175
+ export declare function solvePhase(state: SolveSessionState, isSolving: boolean): SolvePhase;
110
176
  //#endregion
111
- //#region src/client/solve-session.d.ts
177
+ //#region src/client/session/solve-session.d.ts
112
178
  interface SolveSession {
113
179
  readonly values: Record<string, unknown>;
114
180
  readonly error: string;
115
181
  readonly computeErrors: string[];
116
182
  readonly computeWarnings: string[];
183
+ /**
184
+ * The last solve's messages with their level, source and `isGate`. Richer than the two
185
+ * string arrays above and the list a message UI should render; they remain for hosts that
186
+ * only want the text. Empty from a transport that reports no structure.
187
+ */
188
+ readonly diagnostics: SolveDiagnostic[];
189
+ /**
190
+ * The definition refused the last solve, or it was aborted. Applied at once, not held: the
191
+ * outputs are blanked and the viewer cleared, and `computeErrors` holds the reason.
192
+ */
193
+ readonly blocked: boolean;
194
+ /**
195
+ * The last solve raised messages awaiting the user's confirmation, and its result is held
196
+ * back until then. A host shows the messages and calls `acknowledge()` to release them.
197
+ * Never true for a blocked solve.
198
+ */
199
+ readonly awaitingAck: boolean;
200
+ /**
201
+ * Releases a held result into the session. No-op when nothing is held. The solve already
202
+ * ran — this applies what it produced, it does not re-solve.
203
+ */
204
+ acknowledge(): void;
205
+ /**
206
+ * Drops a held result instead of applying it. The viewer keeps what it was showing. No-op
207
+ * when nothing is held, and never re-solves — like `acknowledge()`, it only decides the
208
+ * fate of a result that already exists.
209
+ */
210
+ discard(): void;
211
+ /**
212
+ * What the running (or most recent) solve has said so far: its diagnostics, progress per
213
+ * source, and how it ended. Reset when a solve with a new id starts, so a host renders "what
214
+ * this solve has said", never a tail of the previous one. Stays empty when the driver has no
215
+ * live channel.
216
+ */
217
+ readonly live: LiveSolveState;
218
+ /**
219
+ * Where the session is, as one value instead of four flags: `solving` while a solve runs,
220
+ * `review` while a result waits for `acknowledge()`, `blocked` after a refused or aborted
221
+ * solve, `idle` otherwise.
222
+ */
223
+ readonly phase: SolvePhase;
224
+ /**
225
+ * Asks the transport to stop the solve in flight. Cooperative on both paths — Grasshopper
226
+ * stops at the next component boundary — so the result is a `solveEnded` (or nothing, if it
227
+ * finished first), never an instant halt.
228
+ */
229
+ abort(): void;
117
230
  readonly meshes: unknown[];
118
231
  /**
119
232
  * The last reported result without its meshes — what the viewer is currently showing,
@@ -137,6 +250,12 @@ interface SolveSession {
137
250
  subscribe(listener: () => void): () => void;
138
251
  /** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */
139
252
  notify(): void;
253
+ /**
254
+ * Stops listening to the driver's live events and drops every subscriber. Call when the
255
+ * session is discarded: the event source can outlive it (the cloud stream belongs to the tab),
256
+ * and would otherwise keep feeding a session nobody reads.
257
+ */
258
+ dispose(): void;
140
259
  }
141
260
  interface SolveSessionArgs {
142
261
  schema: UISchema;
@@ -181,6 +300,8 @@ interface RequestResponseDriverOptions<TMesh> {
181
300
  onChange?: () => void;
182
301
  /** Mesh ownership policy for the result memo — see `MeshPolicy` (solve-memo.ts). */
183
302
  meshPolicy?: MeshPolicy<TMesh>;
303
+ /** Live events for the solves this driver runs; without it the session never sees any. */
304
+ events?: SolveEventSource;
184
305
  }
185
306
  /**
186
307
  * `getReporter` is lazy (`() => session`) because the session and driver reference each
@@ -188,6 +309,23 @@ interface RequestResponseDriverOptions<TMesh> {
188
309
  */
189
310
  export declare function createRequestResponseDriver<TMesh = unknown>(onSolve: SolveFn<TMesh>, getReporter: () => SolveReporter<TMesh>, options: RequestResponseDriverOptions<TMesh>): SolveDriver;
190
311
  //#endregion
312
+ //#region src/client/events/solve-event-stream.d.ts
313
+ interface SolveEventStreamOptions {
314
+ /** The SSE endpoint, e.g. `/api/v1/solve-events`. `streamId` is appended as a query param. */
315
+ endpoint: string;
316
+ /** Builds the cancel URL for a solve, e.g. `(id) => \`/api/v1/solve/${id}/cancel\``. */
317
+ cancelEndpoint: (solveId: string) => string;
318
+ fetch?: typeof fetch;
319
+ }
320
+ interface SolveEventStream extends SolveEventSource {
321
+ /** Send this with every solve request so the server can route that solve's events here. */
322
+ readonly streamId: string;
323
+ /** The solve the server last reported as started and not yet ended; null between solves. */
324
+ readonly currentSolveId: string | null;
325
+ close(): void;
326
+ }
327
+ export declare function createSolveEventStream(options: SolveEventStreamOptions): SolveEventStream;
328
+ //#endregion
191
329
  //#region src/client/async-throttle.d.ts
192
330
  /**
193
331
  * Single-in-flight, latest-wins throttle for any async operation: only one run at a
@@ -234,6 +372,8 @@ interface ComputeFetchSolveFnOptions<TMesh = unknown> {
234
372
  }>;
235
373
  channel?: () => 'live' | 'draft' | undefined;
236
374
  versionId?: () => string | null | undefined;
375
+ /** The tab's live-event stream id (`createSolveEventStream().streamId`), so the server routes this solve's events to it. */
376
+ streamId?: () => string | null | undefined;
237
377
  /** Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`. */
238
378
  meshes?: {
239
379
  extract: (response: GrasshopperComputeResponse, opts: {
@@ -249,5 +389,5 @@ interface ComputeFetchSolveFnOptions<TMesh = unknown> {
249
389
  }
250
390
  export declare function createComputeFetchSolveFn<TMesh = unknown>(opts: ComputeFetchSolveFnOptions<TMesh>): SolveFn<TMesh, GrasshopperComputeResponse>;
251
391
  //#endregion
252
- export type { AsyncThrottle, ComputeFetchSolveFnOptions, ExternalInput, ExternalReader, ExternalValueRef, MeshPolicy, RequestResponseDriverOptions, RetainedSolveResult, SolveDriver, SolveFn, SolveMemo, SolveMemoOptions, SolveReporter, SolveResult, SolveSession, SolveSessionArgs, SolveSessionState };
392
+ export { type AsyncThrottle, type ComputeFetchSolveFnOptions, type ExternalInput, type ExternalReader, type ExternalValueRef, type LiveSolveState, type MeshPolicy, type RequestResponseDriverOptions, type RetainedSolveResult, type SolveDiagnostic, type SolveDriver, type SolveEventSource, type SolveEventStream, type SolveEventStreamOptions, type SolveFn, type SolveMemo, type SolveMemoOptions, type SolveOutcome, type SolvePhase, type SolveReporter, type SolveResult, type SolveSession, type SolveSessionArgs, type SolveSessionState, decodeOutcome, finalizeResult };
253
393
  //# sourceMappingURL=client.d.ts.map
package/dist/client.js CHANGED
@@ -1,2 +1,2 @@
1
- import{getDefaultValue as e,getInputItems as t}from"@selvajs/schemas";import{GrasshopperResponseProcessor as n}from"@selvajs/compute/grasshopper";function r(e,t){return`external:${e}:${t}`}function i(e){let{scopeKey:t,inputId:n,value:i}=e;t&&n&&(typeof sessionStorage>`u`||sessionStorage.setItem(r(t,n),JSON.stringify(i)))}function a(e){let{scopeKey:t,inputId:n}=e;if(!t||!n||typeof sessionStorage>`u`)return;let i=sessionStorage.getItem(r(t,n));if(i!==null)try{return JSON.parse(i)}catch{return}}function o(e){let{scopeKey:t,inputId:n}=e;t&&n&&(typeof sessionStorage>`u`||sessionStorage.removeItem(r(t,n)))}function s(e){return t(e).filter(e=>e.source?.kind===`client`).map(e=>({paramId:e.paramId,displayName:e.displayName??e.paramId}))}function c(t,n,r){let i=new Set(s(t).map(e=>e.paramId)),a={};for(let o of t.inputs){if(i.has(o.id)){let e=r({scopeKey:n,inputId:o.id});e!==void 0&&(a[o.id]=e);continue}a[o.id]=o.default??e(o.paramType)}for(let e of t.outputs)a[e.id]=null;return a}function l(e){let t=e===!1;return{hasPendingChanges:t,hasNeverSolved:t}}function u(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}}function d(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 f(e,t){e.error=``,e.computeErrors=t.errors??[],e.computeWarnings=t.warnings??[],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 p(e){let t=e.schema,n=l(t?.instanceSolve),r={values:c(t,e.scopeKey,a),error:``,computeErrors:[],computeWarnings:[],meshes:[],pendingValues:{},hasPendingChanges:n.hasPendingChanges,hasNeverSolved:n.hasNeverSolved,lastResult:null},i=new Set,o=()=>i.forEach(e=>e());function s(){e.driver.solve({...d(t,r.values)}),o()}return{get values(){return r.values},get error(){return r.error},get computeErrors(){return r.computeErrors},get computeWarnings(){return r.computeWarnings},get meshes(){return r.meshes},get lastResult(){return r.lastResult},get hasPendingChanges(){return r.hasPendingChanges},get hasNeverSolved(){return r.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(e,n,i=!1){let{shouldSolve:a}=u(r,e,n,t?.instanceSolve);a||i?(i&&!a&&(r.pendingValues={},r.hasPendingChanges=!1),s()):o()},solve(){s()},loadValues(e){Object.assign(r.values,e),t?.instanceSolve===!1?(r.hasPendingChanges=!0,o()):s()},rebuild(n,i){t=n,e.driver.clearCache?.(),r.meshes=[],r.lastResult=null,r.error=``,r.computeErrors=[],r.computeWarnings=[],r.pendingValues={},r.values=c(n,i,a);let u=l(n?.instanceSolve);r.hasPendingChanges=u.hasPendingChanges,r.hasNeverSolved=u.hasNeverSolved,n&&Object.keys(r.values).length>0&&n.instanceSolve!==!1?s():o()},report(e){f(r,e),o()},reportError(e){r.error=e,o()},subscribe(e){return i.add(e),()=>i.delete(e)},notify:o}}function m(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()},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 h(e){return g(e)}function g(e){if(typeof e!=`object`||!e)return JSON.stringify(e)??`null`;if(Array.isArray(e))return`[${e.map(g).join(`,`)}]`;let t=e;return`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${g(t[e])}`).join(`,`)}}`}function _(e={}){let{max:t=16,meshPolicy:n}=e,r=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=h(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=h(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 v(e,t,n){let{meshPolicy:r,solveDeadlineMs:i,onChange:a}=n,o=_({meshPolicy:r}),s=m(async(n,r)=>{let i=o.get(n);if(i!==void 0){if(r.aborted)return;t().report(i);return}try{let i=await e(n,r);if(r.aborted){console.debug(`[Solve/driver] solve completed after abort — result discarded`);return}let a={...i,values:n};o.set(n,a),t().report(a)}catch(e){if(r.aborted){console.debug(`[Solve/driver] solve aborted (superseded, cancelled, or timed out)`);return}console.warn(`[Solve/driver] solve failed:`,e),t().reportError(e instanceof Error?e.message:String(e))}},{runDeadlineMs:i,onChange:a});return{solve(e){s.trigger(e)},cancel(){s.cancel()},get isSolving(){return s.isRunning},clearCache(){o.clear()}}}function y(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 b=e=>{throw Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},x=()=>{throw Error(`Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.`)};function S(e){let t=e.debug??!1,r=(...e)=>{t&&console.info(...e)},i=0;return async(a,o)=>{let s=i-Date.now();if(s>0)throw Error(`Rate limit reached. Try again in ${Math.ceil(s/1e3)}s.`);let c=performance.now(),l=e.inputs(),u=e.outputs(),d=e.channel?.(),f=e.versionId?.(),p=JSON.stringify({inputs:l,values:a,definitionUrl:e.definitionUrl(),...f?{versionId:f}:d===`draft`?{channel:`draft`}:{}}),m;try{m=await fetch(e.endpoint,{method:`POST`,headers:{"Content-Type":`application/json`},body:p,signal:o})}catch(e){if(o.aborted)return r(`[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(o.aborted)return r(`[Compute] solve aborted after headers — empty result discarded`),{outputs:{}};let h=m.redirected&&new URL(m.url).pathname===`/login`;if((m.status===401||h)&&(e.onSessionExpired??x)(),!m.ok){let t=await m.text().catch(()=>``),n={};try{n=JSON.parse(t)}catch{t&&console.warn(`[Compute] non-JSON error body (HTTP ${m.status}):`,t.slice(0,300))}if(m.status===503||m.status===504)throw Error(n.message||`Compute server is offline or unreachable. Please try again later.`);if(m.status===429){let t=Number(m.headers.get(`Retry-After`))||Number(n.retryAfter)||5;i=Date.now()+t*1e3,(e.onRateLimited??b)(t)}throw Error(n.message||`Compute error (HTTP ${m.status})`)}let g=performance.now()-c,_=performance.now(),v=await m.text();if(o.aborted)return r(`[Compute] solve aborted mid-download — empty result discarded`),{outputs:{}};let S=performance.now()-_,C=v.length,w;try{w=JSON.parse(v)}catch(e){throw Error(`Received an invalid response from the server. Reload the page and try again.`,{cause:e})}let T=new n(w,!1),E=e.meshes?await e.meshes.extract(T.response,{debug:t}):[],D={};for(let e of u){let t=T.getValue({byId:e.id},{parseValues:!0});D[e.id]=t??(e.nickname?T.getValue({byName:e.nickname},{parseValues:!0}):void 0)}if(t){let e=JSON.stringify(a).length,t=y(m.headers.get(`Server-Timing`)),n=t.total??0,i=Math.max(0,g-n),o=C/1048576,s=o>=1?`${o.toFixed(2)} MB`:`${(C/1024).toFixed(0)} KB`,c=S>0?(o/(S/1e3)).toFixed(1):`∞`,l=performance.memory?.usedJSHeapSize,u=l===void 0?``:` | heap=${(l/1048576).toFixed(0)} MB`;if(r(`[Compute] req=${(p.length/1024).toFixed(0)}KB (values ${(e/1024).toFixed(0)}KB) | ttfb=${g.toFixed(0)}ms (network≈${i.toFixed(0)} + server ${n.toFixed(0)}) | download=${S.toFixed(0)}ms (${s} @ ${c} MB/s)${u}`),t.selva_cache!==void 0){let e=t.selva_cache?`HIT — served without calling compute`:`miss — solved on Rhino.Compute`;r(`[Compute] └─ cache: ${e}`)}}return{outputs:D,meshes:E,errors:w.errors??[],warnings:w.warnings??[],source:w}}}export{f as applySolveResult,u as applyValueChange,c as buildInitialValues,o as clearExternalValue,m as createAsyncThrottle,S as createComputeFetchSolveFn,v as createRequestResponseDriver,_ as createSolveMemo,p as createSolveSession,s as getExternalInputs,l as makeInitialFlags,d as pickInputValues,a as readExternalValue,h as stableInputKey,i as writeExternalValue};
1
+ import{n as e,r as t,t as n}from"./outcome-H5XjAroQ.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: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);continue}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}}function _(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 v(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,b(e,t)):S(e.diagnostics)?(e.awaitingAck=!0,e.heldResult=t,e.pendingValues={},e.hasPendingChanges=!1,e):(e.awaitingAck=!1,e.heldResult=null,b(e,t))}function y(e){let t={};for(let n of e?.outputs??[])t[n.id]=null;return t}function b(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 x(e,t){return t?`solving`:e.awaitingAck?`review`:e.blocked?`blocked`:`idle`}function S(e){return(e??[]).some(e=>e.isGate&&e.level!==`remark`)}function C(e){let t=e.schema,n=h(t?.instanceSolve),r={values:m(t,e.scopeKey,c),error:``,computeErrors:[],computeWarnings:[],diagnostics:[],blocked:!1,awaitingAck:!1,heldResult:null,meshes:[],pendingValues:{},hasPendingChanges:n.hasPendingChanges,hasNeverSolved:n.hasNeverSolved,lastResult:null},i=new Set,a=()=>i.forEach(e=>e()),o=d,s=e.driver.onEvent?.(e=>{let t=f(o,e);t!==o&&(o=t,a())});function l(){e.driver.solve({..._(t,r.values)}),a()}return{get values(){return r.values},get error(){return r.error},get computeErrors(){return r.computeErrors},get computeWarnings(){return r.computeWarnings},get diagnostics(){return r.diagnostics},get blocked(){return r.blocked},get awaitingAck(){return r.awaitingAck},acknowledge(){if(!r.awaitingAck)return;let e=r.heldResult;r.awaitingAck=!1,r.heldResult=null,e&&b(r,e),a()},discard(){r.awaitingAck&&(r.awaitingAck=!1,r.heldResult=null,a())},get live(){return o},get phase(){return x(r,e.driver.isSolving)},abort(){e.driver.cancel()},get meshes(){return r.meshes},get lastResult(){return r.lastResult},get hasPendingChanges(){return r.hasPendingChanges},get hasNeverSolved(){return r.hasNeverSolved},get isSolving(){return e.driver.isSolving},setValue(e,n,i=!1){let{shouldSolve:o}=g(r,e,n,t?.instanceSolve);o||i?(i&&!o&&(r.pendingValues={},r.hasPendingChanges=!1),l()):a()},solve(){l()},loadValues(e){Object.assign(r.values,e),t?.instanceSolve===!1?(r.hasPendingChanges=!0,a()):l()},rebuild(n,i){t=n,e.driver.clearCache?.(),r.meshes=[],r.lastResult=null,r.error=``,r.computeErrors=[],r.computeWarnings=[],r.diagnostics=[],r.blocked=!1,r.awaitingAck=!1,r.heldResult=null,r.pendingValues={},r.values=m(n,i,c);let o=h(n?.instanceSolve);r.hasPendingChanges=o.hasPendingChanges,r.hasNeverSolved=o.hasNeverSolved,n&&Object.keys(r.values).length>0&&n.instanceSolve!==!1?l():a()},report(e){v(r,e.blocked||e.aborted?{...e,outputs:y(t),meshes:[]}:e),a()},reportError(e){r.error=e,a()},subscribe(e){return i.add(e),()=>i.delete(e)},notify:a,dispose(){s?.(),i.clear()}}}function w(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()},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 T(e){return E(e)}function E(e){if(typeof e!=`object`||!e)return JSON.stringify(e)??`null`;if(Array.isArray(e))return`[${e.map(E).join(`,`)}]`;let t=e;return`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${E(t[e])}`).join(`,`)}}`}function D(e={}){let{max:t=16,meshPolicy:n}=e,r=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=T(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=T(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 O(e,t,n){let{meshPolicy:r,solveDeadlineMs:i,onChange:a,events:o}=n,s=D({meshPolicy:r}),c=w(async(n,r)=>{let i=s.get(n);if(i!==void 0){if(r.aborted)return;t().report(i);return}try{let i=await e(n,r);if(r.aborted){console.debug(`[Solve/driver] solve completed after abort — result discarded`);return}let a={...i,values:n};s.set(n,a),t().report(a)}catch(e){if(r.aborted){console.debug(`[Solve/driver] solve aborted (superseded, cancelled, or timed out)`);return}console.warn(`[Solve/driver] solve failed:`,e),t().reportError(e instanceof Error?e.message:String(e))}},{runDeadlineMs:i,onChange:a});return{solve(e){c.trigger(e)},cancel(){c.cancel(),o?.cancelCurrent?.()},get isSolving(){return c.isRunning},clearCache(){s.clear()},...o?{onEvent:e=>o.subscribe(e)}:{}}}function k(){return typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID().replace(/-/g,``):Array.from({length:32},()=>Math.floor(Math.random()*16).toString(16)).join(``)}function A(e){let t=k(),n=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 j(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 M=e=>{throw Error(`Rate limit reached. Try again in ${Math.ceil(e)}s.`)},N=()=>{throw Error(`Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.`)};function P(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??N)(),!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??M)(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,!1),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=j(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`:`miss — solved on Rhino.Compute`;o(`[Compute] └─ cache: ${e}`)}}return e({outputs:A,meshes:k,source:T},D)}}export{d as EMPTY_LIVE_SOLVE,v as applySolveResult,g as applyValueChange,m as buildInitialValues,l as clearExternalValue,w as createAsyncThrottle,P as createComputeFetchSolveFn,O as createRequestResponseDriver,A as createSolveEventStream,D as createSolveMemo,C as createSolveSession,n as decodeOutcome,e as finalizeResult,u as getExternalInputs,h as makeInitialFlags,_ as pickInputValues,c as readExternalValue,f as reduceLiveSolve,x as solvePhase,T as stableInputKey,s as writeExternalValue};
2
2
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/client/external-storage.ts","../src/client/solve-session-core.ts","../src/client/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts","../src/client/compute-fetch-solve-fn.ts"],"sourcesContent":["// Transit storage for inputs with source.kind === 'client': a producer route writes the\n// value here, the solver route reads it back. Keyed by (scopeKey, inputId) so values\n// don't bleed across solver contexts — scopeKey is whatever uniquely identifies one\n// (sessionId in plugin-ui/preview, definition guid in selva/library).\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nfunction makeKey(scopeKey: string, inputId: string): string {\n\treturn `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;\n}\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst { scopeKey, inputId, value } = args;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return undefined;\n\tif (typeof sessionStorage === 'undefined') return undefined;\n\tconst raw = sessionStorage.getItem(makeKey(scopeKey, inputId));\n\tif (raw === null) return undefined;\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function clearExternalValue(ref: ExternalValueRef): void {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.removeItem(makeKey(scopeKey, inputId));\n}\n\nexport interface ExternalInput {\n\tparamId: string;\n\tdisplayName: string;\n}\n\nexport function getExternalInputs(schema: UISchema): ExternalInput[] {\n\treturn getInputItems(schema)\n\t\t.filter((item) => item.source?.kind === 'client')\n\t\t.map((item) => ({\n\t\t\tparamId: item.paramId,\n\t\t\tdisplayName: item.displayName ?? item.paramId\n\t\t}));\n}\n","// Pure transition logic for a Solve Session.\n//\n// This module holds the value/lifecycle state machine with no transport and no state\n// ownership: how values are seeded (including client-sourced hydration), when a value\n// change should dispatch a solve vs. defer it, and what a reported solve result does to\n// the flags. `solve-session.ts` is a thin shell that owns the state object and the\n// subscriber set; every decision lives here, which is what makes it directly testable.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getDefaultValue } from '@selvajs/schemas';\nimport { getExternalInputs, type ExternalValueRef } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\n\n/**\n * The slice of a reported result the session keeps addressable, so a host can commit exactly\n * what the viewer is showing — `source` and `values` included, already memo-correct.\n *\n * Mesh-free by construction. `SolveResult.meshes` are GPU-backed and the viewer disposes what\n * it renders on the next scene update, which is why the memo carries a `MeshPolicy`\n * clone/release contract at all. Retaining them here would hand out disposed instances after\n * the next solve, with no policy governing them and no owner. Everything left is inert JSON;\n * live meshes stay on `session.meshes`.\n */\nexport type RetainedSolveResult<TSource = unknown> = Omit<SolveResult<never, TSource>, 'meshes'>;\n\nexport interface SolveSessionState {\n\tvalues: Record<string, unknown>;\n\terror: string;\n\tcomputeErrors: string[];\n\tcomputeWarnings: string[];\n\tmeshes: unknown[];\n\t/** Values changed since the last solve, in manual (instanceSolve === false) mode. */\n\tpendingValues: Record<string, unknown>;\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n\t/**\n\t * The last result reported, minus its meshes. Null before the first solve.\n\t *\n\t * Lags `values` by design: nothing updates it on `setValue` or `loadValues`, so between a\n\t * value change and the next report it describes the *previous* input set. That is what a\n\t * commit gate wants — what is on screen, not what is pending — so `lastResult.values`\n\t * disagreeing with `state.values` mid-scrub is the feature, not a bug.\n\t */\n\tlastResult: RetainedSolveResult | null;\n}\n\nexport type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;\n\n/**\n * Client-sourced inputs are hydrated from `read` and left absent when no stored value\n * exists, so the missing-inputs panel can detect them. Other inputs fall back to their\n * declared default, then the paramType default.\n */\nexport function buildInitialValues(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): Record<string, unknown> {\n\tconst clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));\n\tconst values: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (clientSet.has(input.id)) {\n\t\t\tconst stored = read({ scopeKey, inputId: input.id });\n\t\t\tif (stored !== undefined) values[input.id] = stored;\n\t\t\tcontinue;\n\t\t}\n\t\tvalues[input.id] = input.default ?? getDefaultValue(input.paramType);\n\t}\n\tfor (const output of schema.outputs) {\n\t\tvalues[output.id] = null;\n\t}\n\treturn values;\n}\n\n/** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */\nexport function makeInitialFlags(instanceSolve: boolean | undefined): {\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n} {\n\tconst manual = instanceSolve === false;\n\treturn { hasPendingChanges: manual, hasNeverSolved: manual };\n}\n\n/** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined\n): { state: SolveSessionState; shouldSolve: boolean } {\n\tstate.values[id] = value;\n\tif (instanceSolve === false) {\n\t\tstate.pendingValues[id] = value;\n\t\tstate.hasPendingChanges = true;\n\t\treturn { state, shouldSolve: false };\n\t}\n\treturn { state, shouldSolve: true };\n}\n\n/**\n * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value\n * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB\n * options-list output showed this can re-upload a multi-MB payload no backend reads.\n */\nexport function pickInputValues(\n\tschema: UISchema | undefined,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tif (!schema?.inputs) return values;\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (input.id in values) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\nexport function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.meshes = result.meshes ?? [];\n\tconst { meshes: _meshes, ...retained } = result;\n\tstate.lastResult = retained;\n\tObject.assign(state.values, result.outputs);\n\tstate.pendingValues = {};\n\tstate.hasPendingChanges = false;\n\tstate.hasNeverSolved = false;\n\treturn state;\n}\n","// A framework-free shell over the pure transition logic in solve-session-core.ts: owns the\n// live values/flags, delegates every transition to the core, and drives solves through a\n// transport-agnostic SolveDriver.\n//\n// State is exposed as plain getters; it carries no framework reactivity of its own, so a\n// Svelte host must wrap it — see `useSolveSession.svelte.ts` in `@selvajs/ui`, which\n// republishes these getters as $state. Reading a getter without subscribing gives a\n// correct value but will not re-render.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { readExternalValue } from './external-storage.js';\nimport type { SolveResult } from '../shared/solve-fn.js';\nimport type { SolveDriver } from './drivers/driver.js';\nimport {\n\tbuildInitialValues,\n\tmakeInitialFlags,\n\tapplyValueChange,\n\tapplySolveResult,\n\tpickInputValues,\n\ttype SolveSessionState,\n\ttype RetainedSolveResult\n} from './solve-session-core.js';\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\treadonly meshes: unknown[];\n\t/**\n\t * The last reported result without its meshes — what the viewer is currently showing,\n\t * including the `source`/`values` pair a commit path needs. Null before the first solve,\n\t * and again after `rebuild()`. Survives `reportError`: a failed solve leaves the last good\n\t * geometry on screen, so a host committing what it sees should still see what produced it.\n\t */\n\treadonly lastResult: RetainedSolveResult | null;\n\treadonly hasPendingChanges: boolean;\n\treadonly hasNeverSolved: boolean;\n\treadonly isSolving: boolean;\n\t/** `forceSolve` overrides manual mode for system-initiated reconciliation (e.g. a dynamic value list pruning a vanished selection), so the output can't lag behind the new value. */\n\tsetValue(id: string, value: unknown, forceSolve?: boolean): void;\n\tsolve(): void;\n\t/** Merges incoming values, then solves (auto) or marks dirty (manual). */\n\tloadValues(incoming: Record<string, unknown>): void;\n\trebuild(schema: UISchema, scopeKey: string): void;\n\treport(result: SolveResult): void;\n\treportError(message: string): void;\n\t/** Fires after any mutation of the getters above, including driver-reported `isSolving` transitions. */\n\tsubscribe(listener: () => void): () => void;\n\t/** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */\n\tnotify(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\n\tconst flags = makeInitialFlags(currentSchema?.instanceSolve);\n\tconst state: SolveSessionState = {\n\t\tvalues: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tmeshes: [],\n\t\tpendingValues: {},\n\t\thasPendingChanges: flags.hasPendingChanges,\n\t\thasNeverSolved: flags.hasNeverSolved,\n\t\tlastResult: null\n\t};\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => listeners.forEach((l) => l());\n\n\tfunction dispatch() {\n\t\t// Copy: state.values is the live map, and a driver may hold the payload across an\n\t\t// async solve — a later setValue must not mutate what's already in flight.\n\t\targs.driver.solve({ ...pickInputValues(currentSchema, state.values) });\n\t\temit();\n\t}\n\n\treturn {\n\t\tget values() {\n\t\t\treturn state.values;\n\t\t},\n\t\tget error() {\n\t\t\treturn state.error;\n\t\t},\n\t\tget computeErrors() {\n\t\t\treturn state.computeErrors;\n\t\t},\n\t\tget computeWarnings() {\n\t\t\treturn state.computeWarnings;\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget lastResult() {\n\t\t\treturn state.lastResult;\n\t\t},\n\t\tget hasPendingChanges() {\n\t\t\treturn state.hasPendingChanges;\n\t\t},\n\t\tget hasNeverSolved() {\n\t\t\treturn state.hasNeverSolved;\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn args.driver.isSolving;\n\t\t},\n\n\t\tsetValue(id, value, forceSolve = false) {\n\t\t\tconst { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);\n\t\t\tif (shouldSolve || forceSolve) {\n\t\t\t\tif (forceSolve && !shouldSolve) {\n\t\t\t\t\tstate.pendingValues = {};\n\t\t\t\t\tstate.hasPendingChanges = false;\n\t\t\t\t}\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\tsolve() {\n\t\t\tdispatch();\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tObject.assign(state.values, incoming);\n\t\t\tif (currentSchema?.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\tstate.hasPendingChanges = true;\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\trebuild(schema, scopeKey) {\n\t\t\tcurrentSchema = schema;\n\t\t\t// The new definition has its own input space; a matching input key from the prior\n\t\t\t// one must not serve that definition's cached result.\n\t\t\targs.driver.clearCache?.();\n\t\t\tstate.meshes = [];\n\t\t\t// Same reason as clearCache: a retained result belongs to the previous definition.\n\t\t\tstate.lastResult = null;\n\t\t\tstate.error = '';\n\t\t\tstate.computeErrors = [];\n\t\t\tstate.computeWarnings = [];\n\t\t\tstate.pendingValues = {};\n\t\t\tstate.values = buildInitialValues(schema, scopeKey, readExternalValue);\n\t\t\tconst f = makeInitialFlags(schema?.instanceSolve);\n\t\t\tstate.hasPendingChanges = f.hasPendingChanges;\n\t\t\tstate.hasNeverSolved = f.hasNeverSolved;\n\t\t\tif (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\treport(result) {\n\t\t\tapplySolveResult(state, result);\n\t\t\temit();\n\t\t},\n\n\t\treportError(message) {\n\t\t\tstate.error = message;\n\t\t\temit();\n\t\t},\n\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => listeners.delete(listener);\n\t\t},\n\n\t\tnotify: emit\n\t};\n}\n","/**\n * Single-in-flight, latest-wins throttle for any async operation: only one run at a\n * time, a newly triggered value overwrites the single pending slot (no queue), and a\n * stale run can be aborted via `AbortController` or timeout.\n *\n * Framework-free: `isRunning` is a plain getter, not a store. A reactive host must\n * subscribe via `onChange` (see `@selvajs/ui`'s rune adapter) — polling the getter\n * directly reads correct values but won't trigger a re-render.\n */\ninterface AsyncThrottleOptions {\n\t/**\n\t * How long one run may take before it is aborted (ms). Required and deliberately\n\t * undefaulted: this is a generic throttle, so it has no basis for guessing what\n\t * the caller's work costs. Callers driving a Selva solve pass the deadline the\n\t * server enforces (`COMPUTE_SOLVE_DEADLINE_MS`) — a shorter local guess would abort\n\t * solves the server would have finished.\n\t */\n\trunDeadlineMs: number;\n\tonChange?: () => void;\n}\n\nexport interface AsyncThrottle<T> {\n\ttrigger: (values: T) => void;\n\treadonly isRunning: boolean;\n\treadonly hasPending: boolean;\n\tcancel: () => void;\n}\n\nexport function createAsyncThrottle<T>(\n\trun: (values: T, signal: AbortSignal) => Promise<void>,\n\toptions: AsyncThrottleOptions\n): AsyncThrottle<T> {\n\tconst { runDeadlineMs, onChange } = options;\n\n\tlet isRunning = false;\n\tlet pendingValues: T | null = null;\n\tlet currentAbortController: AbortController | null = null;\n\n\tfunction setRunning(next: boolean) {\n\t\tif (isRunning === next) return;\n\t\tisRunning = next;\n\t\tonChange?.();\n\t}\n\n\tfunction abortCurrent() {\n\t\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\n\t}\n\n\tasync function execute(values: T) {\n\t\tabortCurrent();\n\n\t\tcurrentAbortController = new AbortController();\n\t\tconst { signal } = currentAbortController;\n\t\tconst deadlineId = setTimeout(() => {\n\t\t\t// Cleared in `finally` on every other path, so firing means a genuine\n\t\t\t// timeout — the only signal for it (the abort itself is swallowed below).\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${runDeadlineMs}ms — aborting`);\n\t\t\tcurrentAbortController?.abort();\n\t\t}, runDeadlineMs);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, signal);\n\t\t} catch (err) {\n\t\t\t// AbortError is expected (timeout or cancel). Non-abort errors must be\n\t\t\t// handled inside `run` — re-throwing here would produce an unhandled\n\t\t\t// rejection because `execute` is always called fire-and-forget.\n\t\t\tif (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {\n\t\t\t\tconsole.error('[Solve/throttle] unhandled error in run:', err);\n\t\t\t}\n\t\t} finally {\n\t\t\tclearTimeout(deadlineId);\n\t\t\tsetRunning(false);\n\t\t\tcurrentAbortController = null;\n\n\t\t\tif (pendingValues !== null) {\n\t\t\t\tconst next = pendingValues;\n\t\t\t\tpendingValues = null;\n\t\t\t\texecute(next);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction trigger(values: T) {\n\t\tif (isRunning) {\n\t\t\tif (pendingValues !== null) {\n\t\t\t\t// Latest-wins: the previously-queued values are dropped, not run.\n\t\t\t\t// `debug`, not `info`: a slider scrub fires this per frame, and it is a trace of\n\t\t\t\t// normal operation. Promoting it would spam a host's console on every drag.\n\t\t\t\t// eslint-disable-next-line no-console -- see above\n\t\t\t\tconsole.debug('[Solve/throttle] superseded pending run (latest-wins)');\n\t\t\t}\n\t\t\tpendingValues = values;\n\t\t} else {\n\t\t\texecute(values);\n\t\t}\n\t}\n\n\tfunction cancel() {\n\t\tpendingValues = null;\n\t\tabortCurrent();\n\t}\n\n\treturn {\n\t\ttrigger,\n\t\tget isRunning() {\n\t\t\treturn isRunning;\n\t\t},\n\t\tget hasPending() {\n\t\t\treturn pendingValues !== null;\n\t\t},\n\t\tcancel\n\t};\n}\n","// LRU cache of solve results keyed on a stable serialization of the inputs, so re-solving\n// something already seen this session (e.g. dragging a slider back) returns instantly\n// instead of round-tripping.\n//\n// A viewer typically takes ownership of every mesh it renders, disposing the previous\n// content on the next update. So the memo can't hand out its own stored instances (they'd\n// get disposed under it) or drop entries silently (their GPU buffers would leak): it keeps\n// private copies via an injected clone/release policy, which also keeps `three` out of this\n// package — `TMesh` stays opaque here.\n\nimport type { SolveResult } from '../shared/solve-fn.js';\n\n/**\n * Deterministic string key for a set of input values. Sorts object keys at every level so\n * two logically-equal inputs built in different key order collide.\n */\nexport function stableInputKey(values: Record<string, unknown>): string {\n\treturn serialize(values);\n}\n\nfunction serialize(value: unknown): string {\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(serialize).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(',')}}`;\n}\n\n/**\n * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then\n * stores and serves them by reference, which is safe only because nothing frees them behind\n * its back.\n */\nexport interface MeshPolicy<TMesh> {\n\t/** Called on the way in and on every hit, so the stored entry and a served copy are never the same instance. */\n\tclone(meshes: TMesh[]): TMesh[];\n\t/** Must not free anything shared with live content (e.g. materials another object still uses). */\n\trelease(meshes: TMesh[]): void;\n}\n\nexport interface SolveMemoOptions<TMesh> {\n\t/** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */\n\tmax?: number;\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\nexport interface SolveMemo<TMesh = unknown> {\n\tget(values: Record<string, unknown>): SolveResult<TMesh> | undefined;\n\tset(values: Record<string, unknown>, result: SolveResult<TMesh>): void;\n\tclear(): void;\n}\n\nexport function createSolveMemo<TMesh = unknown>(\n\toptions: SolveMemoOptions<TMesh> = {}\n): SolveMemo<TMesh> {\n\tconst { max = 16, meshPolicy } = options;\n\tconst entries = new Map<string, SolveResult<TMesh>>();\n\n\tfunction release(result: SolveResult<TMesh>): void {\n\t\tif (result.meshes?.length) meshPolicy?.release(result.meshes);\n\t}\n\n\tfunction copy(result: SolveResult<TMesh>): SolveResult<TMesh> {\n\t\tif (!meshPolicy || !result.meshes?.length) return result;\n\t\treturn { ...result, meshes: meshPolicy.clone(result.meshes) };\n\t}\n\n\tfunction evict(key: string): void {\n\t\tconst entry = entries.get(key);\n\t\tif (entry === undefined) return;\n\t\tentries.delete(key);\n\t\trelease(entry);\n\t}\n\n\treturn {\n\t\tget(values) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tconst hit = entries.get(key);\n\t\t\tif (hit === undefined) return undefined;\n\t\t\tentries.delete(key);\n\t\t\tentries.set(key, hit);\n\t\t\tconsole.info(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);\n\t\t\treturn copy(hit);\n\t\t},\n\t\tset(values, result) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tevict(key);\n\t\t\tentries.set(key, copy(result));\n\t\t\twhile (entries.size > max) {\n\t\t\t\tconst oldest = entries.keys().next().value;\n\t\t\t\tif (oldest === undefined) break;\n\t\t\t\tevict(oldest);\n\t\t\t\tconsole.info(`[Solve/memo] evicted LRU entry (cap ${max})`);\n\t\t\t}\n\t\t},\n\t\tclear() {\n\t\t\tif (entries.size > 0) {\n\t\t\t\tconsole.info(`[Solve/memo] cleared ${entries.size} entries (definition changed)`);\n\t\t\t}\n\t\t\tentries.forEach(release);\n\t\t\tentries.clear();\n\t\t}\n\t};\n}\n","import { createAsyncThrottle } from '../async-throttle.js';\nimport { createSolveMemo, type MeshPolicy } from '../solve-memo.js';\nimport type { SolveFn } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\t/** How long one solve may take before the client aborts it (ms). Pass the server's `COMPUTE_SOLVE_DEADLINE_MS` so the two agree. */\n\tsolveDeadlineMs: number;\n\tonChange?: () => void;\n\t/** Mesh ownership policy for the result memo — see `MeshPolicy` (solve-memo.ts). */\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\n/**\n * `getReporter` is lazy (`() => session`) because the session and driver reference each\n * other — the host builds the driver first, then the session, then can hand this back.\n */\nexport function createRequestResponseDriver<TMesh = unknown>(\n\tonSolve: SolveFn<TMesh>,\n\tgetReporter: () => SolveReporter<TMesh>,\n\toptions: RequestResponseDriverOptions<TMesh>\n): SolveDriver {\n\tconst { meshPolicy, solveDeadlineMs, onChange } = options;\n\n\t// Checked inside the throttled run, not before triggering it, so a hit only serves\n\t// after the throttle's latest-wins ordering has picked these values to run.\n\tconst memo = createSolveMemo<TMesh>({ meshPolicy });\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(\n\t\tasync (values, signal) => {\n\t\t\tconst cached = memo.get(values);\n\t\t\tif (cached !== undefined) {\n\t\t\t\tif (signal.aborted) return;\n\t\t\t\tgetReporter().report(cached);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await onSolve(values, signal);\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve completed after abort — result discarded');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Stamped here rather than in the SolveFn: a memo hit never calls it, so values\n\t\t\t\t// captured inside would pair the served result with whatever solved last.\n\t\t\t\tconst stamped = { ...result, values };\n\t\t\t\tmemo.set(values, stamped);\n\t\t\t\tgetReporter().report(stamped);\n\t\t\t} catch (err) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve aborted (superseded, cancelled, or timed out)');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\t\tgetReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t\t}\n\t\t},\n\t\t// The throttle is generic, so the solve deadline is what bounds one of its runs.\n\t\t{ runDeadlineMs: solveDeadlineMs, onChange }\n\t);\n\n\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel() {\n\t\t\tthrottle.cancel();\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tclearCache() {\n\t\t\tmemo.clear();\n\t\t}\n\t};\n}\n","/**\n * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a\n * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +\n * `SolveEngine` produce server-side) and unpacks the result.\n *\n * Ports the resilience behavior an interactive solve endpoint needs regardless\n * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response\n * guarding, and abort handling at every await point — so a new consumer gets it\n * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:\n * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a\n * viewer-enabled consumer supplies its own `meshes.extract` hook (typically\n * `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).\n */\n\nimport {\n\tGrasshopperResponseProcessor,\n\ttype GrasshopperComputeResponse\n} from '@selvajs/compute/grasshopper';\nimport type { SolveFn, SolveResult } from '../shared/solve-fn.js';\n\nexport interface ComputeFetchSolveFnOptions<TMesh = unknown> {\n\t/** The compute endpoint to POST to, e.g. `/api/compute`. */\n\tendpoint: string;\n\t/** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */\n\tdefinitionUrl: () => string;\n\t/** Schema input params, sent as the request body's `inputs`. */\n\tinputs: () => Array<{ id: string }>;\n\t/** Schema output params — drives byId/byName output extraction after a solve. */\n\toutputs: () => Array<{ id: string; nickname?: string }>;\n\tchannel?: () => 'live' | 'draft' | undefined;\n\tversionId?: () => string | null | undefined;\n\t/** Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`. */\n\tmeshes?: {\n\t\textract: (\n\t\t\tresponse: GrasshopperComputeResponse,\n\t\t\topts: { debug: boolean }\n\t\t) => TMesh[] | Promise<TMesh[]>;\n\t};\n\t/** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */\n\tdebug?: boolean;\n\t/** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */\n\tonRateLimited?: (retryAfterSeconds: number) => void;\n\t/** Called on a 401 or a login-page redirect. Default: throws an actionable \"reload to sign in again\" Error. */\n\tonSessionExpired?: () => void;\n}\n\nfunction parseServerTiming(header: string | null): Record<string, number> {\n\tconst out: Record<string, number> = {};\n\tif (!header) return out;\n\tfor (const part of header.split(',')) {\n\t\tconst [name, ...params] = part.split(';').map((s) => s.trim());\n\t\tconst dur = params.find((p) => p.startsWith('dur='));\n\t\tif (!name || !dur) continue;\n\t\tconst ms = Number(dur.slice(4));\n\t\tif (Number.isFinite(ms)) out[name] = ms;\n\t}\n\treturn out;\n}\n\nconst defaultOnRateLimited = (retryAfterSeconds: number): never => {\n\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);\n};\n\nconst defaultOnSessionExpired = (): never => {\n\tthrow new Error(\n\t\t'Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.'\n\t);\n};\n\nexport function createComputeFetchSolveFn<TMesh = unknown>(\n\topts: ComputeFetchSolveFnOptions<TMesh>\n): SolveFn<TMesh, GrasshopperComputeResponse> {\n\tconst debug = opts.debug ?? false;\n\tconst log = (...args: unknown[]) => {\n\t\tif (debug) console.info(...args);\n\t};\n\n\tlet cooldownUntil = 0;\n\n\treturn async (values, signal): Promise<SolveResult<TMesh, GrasshopperComputeResponse>> => {\n\t\tconst remainingMs = cooldownUntil - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(remainingMs / 1000)}s.`);\n\t\t}\n\n\t\tconst solveStart = performance.now();\n\t\tconst inputs = opts.inputs();\n\t\tconst outputs = opts.outputs();\n\t\tconst channel = opts.channel?.();\n\t\tconst versionId = opts.versionId?.();\n\n\t\tconst payload = JSON.stringify({\n\t\t\tinputs,\n\t\t\tvalues,\n\t\t\tdefinitionUrl: opts.definitionUrl(),\n\t\t\t...(versionId ? { versionId } : channel === 'draft' ? { channel: 'draft' } : {})\n\t\t});\n\n\t\tlet res: Response;\n\t\ttry {\n\t\t\tres = await fetch(opts.endpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tbody: payload,\n\t\t\t\tsignal\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\tlog('[Compute] solve aborted during fetch — empty result discarded');\n\t\t\t\treturn { outputs: {} };\n\t\t\t}\n\t\t\tthrow new Error(\n\t\t\t\t'Request blocked — your session may have expired. Reload the page to sign in again.',\n\t\t\t\t{ cause: err }\n\t\t\t);\n\t\t}\n\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted after headers — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\n\t\tconst redirectedToLogin = res.redirected && new URL(res.url).pathname === '/login';\n\t\tif (res.status === 401 || redirectedToLogin) {\n\t\t\t(opts.onSessionExpired ?? defaultOnSessionExpired)();\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tconst errorBody = await res.text().catch(() => '');\n\t\t\tlet d: { message?: string; retryAfter?: number } = {};\n\t\t\ttry {\n\t\t\t\td = JSON.parse(errorBody);\n\t\t\t} catch {\n\t\t\t\tif (errorBody) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[Compute] non-JSON error body (HTTP ${res.status}):`,\n\t\t\t\t\t\terrorBody.slice(0, 300)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (res.status === 503 || res.status === 504) {\n\t\t\t\t// The server names the failure class (unreachable, bad API key,\n\t\t\t\t// rate-limited, timed out) — show it rather than a blanket \"offline\".\n\t\t\t\tthrow new Error(\n\t\t\t\t\td.message || 'Compute server is offline or unreachable. Please try again later.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (res.status === 429) {\n\t\t\t\tconst retryAfter = Number(res.headers.get('Retry-After')) || Number(d.retryAfter) || 5;\n\t\t\t\tcooldownUntil = Date.now() + retryAfter * 1000;\n\t\t\t\t(opts.onRateLimited ?? defaultOnRateLimited)(retryAfter);\n\t\t\t}\n\t\t\tthrow new Error(d.message || `Compute error (HTTP ${res.status})`);\n\t\t}\n\n\t\tconst ttfbMs = performance.now() - solveStart;\n\n\t\tconst downloadStart = performance.now();\n\t\tconst bodyText = await res.text();\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted mid-download — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\t\tconst downloadMs = performance.now() - downloadStart;\n\t\tconst bytes = bodyText.length;\n\n\t\tlet solved: GrasshopperComputeResponse;\n\t\ttry {\n\t\t\tsolved = JSON.parse(bodyText);\n\t\t} catch (err) {\n\t\t\tthrow new Error(\n\t\t\t\t'Received an invalid response from the server. Reload the page and try again.',\n\t\t\t\t{\n\t\t\t\t\tcause: err\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\tconst processor = new GrasshopperResponseProcessor(solved, false);\n\n\t\tconst meshes = opts.meshes ? await opts.meshes.extract(processor.response, { debug }) : [];\n\n\t\tconst resultOutputs: Record<string, unknown> = {};\n\t\tfor (const o of outputs) {\n\t\t\tconst byId = processor.getValue({ byId: o.id }, { parseValues: true });\n\t\t\tresultOutputs[o.id] =\n\t\t\t\tbyId ??\n\t\t\t\t(o.nickname\n\t\t\t\t\t? processor.getValue({ byName: o.nickname }, { parseValues: true })\n\t\t\t\t\t: undefined);\n\t\t}\n\n\t\tif (debug) {\n\t\t\tconst valuesBytes = JSON.stringify(values).length;\n\t\t\tconst serverTiming = parseServerTiming(res.headers.get('Server-Timing'));\n\t\t\tconst serverTotal = serverTiming.total ?? 0;\n\t\t\tconst networkMs = Math.max(0, ttfbMs - serverTotal);\n\t\t\tconst sizeMB = bytes / (1024 * 1024);\n\t\t\tconst size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bytes / 1024).toFixed(0)} KB`;\n\t\t\tconst mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1000)).toFixed(1) : '∞';\n\t\t\tconst heapMB = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory\n\t\t\t\t?.usedJSHeapSize;\n\t\t\tconst heap = heapMB !== undefined ? ` | heap=${(heapMB / (1024 * 1024)).toFixed(0)} MB` : '';\n\t\t\tlog(\n\t\t\t\t`[Compute] req=${(payload.length / 1024).toFixed(0)}KB (values ${(valuesBytes / 1024).toFixed(0)}KB) | ` +\n\t\t\t\t\t`ttfb=${ttfbMs.toFixed(0)}ms (network≈${networkMs.toFixed(0)} + server ${serverTotal.toFixed(0)}) | ` +\n\t\t\t\t\t`download=${downloadMs.toFixed(0)}ms (${size} @ ${mbps} MB/s)${heap}`\n\t\t\t);\n\t\t\tif (serverTiming.selva_cache !== undefined) {\n\t\t\t\tconst cacheMsg = serverTiming.selva_cache\n\t\t\t\t\t? 'HIT — served without calling compute'\n\t\t\t\t\t: 'miss — solved on Rhino.Compute';\n\t\t\t\tlog(`[Compute] └─ cache: ${cacheMsg}`);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\toutputs: resultOutputs,\n\t\t\tmeshes,\n\t\t\terrors: solved.errors ?? [],\n\t\t\twarnings: solved.warnings ?? [],\n\t\t\tsource: solved\n\t\t};\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client/external-storage.ts","../src/client/events/live-solve.ts","../src/client/session/solve-session-core.ts","../src/client/session/solve-session.ts","../src/client/async-throttle.ts","../src/client/solve-memo.ts","../src/client/drivers/request-response.ts","../src/client/events/solve-event-stream.ts","../src/client/compute-fetch-solve-fn.ts"],"sourcesContent":["// Transit storage for inputs with source.kind === 'client': a producer route writes the\n// value here, the solver route reads it back. Keyed by (scopeKey, inputId) so values\n// don't bleed across solver contexts — scopeKey is whatever uniquely identifies one\n// (sessionId in plugin-ui/preview, definition guid in selva/library).\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getInputItems } from '@selvajs/schemas';\n\nconst STORAGE_PREFIX = 'external';\n\nfunction makeKey(scopeKey: string, inputId: string): string {\n\treturn `${STORAGE_PREFIX}:${scopeKey}:${inputId}`;\n}\n\nexport interface ExternalValueRef {\n\tscopeKey: string;\n\tinputId: string;\n}\n\nexport function writeExternalValue(args: ExternalValueRef & { value: unknown }): void {\n\tconst { scopeKey, inputId, value } = args;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.setItem(makeKey(scopeKey, inputId), JSON.stringify(value));\n}\n\nexport function readExternalValue(ref: ExternalValueRef): unknown | undefined {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return undefined;\n\tif (typeof sessionStorage === 'undefined') return undefined;\n\tconst raw = sessionStorage.getItem(makeKey(scopeKey, inputId));\n\tif (raw === null) return undefined;\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function clearExternalValue(ref: ExternalValueRef): void {\n\tconst { scopeKey, inputId } = ref;\n\tif (!scopeKey || !inputId) return;\n\tif (typeof sessionStorage === 'undefined') return;\n\tsessionStorage.removeItem(makeKey(scopeKey, inputId));\n}\n\nexport interface ExternalInput {\n\tparamId: string;\n\tdisplayName: string;\n}\n\nexport function getExternalInputs(schema: UISchema): ExternalInput[] {\n\treturn getInputItems(schema)\n\t\t.filter((item) => item.source?.kind === 'client')\n\t\t.map((item) => ({\n\t\t\tparamId: item.paramId,\n\t\t\tdisplayName: item.displayName ?? item.paramId\n\t\t}));\n}\n","// What the running (or most recent) solve has said over the live channel, folded into one state\n// a host can render. Pure: `reduceLiveSolve` returns a new state when an event changes something\n// and the same object when it does not, so a host can compare by reference.\n//\n// One case per known kind. An unknown kind is kept in `other`, untouched, so a newer plugin's\n// events reach a host that knows them without this package having to.\n\nimport type { ProgressPayload, SolveEvent } from '@selvajs/schemas';\nimport { decodeOutcome } from '../../shared/outcome.js';\nimport type { SolveDiagnostic } from '../../shared/solve-fn.js';\n\nexport interface LiveSolveState {\n\t/** The solve these events belong to; null before the first event. */\n\treadonly solveId: string | null;\n\t/** Between that solve's first event and its `solveEnded`. */\n\treadonly running: boolean;\n\t/** `diagnostic` events in arrival order. */\n\treadonly diagnostics: readonly SolveDiagnostic[];\n\t/** The newest `progress` per source. A report without a source is keyed by `''`. */\n\treadonly progress: ReadonlyMap<string, ProgressPayload>;\n\t/** `solveEnded`'s payload once it arrives: `ok`, `blocked`, `aborted` or `error`. */\n\treadonly ended: { readonly kind: string } | null;\n\t/** Events of kinds this package does not know, oldest first. */\n\treadonly other: readonly SolveEvent[];\n\t/** Highest `seq` applied for this solve. A late or repeated delivery at or below it is ignored. */\n\treadonly lastSeq: number;\n}\n\nexport const EMPTY_LIVE_SOLVE: LiveSolveState = Object.freeze({\n\tsolveId: null,\n\trunning: false,\n\tdiagnostics: [],\n\tprogress: new Map(),\n\tended: null,\n\tother: [],\n\tlastSeq: 0\n});\n\nexport function reduceLiveSolve(state: LiveSolveState, event: SolveEvent): LiveSolveState {\n\t// A new id starts a new solve: nothing the previous one said carries over.\n\tconst base: LiveSolveState =\n\t\tevent.solveId === state.solveId\n\t\t\t? state\n\t\t\t: { ...EMPTY_LIVE_SOLVE, solveId: event.solveId, running: true };\n\tif (event.seq <= base.lastSeq) return state;\n\tconst next = { ...base, lastSeq: event.seq };\n\tconst payload = (event.payload ?? {}) as Record<string, unknown>;\n\n\tswitch (event.type) {\n\t\tcase 'solveStarted':\n\t\t\treturn { ...next, running: true };\n\n\t\tcase 'solveEnded':\n\t\t\treturn {\n\t\t\t\t...next,\n\t\t\t\trunning: false,\n\t\t\t\tended: { kind: typeof payload.kind === 'string' ? payload.kind : 'ok' }\n\t\t\t};\n\n\t\tcase 'diagnostic': {\n\t\t\t// Same normalization as a finished solve's messages, so a live row and its final\n\t\t\t// counterpart are the same object shape.\n\t\t\tconst [diagnostic] = decodeOutcome({ diagnostics: [payload] })?.diagnostics ?? [];\n\t\t\treturn diagnostic ? { ...next, diagnostics: [...next.diagnostics, diagnostic] } : next;\n\t\t}\n\n\t\tcase 'progress': {\n\t\t\tconst source = typeof payload.source === 'string' ? payload.source : '';\n\t\t\tconst progress = new Map(next.progress);\n\t\t\tconst report = readProgress(payload);\n\t\t\t// A bar that jumps back reads as broken. Within one solve a source only moves forward.\n\t\t\tconst previous = next.progress.get(source)?.fraction;\n\t\t\tif (previous !== undefined && report.fraction !== undefined && report.fraction < previous) {\n\t\t\t\treport.fraction = previous;\n\t\t\t}\n\t\t\tprogress.set(source, report);\n\t\t\treturn { ...next, progress };\n\t\t}\n\n\t\tdefault:\n\t\t\treturn { ...next, other: [...next.other, event] };\n\t}\n}\n\nfunction readProgress(payload: Record<string, unknown>): ProgressPayload {\n\tconst num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);\n\tconst fraction = num(payload.fraction);\n\tconst done = num(payload.done);\n\tconst total = num(payload.total);\n\tconst nextFraction = num(payload.nextFraction);\n\tconst stepMs = num(payload.stepMs);\n\tconst clamp = (v: number) => Math.min(1, Math.max(0, v));\n\treturn {\n\t\t...(typeof payload.source === 'string' ? { source: payload.source } : {}),\n\t\t...(fraction !== undefined ? { fraction: clamp(fraction) } : {}),\n\t\t...(done !== undefined ? { done } : {}),\n\t\t...(total !== undefined ? { total } : {}),\n\t\t...(typeof payload.label === 'string' ? { label: payload.label } : {}),\n\t\t...(nextFraction !== undefined ? { nextFraction: clamp(nextFraction) } : {}),\n\t\t...(stepMs !== undefined && stepMs > 0 ? { stepMs } : {})\n\t};\n}\n","// Pure transition logic for a Solve Session.\n//\n// This module holds the value/lifecycle state machine with no transport and no state\n// ownership: how values are seeded (including client-sourced hydration), when a value\n// change should dispatch a solve vs. defer it, and what a reported solve result does to\n// the flags. `solve-session.ts` is a thin shell that owns the state object and the\n// subscriber set; every decision lives here, which is what makes it directly testable.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { getDefaultValue } from '@selvajs/schemas';\nimport { getExternalInputs, type ExternalValueRef } from '../external-storage.js';\nimport type { SolveDiagnostic, SolveResult } from '../../shared/solve-fn.js';\n\n/**\n * The slice of a reported result the session keeps addressable, so a host can commit exactly\n * what the viewer is showing — `source` and `values` included, already memo-correct.\n *\n * Mesh-free by construction. `SolveResult.meshes` are GPU-backed and the viewer disposes what\n * it renders on the next scene update, which is why the memo carries a `MeshPolicy`\n * clone/release contract at all. Retaining them here would hand out disposed instances after\n * the next solve, with no policy governing them and no owner. Everything left is inert JSON;\n * live meshes stay on `session.meshes`.\n */\nexport type RetainedSolveResult<TSource = unknown> = Omit<SolveResult<never, TSource>, 'meshes'>;\n\nexport interface SolveSessionState {\n\tvalues: Record<string, unknown>;\n\terror: string;\n\tcomputeErrors: string[];\n\tcomputeWarnings: string[];\n\t/**\n\t * The same messages with their level, source and `isGate`. What decides whether a solve\n\t * interrupts the user; the two string arrays above stay the plain display lists.\n\t */\n\tdiagnostics: SolveDiagnostic[];\n\t/**\n\t * The last solve was refused by a guard in the definition. Separate from `computeErrors`\n\t * being non-empty: a solve can report errors and still return usable outputs, and the UI\n\t * needs to tell \"solved, with complaints\" from \"would not solve\".\n\t */\n\tblocked: boolean;\n\t/**\n\t * The last solve raised messages the user has not acknowledged yet. While true the result\n\t * is withheld: `meshes` and the solve's outputs stay at their previous values until\n\t * `acknowledge()` releases them.\n\t */\n\tawaitingAck: boolean;\n\t/**\n\t * The withheld result, kept whole so `acknowledge()` can apply it without a re-solve.\n\t * Null whenever `awaitingAck` is false. A newer solve replaces it: the user is always\n\t * confirming the most recent result, never a stale one.\n\t */\n\theldResult: SolveResult | null;\n\tmeshes: unknown[];\n\t/** Values changed since the last solve, in manual (instanceSolve === false) mode. */\n\tpendingValues: Record<string, unknown>;\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n\t/**\n\t * The last result reported, minus its meshes. Null before the first solve.\n\t *\n\t * Lags `values` by design: nothing updates it on `setValue` or `loadValues`, so between a\n\t * value change and the next report it describes the *previous* input set. That is what a\n\t * commit gate wants — what is on screen, not what is pending — so `lastResult.values`\n\t * disagreeing with `state.values` mid-scrub is the feature, not a bug.\n\t */\n\tlastResult: RetainedSolveResult | null;\n}\n\nexport type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;\n\n/**\n * Client-sourced inputs are hydrated from `read` and left absent when no stored value\n * exists, so the missing-inputs panel can detect them. Other inputs fall back to their\n * declared default, then the paramType default.\n */\nexport function buildInitialValues(\n\tschema: UISchema,\n\tscopeKey: string,\n\tread: ExternalReader\n): Record<string, unknown> {\n\tconst clientSet = new Set(getExternalInputs(schema).map((e) => e.paramId));\n\tconst values: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (clientSet.has(input.id)) {\n\t\t\tconst stored = read({ scopeKey, inputId: input.id });\n\t\t\tif (stored !== undefined) values[input.id] = stored;\n\t\t\tcontinue;\n\t\t}\n\t\tvalues[input.id] = input.default ?? getDefaultValue(input.paramType);\n\t}\n\tfor (const output of schema.outputs) {\n\t\tvalues[output.id] = null;\n\t}\n\treturn values;\n}\n\n/** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */\nexport function makeInitialFlags(instanceSolve: boolean | undefined): {\n\thasPendingChanges: boolean;\n\thasNeverSolved: boolean;\n} {\n\tconst manual = instanceSolve === false;\n\treturn { hasPendingChanges: manual, hasNeverSolved: manual };\n}\n\n/** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */\nexport function applyValueChange(\n\tstate: SolveSessionState,\n\tid: string,\n\tvalue: unknown,\n\tinstanceSolve: boolean | undefined\n): { state: SolveSessionState; shouldSolve: boolean } {\n\tstate.values[id] = value;\n\tif (instanceSolve === false) {\n\t\tstate.pendingValues[id] = value;\n\t\tstate.hasPendingChanges = true;\n\t\treturn { state, shouldSolve: false };\n\t}\n\treturn { state, shouldSolve: true };\n}\n\n/**\n * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value\n * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB\n * options-list output showed this can re-upload a multi-MB payload no backend reads.\n */\nexport function pickInputValues(\n\tschema: UISchema | undefined,\n\tvalues: Record<string, unknown>\n): Record<string, unknown> {\n\tif (!schema?.inputs) return values;\n\tconst picked: Record<string, unknown> = {};\n\tfor (const input of schema.inputs) {\n\t\tif (input.id in values) picked[input.id] = values[input.id];\n\t}\n\treturn picked;\n}\n\nexport function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState {\n\tstate.error = '';\n\tstate.computeErrors = result.errors ?? [];\n\tstate.computeWarnings = result.warnings ?? [];\n\tstate.diagnostics = result.diagnostics ?? [];\n\tstate.blocked = result.blocked ?? false;\n\n\t// A blocked solve is applied at once: there is no result to weigh, and the previous one must\n\t// not stay on screen looking like the answer to the current inputs. The caller blanks the\n\t// outputs (see `blankOutputs`); the empty mesh list clears the viewer.\n\tif (state.blocked) {\n\t\tstate.awaitingAck = false;\n\t\tstate.heldResult = null;\n\t\treturn commitSolveResult(state, result);\n\t}\n\n\t// The solve is finished either way — Grasshopper cannot be paused mid-solution — so what is\n\t// held back here is the *result*, not the computation. Everything the solve produced is kept\n\t// intact in `heldResult` and released verbatim by `acknowledge()`.\n\tif (needsAcknowledgement(state.diagnostics)) {\n\t\tstate.awaitingAck = true;\n\t\tstate.heldResult = result;\n\t\t// The input set stays dirty-free regardless: the values were sent and solved, and\n\t\t// leaving them pending would re-solve them on the next tick.\n\t\tstate.pendingValues = {};\n\t\tstate.hasPendingChanges = false;\n\t\treturn state;\n\t}\n\n\tstate.awaitingAck = false;\n\tstate.heldResult = null;\n\treturn commitSolveResult(state, result);\n}\n\n/**\n * Every schema output set to null. A blocked solve reports `outputs: {}`, and merging that into\n * `values` would leave the previous solve's outputs behind.\n */\nexport function blankOutputs(schema: UISchema | undefined): Record<string, null> {\n\tconst blank: Record<string, null> = {};\n\tfor (const output of schema?.outputs ?? []) blank[output.id] = null;\n\treturn blank;\n}\n\n/** Writes a result into the state. Shared by the clean path and by `acknowledge()`. */\nexport function commitSolveResult(\n\tstate: SolveSessionState,\n\tresult: SolveResult\n): SolveSessionState {\n\tstate.meshes = result.meshes ?? [];\n\tconst { meshes: _meshes, ...retained } = result;\n\tstate.lastResult = retained;\n\tObject.assign(state.values, result.outputs);\n\tstate.pendingValues = {};\n\tstate.hasPendingChanges = false;\n\tstate.hasNeverSolved = false;\n\treturn state;\n}\n\nexport type SolvePhase = 'idle' | 'solving' | 'review' | 'blocked';\n\n/**\n * One value for what a host should show. Solving wins: a new solve supersedes a held or refused\n * result. `isSolving` is passed in because the driver owns it.\n */\nexport function solvePhase(state: SolveSessionState, isSolving: boolean): SolvePhase {\n\tif (isSolving) return 'solving';\n\tif (state.awaitingAck) return 'review';\n\tif (state.blocked) return 'blocked';\n\treturn 'idle';\n}\n\n/**\n * Whether this solve's messages still need the user's acknowledgement.\n *\n * Only a Selva Message component gates. Grasshopper raises warnings constantly for reasons that\n * concern whoever is editing the definition, not whoever is using the app — a component\n * complaining about its own inputs is not something an end user can act on, and interrupting\n * them for it trains them to dismiss the one message that mattered.\n *\n * Every solve that gates asks again, by design — an acknowledgement covers the solve in front of\n * the user, never a future one. Under `instanceSolve` with a live warning that means a dialog\n * per solve, which is the intended checkpoint, not an oversight.\n *\n * Without `diagnostics` (a transport that reports no structure) nothing gates: the messages are\n * still shown, just without interrupting.\n */\nexport function needsAcknowledgement(diagnostics: SolveDiagnostic[] | undefined): boolean {\n\treturn (diagnostics ?? []).some((d) => d.isGate && d.level !== 'remark');\n}\n","// A framework-free shell over the pure transition logic in solve-session-core.ts: owns the\n// live values/flags, delegates every transition to the core, and drives solves through a\n// transport-agnostic SolveDriver.\n//\n// State is exposed as plain getters; it carries no framework reactivity of its own, so a\n// Svelte host must wrap it — see `useSolveSession.svelte.ts` in `@selvajs/ui`, which\n// republishes these getters as $state. Reading a getter without subscribing gives a\n// correct value but will not re-render.\n\nimport type { UISchema } from '@selvajs/schemas';\nimport { readExternalValue } from '../external-storage.js';\nimport type { SolveDiagnostic, SolveResult } from '../../shared/solve-fn.js';\nimport type { SolveDriver } from '../drivers/driver.js';\nimport { EMPTY_LIVE_SOLVE, reduceLiveSolve, type LiveSolveState } from '../events/live-solve.js';\nimport {\n\tbuildInitialValues,\n\tmakeInitialFlags,\n\tapplyValueChange,\n\tapplySolveResult,\n\tblankOutputs,\n\tcommitSolveResult,\n\tpickInputValues,\n\tsolvePhase,\n\ttype SolvePhase,\n\ttype SolveSessionState,\n\ttype RetainedSolveResult\n} from './solve-session-core.js';\n\nexport interface SolveSession {\n\treadonly values: Record<string, unknown>;\n\treadonly error: string;\n\treadonly computeErrors: string[];\n\treadonly computeWarnings: string[];\n\t/**\n\t * The last solve's messages with their level, source and `isGate`. Richer than the two\n\t * string arrays above and the list a message UI should render; they remain for hosts that\n\t * only want the text. Empty from a transport that reports no structure.\n\t */\n\treadonly diagnostics: SolveDiagnostic[];\n\t/**\n\t * The definition refused the last solve, or it was aborted. Applied at once, not held: the\n\t * outputs are blanked and the viewer cleared, and `computeErrors` holds the reason.\n\t */\n\treadonly blocked: boolean;\n\t/**\n\t * The last solve raised messages awaiting the user's confirmation, and its result is held\n\t * back until then. A host shows the messages and calls `acknowledge()` to release them.\n\t * Never true for a blocked solve.\n\t */\n\treadonly awaitingAck: boolean;\n\t/**\n\t * Releases a held result into the session. No-op when nothing is held. The solve already\n\t * ran — this applies what it produced, it does not re-solve.\n\t */\n\tacknowledge(): void;\n\t/**\n\t * Drops a held result instead of applying it. The viewer keeps what it was showing. No-op\n\t * when nothing is held, and never re-solves — like `acknowledge()`, it only decides the\n\t * fate of a result that already exists.\n\t */\n\tdiscard(): void;\n\t/**\n\t * What the running (or most recent) solve has said so far: its diagnostics, progress per\n\t * source, and how it ended. Reset when a solve with a new id starts, so a host renders \"what\n\t * this solve has said\", never a tail of the previous one. Stays empty when the driver has no\n\t * live channel.\n\t */\n\treadonly live: LiveSolveState;\n\t/**\n\t * Where the session is, as one value instead of four flags: `solving` while a solve runs,\n\t * `review` while a result waits for `acknowledge()`, `blocked` after a refused or aborted\n\t * solve, `idle` otherwise.\n\t */\n\treadonly phase: SolvePhase;\n\t/**\n\t * Asks the transport to stop the solve in flight. Cooperative on both paths — Grasshopper\n\t * stops at the next component boundary — so the result is a `solveEnded` (or nothing, if it\n\t * finished first), never an instant halt.\n\t */\n\tabort(): void;\n\treadonly meshes: unknown[];\n\t/**\n\t * The last reported result without its meshes — what the viewer is currently showing,\n\t * including the `source`/`values` pair a commit path needs. Null before the first solve,\n\t * and again after `rebuild()`. Survives `reportError`: a failed solve leaves the last good\n\t * geometry on screen, so a host committing what it sees should still see what produced it.\n\t */\n\treadonly lastResult: RetainedSolveResult | null;\n\treadonly hasPendingChanges: boolean;\n\treadonly hasNeverSolved: boolean;\n\treadonly isSolving: boolean;\n\t/** `forceSolve` overrides manual mode for system-initiated reconciliation (e.g. a dynamic value list pruning a vanished selection), so the output can't lag behind the new value. */\n\tsetValue(id: string, value: unknown, forceSolve?: boolean): void;\n\tsolve(): void;\n\t/** Merges incoming values, then solves (auto) or marks dirty (manual). */\n\tloadValues(incoming: Record<string, unknown>): void;\n\trebuild(schema: UISchema, scopeKey: string): void;\n\treport(result: SolveResult): void;\n\treportError(message: string): void;\n\t/** Fires after any mutation of the getters above, including driver-reported `isSolving` transitions. */\n\tsubscribe(listener: () => void): () => void;\n\t/** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */\n\tnotify(): void;\n\t/**\n\t * Stops listening to the driver's live events and drops every subscriber. Call when the\n\t * session is discarded: the event source can outlive it (the cloud stream belongs to the tab),\n\t * and would otherwise keep feeding a session nobody reads.\n\t */\n\tdispose(): void;\n}\n\nexport interface SolveSessionArgs {\n\tschema: UISchema;\n\tscopeKey: string;\n\tdriver: SolveDriver;\n}\n\nexport function createSolveSession(args: SolveSessionArgs): SolveSession {\n\tlet currentSchema = args.schema;\n\n\tconst flags = makeInitialFlags(currentSchema?.instanceSolve);\n\tconst state: SolveSessionState = {\n\t\tvalues: buildInitialValues(currentSchema, args.scopeKey, readExternalValue),\n\t\terror: '',\n\t\tcomputeErrors: [],\n\t\tcomputeWarnings: [],\n\t\tdiagnostics: [],\n\t\tblocked: false,\n\t\tawaitingAck: false,\n\t\theldResult: null,\n\t\tmeshes: [],\n\t\tpendingValues: {},\n\t\thasPendingChanges: flags.hasPendingChanges,\n\t\thasNeverSolved: flags.hasNeverSolved,\n\t\tlastResult: null\n\t};\n\n\tconst listeners = new Set<() => void>();\n\tconst emit = () => listeners.forEach((l) => l());\n\n\tlet live: LiveSolveState = EMPTY_LIVE_SOLVE;\n\t// A driver with no live channel never calls this; nothing else in the session depends on it.\n\tconst stopEvents = args.driver.onEvent?.((event) => {\n\t\tconst next = reduceLiveSolve(live, event);\n\t\tif (next === live) return;\n\t\tlive = next;\n\t\temit();\n\t});\n\n\tfunction dispatch() {\n\t\t// Copy: state.values is the live map, and a driver may hold the payload across an\n\t\t// async solve — a later setValue must not mutate what's already in flight.\n\t\targs.driver.solve({ ...pickInputValues(currentSchema, state.values) });\n\t\temit();\n\t}\n\n\treturn {\n\t\tget values() {\n\t\t\treturn state.values;\n\t\t},\n\t\tget error() {\n\t\t\treturn state.error;\n\t\t},\n\t\tget computeErrors() {\n\t\t\treturn state.computeErrors;\n\t\t},\n\t\tget computeWarnings() {\n\t\t\treturn state.computeWarnings;\n\t\t},\n\t\tget diagnostics() {\n\t\t\treturn state.diagnostics;\n\t\t},\n\t\tget blocked() {\n\t\t\treturn state.blocked;\n\t\t},\n\t\tget awaitingAck() {\n\t\t\treturn state.awaitingAck;\n\t\t},\n\n\t\tacknowledge() {\n\t\t\tif (!state.awaitingAck) return;\n\t\t\tconst held = state.heldResult;\n\t\t\tstate.awaitingAck = false;\n\t\t\tstate.heldResult = null;\n\t\t\tif (held) commitSolveResult(state, held);\n\t\t\temit();\n\t\t},\n\n\t\tdiscard() {\n\t\t\tif (!state.awaitingAck) return;\n\t\t\tstate.awaitingAck = false;\n\t\t\tstate.heldResult = null;\n\t\t\t// `values`, `meshes` and `lastResult` are untouched: the viewer keeps showing the last\n\t\t\t// result the user accepted. The messages stay on `computeErrors`/`computeWarnings` so\n\t\t\t// the footer still explains why nothing changed.\n\t\t\temit();\n\t\t},\n\t\tget live() {\n\t\t\treturn live;\n\t\t},\n\t\tget phase() {\n\t\t\treturn solvePhase(state, args.driver.isSolving);\n\t\t},\n\t\tabort() {\n\t\t\targs.driver.cancel();\n\t\t},\n\t\tget meshes() {\n\t\t\treturn state.meshes;\n\t\t},\n\t\tget lastResult() {\n\t\t\treturn state.lastResult;\n\t\t},\n\t\tget hasPendingChanges() {\n\t\t\treturn state.hasPendingChanges;\n\t\t},\n\t\tget hasNeverSolved() {\n\t\t\treturn state.hasNeverSolved;\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn args.driver.isSolving;\n\t\t},\n\n\t\tsetValue(id, value, forceSolve = false) {\n\t\t\tconst { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);\n\t\t\tif (shouldSolve || forceSolve) {\n\t\t\t\tif (forceSolve && !shouldSolve) {\n\t\t\t\t\tstate.pendingValues = {};\n\t\t\t\t\tstate.hasPendingChanges = false;\n\t\t\t\t}\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\tsolve() {\n\t\t\tdispatch();\n\t\t},\n\n\t\tloadValues(incoming) {\n\t\t\tObject.assign(state.values, incoming);\n\t\t\tif (currentSchema?.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\tstate.hasPendingChanges = true;\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\trebuild(schema, scopeKey) {\n\t\t\tcurrentSchema = schema;\n\t\t\t// The new definition has its own input space; a matching input key from the prior\n\t\t\t// one must not serve that definition's cached result.\n\t\t\targs.driver.clearCache?.();\n\t\t\tstate.meshes = [];\n\t\t\t// Same reason as clearCache: a retained result belongs to the previous definition.\n\t\t\tstate.lastResult = null;\n\t\t\tstate.error = '';\n\t\t\tstate.computeErrors = [];\n\t\t\tstate.computeWarnings = [];\n\t\t\tstate.diagnostics = [];\n\t\t\tstate.blocked = false;\n\t\t\tstate.awaitingAck = false;\n\t\t\t// Belongs to the previous definition, same as lastResult.\n\t\t\tstate.heldResult = null;\n\t\t\tstate.pendingValues = {};\n\t\t\tstate.values = buildInitialValues(schema, scopeKey, readExternalValue);\n\t\t\tconst f = makeInitialFlags(schema?.instanceSolve);\n\t\t\tstate.hasPendingChanges = f.hasPendingChanges;\n\t\t\tstate.hasNeverSolved = f.hasNeverSolved;\n\t\t\tif (schema && Object.keys(state.values).length > 0 && schema.instanceSolve !== false) {\n\t\t\t\tdispatch();\n\t\t\t} else {\n\t\t\t\temit();\n\t\t\t}\n\t\t},\n\n\t\treport(result) {\n\t\t\t// `finalizeResult` already dropped a rejected solve's outputs; this also nulls the\n\t\t\t// ones still in `values` from the previous solve, which only the session can name.\n\t\t\tapplySolveResult(\n\t\t\t\tstate,\n\t\t\t\tresult.blocked || result.aborted\n\t\t\t\t\t? { ...result, outputs: blankOutputs(currentSchema), meshes: [] }\n\t\t\t\t\t: result\n\t\t\t);\n\t\t\temit();\n\t\t},\n\n\t\treportError(message) {\n\t\t\tstate.error = message;\n\t\t\temit();\n\t\t},\n\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => listeners.delete(listener);\n\t\t},\n\n\t\tnotify: emit,\n\n\t\tdispose() {\n\t\t\tstopEvents?.();\n\t\t\tlisteners.clear();\n\t\t}\n\t};\n}\n","/**\n * Single-in-flight, latest-wins throttle for any async operation: only one run at a\n * time, a newly triggered value overwrites the single pending slot (no queue), and a\n * stale run can be aborted via `AbortController` or timeout.\n *\n * Framework-free: `isRunning` is a plain getter, not a store. A reactive host must\n * subscribe via `onChange` (see `@selvajs/ui`'s rune adapter) — polling the getter\n * directly reads correct values but won't trigger a re-render.\n */\ninterface AsyncThrottleOptions {\n\t/**\n\t * How long one run may take before it is aborted (ms). Required and deliberately\n\t * undefaulted: this is a generic throttle, so it has no basis for guessing what\n\t * the caller's work costs. Callers driving a Selva solve pass the deadline the\n\t * server enforces (`COMPUTE_SOLVE_DEADLINE_MS`) — a shorter local guess would abort\n\t * solves the server would have finished.\n\t */\n\trunDeadlineMs: number;\n\tonChange?: () => void;\n}\n\nexport interface AsyncThrottle<T> {\n\ttrigger: (values: T) => void;\n\treadonly isRunning: boolean;\n\treadonly hasPending: boolean;\n\tcancel: () => void;\n}\n\nexport function createAsyncThrottle<T>(\n\trun: (values: T, signal: AbortSignal) => Promise<void>,\n\toptions: AsyncThrottleOptions\n): AsyncThrottle<T> {\n\tconst { runDeadlineMs, onChange } = options;\n\n\tlet isRunning = false;\n\tlet pendingValues: T | null = null;\n\tlet currentAbortController: AbortController | null = null;\n\n\tfunction setRunning(next: boolean) {\n\t\tif (isRunning === next) return;\n\t\tisRunning = next;\n\t\tonChange?.();\n\t}\n\n\tfunction abortCurrent() {\n\t\tcurrentAbortController?.abort();\n\t\tcurrentAbortController = null;\n\t}\n\n\tasync function execute(values: T) {\n\t\tabortCurrent();\n\n\t\tcurrentAbortController = new AbortController();\n\t\tconst { signal } = currentAbortController;\n\t\tconst deadlineId = setTimeout(() => {\n\t\t\t// Cleared in `finally` on every other path, so firing means a genuine\n\t\t\t// timeout — the only signal for it (the abort itself is swallowed below).\n\t\t\tconsole.warn(`[Solve/throttle] run exceeded ${runDeadlineMs}ms — aborting`);\n\t\t\tcurrentAbortController?.abort();\n\t\t}, runDeadlineMs);\n\n\t\tsetRunning(true);\n\t\ttry {\n\t\t\tawait run(values, signal);\n\t\t} catch (err) {\n\t\t\t// AbortError is expected (timeout or cancel). Non-abort errors must be\n\t\t\t// handled inside `run` — re-throwing here would produce an unhandled\n\t\t\t// rejection because `execute` is always called fire-and-forget.\n\t\t\tif (!(err instanceof Error) || (err.name !== 'AbortError' && err.name !== 'TimeoutError')) {\n\t\t\t\tconsole.error('[Solve/throttle] unhandled error in run:', err);\n\t\t\t}\n\t\t} finally {\n\t\t\tclearTimeout(deadlineId);\n\t\t\tsetRunning(false);\n\t\t\tcurrentAbortController = null;\n\n\t\t\tif (pendingValues !== null) {\n\t\t\t\tconst next = pendingValues;\n\t\t\t\tpendingValues = null;\n\t\t\t\texecute(next);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction trigger(values: T) {\n\t\tif (isRunning) {\n\t\t\tif (pendingValues !== null) {\n\t\t\t\t// Latest-wins: the previously-queued values are dropped, not run.\n\t\t\t\t// `debug`, not `info`: a slider scrub fires this per frame, and it is a trace of\n\t\t\t\t// normal operation. Promoting it would spam a host's console on every drag.\n\t\t\t\t// eslint-disable-next-line no-console -- see above\n\t\t\t\tconsole.debug('[Solve/throttle] superseded pending run (latest-wins)');\n\t\t\t}\n\t\t\tpendingValues = values;\n\t\t} else {\n\t\t\texecute(values);\n\t\t}\n\t}\n\n\tfunction cancel() {\n\t\tpendingValues = null;\n\t\tabortCurrent();\n\t}\n\n\treturn {\n\t\ttrigger,\n\t\tget isRunning() {\n\t\t\treturn isRunning;\n\t\t},\n\t\tget hasPending() {\n\t\t\treturn pendingValues !== null;\n\t\t},\n\t\tcancel\n\t};\n}\n","// LRU cache of solve results keyed on a stable serialization of the inputs, so re-solving\n// something already seen this session (e.g. dragging a slider back) returns instantly\n// instead of round-tripping.\n//\n// A viewer typically takes ownership of every mesh it renders, disposing the previous\n// content on the next update. So the memo can't hand out its own stored instances (they'd\n// get disposed under it) or drop entries silently (their GPU buffers would leak): it keeps\n// private copies via an injected clone/release policy, which also keeps `three` out of this\n// package — `TMesh` stays opaque here.\n\nimport type { SolveResult } from '../shared/solve-fn.js';\n\n/**\n * Deterministic string key for a set of input values. Sorts object keys at every level so\n * two logically-equal inputs built in different key order collide.\n */\nexport function stableInputKey(values: Record<string, unknown>): string {\n\treturn serialize(values);\n}\n\nfunction serialize(value: unknown): string {\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';\n\tif (Array.isArray(value)) return `[${value.map(serialize).join(',')}]`;\n\tconst obj = value as Record<string, unknown>;\n\tconst keys = Object.keys(obj).sort();\n\treturn `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(',')}}`;\n}\n\n/**\n * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then\n * stores and serves them by reference, which is safe only because nothing frees them behind\n * its back.\n */\nexport interface MeshPolicy<TMesh> {\n\t/** Called on the way in and on every hit, so the stored entry and a served copy are never the same instance. */\n\tclone(meshes: TMesh[]): TMesh[];\n\t/** Must not free anything shared with live content (e.g. materials another object still uses). */\n\trelease(meshes: TMesh[]): void;\n}\n\nexport interface SolveMemoOptions<TMesh> {\n\t/** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */\n\tmax?: number;\n\tmeshPolicy?: MeshPolicy<TMesh>;\n}\n\nexport interface SolveMemo<TMesh = unknown> {\n\tget(values: Record<string, unknown>): SolveResult<TMesh> | undefined;\n\tset(values: Record<string, unknown>, result: SolveResult<TMesh>): void;\n\tclear(): void;\n}\n\nexport function createSolveMemo<TMesh = unknown>(\n\toptions: SolveMemoOptions<TMesh> = {}\n): SolveMemo<TMesh> {\n\tconst { max = 16, meshPolicy } = options;\n\tconst entries = new Map<string, SolveResult<TMesh>>();\n\n\tfunction release(result: SolveResult<TMesh>): void {\n\t\tif (result.meshes?.length) meshPolicy?.release(result.meshes);\n\t}\n\n\tfunction copy(result: SolveResult<TMesh>): SolveResult<TMesh> {\n\t\tif (!meshPolicy || !result.meshes?.length) return result;\n\t\treturn { ...result, meshes: meshPolicy.clone(result.meshes) };\n\t}\n\n\tfunction evict(key: string): void {\n\t\tconst entry = entries.get(key);\n\t\tif (entry === undefined) return;\n\t\tentries.delete(key);\n\t\trelease(entry);\n\t}\n\n\treturn {\n\t\tget(values) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tconst hit = entries.get(key);\n\t\t\tif (hit === undefined) return undefined;\n\t\t\tentries.delete(key);\n\t\t\tentries.set(key, hit);\n\t\t\tconsole.info(`[Solve/memo] HIT — served from client memo (${entries.size}/${max})`);\n\t\t\treturn copy(hit);\n\t\t},\n\t\tset(values, result) {\n\t\t\tconst key = stableInputKey(values);\n\t\t\tevict(key);\n\t\t\tentries.set(key, copy(result));\n\t\t\twhile (entries.size > max) {\n\t\t\t\tconst oldest = entries.keys().next().value;\n\t\t\t\tif (oldest === undefined) break;\n\t\t\t\tevict(oldest);\n\t\t\t\tconsole.info(`[Solve/memo] evicted LRU entry (cap ${max})`);\n\t\t\t}\n\t\t},\n\t\tclear() {\n\t\t\tif (entries.size > 0) {\n\t\t\t\tconsole.info(`[Solve/memo] cleared ${entries.size} entries (definition changed)`);\n\t\t\t}\n\t\t\tentries.forEach(release);\n\t\t\tentries.clear();\n\t\t}\n\t};\n}\n","import { createAsyncThrottle } from '../async-throttle.js';\nimport { createSolveMemo, type MeshPolicy } from '../solve-memo.js';\nimport type { SolveFn } from '../../shared/solve-fn.js';\nimport type { SolveDriver, SolveEventSource, SolveReporter } from './driver.js';\n\nexport interface RequestResponseDriverOptions<TMesh> {\n\t/** How long one solve may take before the client aborts it (ms). Pass the server's `COMPUTE_SOLVE_DEADLINE_MS` so the two agree. */\n\tsolveDeadlineMs: number;\n\tonChange?: () => void;\n\t/** Mesh ownership policy for the result memo — see `MeshPolicy` (solve-memo.ts). */\n\tmeshPolicy?: MeshPolicy<TMesh>;\n\t/** Live events for the solves this driver runs; without it the session never sees any. */\n\tevents?: SolveEventSource;\n}\n\n/**\n * `getReporter` is lazy (`() => session`) because the session and driver reference each\n * other — the host builds the driver first, then the session, then can hand this back.\n */\nexport function createRequestResponseDriver<TMesh = unknown>(\n\tonSolve: SolveFn<TMesh>,\n\tgetReporter: () => SolveReporter<TMesh>,\n\toptions: RequestResponseDriverOptions<TMesh>\n): SolveDriver {\n\tconst { meshPolicy, solveDeadlineMs, onChange, events } = options;\n\n\t// Checked inside the throttled run, not before triggering it, so a hit only serves\n\t// after the throttle's latest-wins ordering has picked these values to run.\n\tconst memo = createSolveMemo<TMesh>({ meshPolicy });\n\n\tconst throttle = createAsyncThrottle<Record<string, unknown>>(\n\t\tasync (values, signal) => {\n\t\t\tconst cached = memo.get(values);\n\t\t\tif (cached !== undefined) {\n\t\t\t\tif (signal.aborted) return;\n\t\t\t\tgetReporter().report(cached);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result = await onSolve(values, signal);\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve completed after abort — result discarded');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Stamped here rather than in the SolveFn: a memo hit never calls it, so values\n\t\t\t\t// captured inside would pair the served result with whatever solved last.\n\t\t\t\tconst stamped = { ...result, values };\n\t\t\t\tmemo.set(values, stamped);\n\t\t\t\tgetReporter().report(stamped);\n\t\t\t} catch (err) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- normal during a slider scrub; not a warning\n\t\t\t\t\tconsole.debug('[Solve/driver] solve aborted (superseded, cancelled, or timed out)');\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconsole.warn('[Solve/driver] solve failed:', err);\n\t\t\t\tgetReporter().reportError(err instanceof Error ? err.message : String(err));\n\t\t\t}\n\t\t},\n\t\t// The throttle is generic, so the solve deadline is what bounds one of its runs.\n\t\t{ runDeadlineMs: solveDeadlineMs, onChange }\n\t);\n\n\treturn {\n\t\tsolve(values) {\n\t\t\tthrottle.trigger(values);\n\t\t},\n\t\tcancel() {\n\t\t\t// Dropping the fetch alone stops the Compute child: the server closes the solve and the\n\t\t\t// plugin's next callback gets a 410. The explicit cancel is for a server that keeps the\n\t\t\t// solve open, and loses the race harmlessly (404) when the fetch abort closes it first.\n\t\t\tthrottle.cancel();\n\t\t\tevents?.cancelCurrent?.();\n\t\t},\n\t\tget isSolving() {\n\t\t\treturn throttle.isRunning;\n\t\t},\n\t\tclearCache() {\n\t\t\tmemo.clear();\n\t\t},\n\t\t...(events ? { onEvent: (listener) => events.subscribe(listener) } : {})\n\t} satisfies SolveDriver;\n}\n","// The cloud half of the live channel: one SSE stream per tab, carrying every solve this tab\n// runs. The stream id is minted here rather than by the server so a solve request can name it\n// before the stream has even connected.\n//\n// Opened when created, not on first subscribe: the server binds a solve to the stream only if\n// the stream is already registered, and a lazily opened one was rarely connected by the time\n// the first solve ran.\n\nimport type { SolveEvent } from '@selvajs/schemas';\nimport type { SolveEventSource } from '../drivers/driver.js';\n\nexport interface SolveEventStreamOptions {\n\t/** The SSE endpoint, e.g. `/api/v1/solve-events`. `streamId` is appended as a query param. */\n\tendpoint: string;\n\t/** Builds the cancel URL for a solve, e.g. `(id) => \\`/api/v1/solve/${id}/cancel\\``. */\n\tcancelEndpoint: (solveId: string) => string;\n\tfetch?: typeof fetch;\n}\n\nexport interface SolveEventStream extends SolveEventSource {\n\t/** Send this with every solve request so the server can route that solve's events here. */\n\treadonly streamId: string;\n\t/** The solve the server last reported as started and not yet ended; null between solves. */\n\treadonly currentSolveId: string | null;\n\tclose(): void;\n}\n\nfunction mintStreamId(): string {\n\tif (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {\n\t\treturn crypto.randomUUID().replace(/-/g, '');\n\t}\n\treturn Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');\n}\n\nexport function createSolveEventStream(options: SolveEventStreamOptions): SolveEventStream {\n\tconst streamId = mintStreamId();\n\tconst listeners = new Set<(event: SolveEvent) => void>();\n\tconst doFetch = options.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args));\n\tlet source: EventSource | null = null;\n\tlet currentSolveId: string | null = null;\n\n\tfunction open() {\n\t\tif (source || typeof EventSource === 'undefined') return;\n\t\tconst url = `${options.endpoint}${options.endpoint.includes('?') ? '&' : '?'}streamId=${streamId}`;\n\t\tsource = new EventSource(url);\n\t\tsource.addEventListener('solve', (raw) => {\n\t\t\tlet event: SolveEvent;\n\t\t\ttry {\n\t\t\t\tevent = JSON.parse((raw as MessageEvent<string>).data);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.type === 'solveStarted') currentSolveId = event.solveId;\n\t\t\telse if (event.type === 'solveEnded' && currentSolveId === event.solveId)\n\t\t\t\tcurrentSolveId = null;\n\t\t\tfor (const listener of listeners) listener(event);\n\t\t});\n\t\t// EventSource reconnects on its own; nothing to do on error beyond not tearing down.\n\t}\n\n\topen();\n\n\treturn {\n\t\tstreamId,\n\t\tget currentSolveId() {\n\t\t\treturn currentSolveId;\n\t\t},\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\topen();\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tcancelCurrent() {\n\t\t\tconst solveId = currentSolveId;\n\t\t\tif (!solveId) return;\n\t\t\t// Fire-and-forget: the answer arrives as `solveEnded` on the stream, or not at all if\n\t\t\t// the solve finished first, and either way there is nothing to await here.\n\t\t\tvoid doFetch(options.cancelEndpoint(solveId), { method: 'POST', keepalive: true }).catch(\n\t\t\t\t() => {}\n\t\t\t);\n\t\t},\n\t\tclose() {\n\t\t\tsource?.close();\n\t\t\tsource = null;\n\t\t\tlisteners.clear();\n\t\t\tcurrentSolveId = null;\n\t\t}\n\t};\n}\n","/**\n * `createComputeFetchSolveFn` — a ready-made `SolveFn` that POSTs to a\n * `/api/compute`-shaped HTTP endpoint (the wire contract `runSolvePipeline` +\n * `SolveEngine` produce server-side) and unpacks the result.\n *\n * Ports the resilience behavior an interactive solve endpoint needs regardless\n * of app — 429 cooldown, session-expiry/redirect detection, non-JSON-response\n * guarding, and abort handling at every await point — so a new consumer gets it\n * for free instead of re-deriving it by hand. Mesh decoding is NOT built in:\n * this package stays renderer-agnostic (see `shared/solve-fn.ts`), so a\n * viewer-enabled consumer supplies its own `meshes.extract` hook (typically\n * `getThreeMeshesFromComputeResponse` from `@selvajs/visualization/parse`).\n */\n\nimport {\n\tGrasshopperResponseProcessor,\n\ttype GrasshopperComputeResponse\n} from '@selvajs/compute/grasshopper';\nimport type { SolveFn, SolveResult } from '../shared/solve-fn.js';\nimport { decodeOutcome, finalizeResult, outcomeFromComputeMessages } from '../shared/outcome.js';\n\nexport interface ComputeFetchSolveFnOptions<TMesh = unknown> {\n\t/** The compute endpoint to POST to, e.g. `/api/compute`. */\n\tendpoint: string;\n\t/** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */\n\tdefinitionUrl: () => string;\n\t/** Schema input params, sent as the request body's `inputs`. */\n\tinputs: () => Array<{ id: string }>;\n\t/** Schema output params — drives byId/byName output extraction after a solve. */\n\toutputs: () => Array<{ id: string; nickname?: string }>;\n\tchannel?: () => 'live' | 'draft' | undefined;\n\tversionId?: () => string | null | undefined;\n\t/** The tab's live-event stream id (`createSolveEventStream().streamId`), so the server routes this solve's events to it. */\n\tstreamId?: () => string | null | undefined;\n\t/** Omit entirely for a non-viewer consumer — `meshes` on the result stays `[]`. */\n\tmeshes?: {\n\t\textract: (\n\t\t\tresponse: GrasshopperComputeResponse,\n\t\t\topts: { debug: boolean }\n\t\t) => TMesh[] | Promise<TMesh[]>;\n\t};\n\t/** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */\n\tdebug?: boolean;\n\t/** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */\n\tonRateLimited?: (retryAfterSeconds: number) => void;\n\t/** Called on a 401 or a login-page redirect. Default: throws an actionable \"reload to sign in again\" Error. */\n\tonSessionExpired?: () => void;\n}\n\nfunction parseServerTiming(header: string | null): Record<string, number> {\n\tconst out: Record<string, number> = {};\n\tif (!header) return out;\n\tfor (const part of header.split(',')) {\n\t\tconst [name, ...params] = part.split(';').map((s) => s.trim());\n\t\tconst dur = params.find((p) => p.startsWith('dur='));\n\t\tif (!name || !dur) continue;\n\t\tconst ms = Number(dur.slice(4));\n\t\tif (Number.isFinite(ms)) out[name] = ms;\n\t}\n\treturn out;\n}\n\nconst defaultOnRateLimited = (retryAfterSeconds: number): never => {\n\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(retryAfterSeconds)}s.`);\n};\n\nconst defaultOnSessionExpired = (): never => {\n\tthrow new Error(\n\t\t'Your session has expired. Sign in again in a new tab, then re-run — your inputs are preserved.'\n\t);\n};\n\nexport function createComputeFetchSolveFn<TMesh = unknown>(\n\topts: ComputeFetchSolveFnOptions<TMesh>\n): SolveFn<TMesh, GrasshopperComputeResponse> {\n\tconst debug = opts.debug ?? false;\n\tconst log = (...args: unknown[]) => {\n\t\tif (debug) console.info(...args);\n\t};\n\n\tlet cooldownUntil = 0;\n\n\treturn async (values, signal): Promise<SolveResult<TMesh, GrasshopperComputeResponse>> => {\n\t\tconst remainingMs = cooldownUntil - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tthrow new Error(`Rate limit reached. Try again in ${Math.ceil(remainingMs / 1000)}s.`);\n\t\t}\n\n\t\tconst solveStart = performance.now();\n\t\tconst inputs = opts.inputs();\n\t\tconst outputs = opts.outputs();\n\t\tconst channel = opts.channel?.();\n\t\tconst versionId = opts.versionId?.();\n\t\tconst streamId = opts.streamId?.();\n\n\t\tconst payload = JSON.stringify({\n\t\t\tinputs,\n\t\t\tvalues,\n\t\t\tdefinitionUrl: opts.definitionUrl(),\n\t\t\t...(versionId ? { versionId } : channel === 'draft' ? { channel: 'draft' } : {}),\n\t\t\t...(streamId ? { streamId } : {})\n\t\t});\n\n\t\tlet res: Response;\n\t\ttry {\n\t\t\tres = await fetch(opts.endpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tbody: payload,\n\t\t\t\tsignal\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tif (signal.aborted) {\n\t\t\t\tlog('[Compute] solve aborted during fetch — empty result discarded');\n\t\t\t\treturn { outputs: {} };\n\t\t\t}\n\t\t\tthrow new Error(\n\t\t\t\t'Request blocked — your session may have expired. Reload the page to sign in again.',\n\t\t\t\t{ cause: err }\n\t\t\t);\n\t\t}\n\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted after headers — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\n\t\tconst redirectedToLogin = res.redirected && new URL(res.url).pathname === '/login';\n\t\tif (res.status === 401 || redirectedToLogin) {\n\t\t\t(opts.onSessionExpired ?? defaultOnSessionExpired)();\n\t\t}\n\n\t\tif (!res.ok) {\n\t\t\tconst errorBody = await res.text().catch(() => '');\n\t\t\tlet d: { message?: string; retryAfter?: number } = {};\n\t\t\ttry {\n\t\t\t\td = JSON.parse(errorBody);\n\t\t\t} catch {\n\t\t\t\tif (errorBody) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[Compute] non-JSON error body (HTTP ${res.status}):`,\n\t\t\t\t\t\terrorBody.slice(0, 300)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (res.status === 503 || res.status === 504) {\n\t\t\t\t// The server names the failure class (unreachable, bad API key,\n\t\t\t\t// rate-limited, timed out) — show it rather than a blanket \"offline\".\n\t\t\t\tthrow new Error(\n\t\t\t\t\td.message || 'Compute server is offline or unreachable. Please try again later.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (res.status === 429) {\n\t\t\t\tconst retryAfter = Number(res.headers.get('Retry-After')) || Number(d.retryAfter) || 5;\n\t\t\t\tcooldownUntil = Date.now() + retryAfter * 1000;\n\t\t\t\t(opts.onRateLimited ?? defaultOnRateLimited)(retryAfter);\n\t\t\t}\n\t\t\tthrow new Error(d.message || `Compute error (HTTP ${res.status})`);\n\t\t}\n\n\t\tconst ttfbMs = performance.now() - solveStart;\n\n\t\tconst downloadStart = performance.now();\n\t\tconst bodyText = await res.text();\n\t\tif (signal.aborted) {\n\t\t\tlog('[Compute] solve aborted mid-download — empty result discarded');\n\t\t\treturn { outputs: {} };\n\t\t}\n\t\tconst downloadMs = performance.now() - downloadStart;\n\t\tconst bytes = bodyText.length;\n\n\t\tlet solved: GrasshopperComputeResponse;\n\t\ttry {\n\t\t\tsolved = JSON.parse(bodyText);\n\t\t} catch (err) {\n\t\t\tthrow new Error(\n\t\t\t\t'Received an invalid response from the server. Reload the page and try again.',\n\t\t\t\t{\n\t\t\t\t\tcause: err\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\tconst processor = new GrasshopperResponseProcessor(solved, false);\n\n\t\t// The plugin's own verdict when the fork returned one; otherwise recovered from the\n\t\t// markers in Compute's flattened strings (stock server, or a plugin that predates it).\n\t\tconst outcome =\n\t\t\tdecodeOutcome(solved.selva?.outcome) ??\n\t\t\toutcomeFromComputeMessages(solved.errors ?? [], solved.warnings ?? []);\n\t\t// Decided before parsing: an older fork still ships a rejected solve's outputs, and the\n\t\t// geometry alone can be megabytes the client would only throw away.\n\t\tconst rejected = outcome.blocked || outcome.aborted;\n\n\t\tconst meshes =\n\t\t\topts.meshes && !rejected ? await opts.meshes.extract(processor.response, { debug }) : [];\n\n\t\tconst resultOutputs: Record<string, unknown> = {};\n\t\tfor (const o of rejected ? [] : outputs) {\n\t\t\tconst byId = processor.getValue({ byId: o.id }, { parseValues: true });\n\t\t\tresultOutputs[o.id] =\n\t\t\t\tbyId ??\n\t\t\t\t(o.nickname\n\t\t\t\t\t? processor.getValue({ byName: o.nickname }, { parseValues: true })\n\t\t\t\t\t: undefined);\n\t\t}\n\n\t\tif (debug) {\n\t\t\tconst valuesBytes = JSON.stringify(values).length;\n\t\t\tconst serverTiming = parseServerTiming(res.headers.get('Server-Timing'));\n\t\t\tconst serverTotal = serverTiming.total ?? 0;\n\t\t\tconst networkMs = Math.max(0, ttfbMs - serverTotal);\n\t\t\tconst sizeMB = bytes / (1024 * 1024);\n\t\t\tconst size = sizeMB >= 1 ? `${sizeMB.toFixed(2)} MB` : `${(bytes / 1024).toFixed(0)} KB`;\n\t\t\tconst mbps = downloadMs > 0 ? (sizeMB / (downloadMs / 1000)).toFixed(1) : '∞';\n\t\t\tconst heapMB = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory\n\t\t\t\t?.usedJSHeapSize;\n\t\t\tconst heap = heapMB !== undefined ? ` | heap=${(heapMB / (1024 * 1024)).toFixed(0)} MB` : '';\n\t\t\tlog(\n\t\t\t\t`[Compute] req=${(payload.length / 1024).toFixed(0)}KB (values ${(valuesBytes / 1024).toFixed(0)}KB) | ` +\n\t\t\t\t\t`ttfb=${ttfbMs.toFixed(0)}ms (network≈${networkMs.toFixed(0)} + server ${serverTotal.toFixed(0)}) | ` +\n\t\t\t\t\t`download=${downloadMs.toFixed(0)}ms (${size} @ ${mbps} MB/s)${heap}`\n\t\t\t);\n\t\t\tif (serverTiming.selva_cache !== undefined) {\n\t\t\t\tconst cacheMsg = serverTiming.selva_cache\n\t\t\t\t\t? 'HIT — served without calling compute'\n\t\t\t\t\t: 'miss — solved on Rhino.Compute';\n\t\t\t\tlog(`[Compute] └─ cache: ${cacheMsg}`);\n\t\t\t}\n\t\t}\n\n\t\treturn finalizeResult({ outputs: resultOutputs, meshes, source: solved }, outcome);\n\t};\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=`[Selva:blocked]`,t=/:\s*component\s+"([^"]*)"\s*\(([0-9a-fA-F-]{36}|\.\.\.)\)\s*$/;function n(n){let r=n.includes(e),i=!r&&n.includes(`[Selva:log]`),a=r||i||n.includes(`[Selva:msg]`),o=n,s,c=t.exec(o);return c&&(s=c[1]||void 0,o=o.slice(0,c.index)),a&&(o=o.replace(e,``).replace(`[Selva:msg]`,``).replace(`[Selva:log]`,``).replace(/^\s*\d+\.\s*Solution exception:\s*/,``)),{message:o.replace(/\s{2,}/g,` `).trim(),source:s,blocked:r,authored:a,logOnly:i}}function r(e,t){let r=(e,t)=>{let{message:r,source:i,authored:a,logOnly:o}=n(e),s=(a||i===`Message`)&&!o;return{level:t,message:r,...i?{source:i}:{},...s?{isGate:s}:{}}};return[...e.map(e=>r(e,`error`)),...t.map(e=>r(e,`warning`))]}const i=new Set([`error`,`warning`,`remark`]);function a(e){if(!e||typeof e!=`object`)return null;let t=e;if(!Array.isArray(t.diagnostics))return null;let n=new Set,r=[];for(let e of t.diagnostics){if(!e||typeof e!=`object`)continue;let t=e,a=typeof t.message==`string`?t.message.trim():``;if(!a)continue;let o=i.has(t.level)?t.level:`remark`,s=typeof t.source==`string`&&t.source?t.source:void 0,c=t.isGate===!0,l=`${o}\u0000${a}\u0000${s??``}\u0000${c}`;n.has(l)||(n.add(l),r.push({level:o,message:a,...s?{source:s}:{},...c?{isGate:c}:{}}))}let a=t.aborted===!0;return{diagnostics:r,blocked:t.blocked===!0||a,aborted:a}}function o(t,n){return{diagnostics:r(t,n),blocked:t.some(t=>t.includes(e)),aborted:!1}}function s(e,t){let n=t.blocked||t.aborted,r=n?[]:e.meshes;return{outputs:n?{}:e.outputs,...r===void 0?{}:{meshes:r},errors:t.diagnostics.filter(e=>e.level===`error`).map(e=>e.message),warnings:t.diagnostics.filter(e=>e.level===`warning`).map(e=>e.message),diagnostics:t.diagnostics,blocked:t.blocked,aborted:t.aborted,...e.source===void 0?{}:{source:e.source}}}Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return a}});
2
+ //# sourceMappingURL=outcome-DdbgqiIG.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcome-DdbgqiIG.cjs","names":[],"sources":["../src/shared/solve-fn.ts","../src/shared/compute-diagnostics.ts","../src/shared/outcome.ts"],"sourcesContent":["import type { SolveDiagnostic } from '@selvajs/schemas';\n\n/**\n * The solve contract — what a solve returns and what a caller supplies to run one.\n *\n * Lives in `shared/` because both halves of the package speak it: `client/` calls a `SolveFn` and\n * stores its `SolveResult`; `server/` produces the payload one is built from.\n */\n\n/**\n * `TMesh` is deliberately opaque and defaults to `unknown`: this package never inspects meshes, and\n * typing them would drag a renderer dependency into a package whose whole point is not having one.\n * The app that owns assembly (parse → `THREE.Object3D[]`) is the only place that knows the concrete\n * type, and it narrows by writing `SolveResult<THREE.Object3D>` at its own seam.\n */\n/**\n * Marker the Grasshopper Message component prepends to a blocking error.\n *\n * Rhino.Compute flattens runtime messages to plain strings with no component attribution, so\n * this is the only way that path can tell a deliberate block from any other solve error. The\n * local WebSocket path carries `isGate` structurally and does not need it — but the component\n * writes the marker unconditionally, because the same definition runs on both.\n *\n * Changing this string breaks blocking on deployed Selva for every already-published\n * definition, since the text is baked into the .gh file's solve output, not the plugin.\n */\nexport const SOLVE_BLOCKED_MARKER = '[Selva:blocked]';\n\n/**\n * Marker the Message component prepends to a remark or warning — the non-blocking levels.\n *\n * Same reason as `SOLVE_BLOCKED_MARKER`: on Rhino.Compute nothing structural survives, and the\n * UI has to tell a message the author chose to write from an incidental Grasshopper warning.\n * Definitions saved before this existed lack it, so the reader falls back to the component name.\n */\nexport const SOLVE_AUTHORED_MARKER = '[Selva:msg]';\n\n/**\n * Marker for an authored message whose author chose Notify = Log.\n *\n * Still theirs — it keeps its attribution and its place in the message list — but it does not\n * interrupt. Errors never carry it: their result is withheld, so they always interrupt.\n */\nexport const SOLVE_LOG_ONLY_MARKER = '[Selva:log]';\n\n/**\n * One message a solve raised, with who raised it and whether it was deliberate (`isGate`).\n * Generated from `wire-schema.json`; both transports decode into it (see `outcome.ts`).\n */\nexport type { SolveDiagnostic };\n\nexport interface SolveResult<TMesh = unknown, TSource = unknown> {\n\toutputs: Record<string, unknown>;\n\tmeshes?: TMesh[];\n\terrors?: string[];\n\twarnings?: string[];\n\t/** Structured form of `errors`/`warnings`, including remarks. Absent from transports that report neither. */\n\tdiagnostics?: SolveDiagnostic[];\n\t/**\n\t * The definition refused this solve — a guard in the definition raised an error and its\n\t * outputs were withheld deliberately. Distinct from an errored solve that still returned\n\t * values, and from a transport failure: the solve ran and its answer is \"no\". Consumers\n\t * should show `errors` instead of treating the empty outputs as a result.\n\t */\n\tblocked?: boolean;\n\t/**\n\t * The solve was stopped before it finished. Always also `blocked`: what computed before\n\t * the stop is not a result. Absent from a transport that cannot tell.\n\t */\n\taborted?: boolean;\n\t/**\n\t * The unparsed payload this result was built from, passed through verbatim. Opaque here for the\n\t * same reason as `TMesh`: a consumer that must persist or re-submit exactly what it showed the\n\t * user narrows it at its own seam. Travels with the result through the driver's memo, so a\n\t * cached hit carries the source that produced it. Unlike `meshes` it needs no ownership policy —\n\t * it is inert data, not a GPU-backed handle.\n\t *\n\t * Supplied by the `SolveFn`, so it is present only when the one in use sets it\n\t * (`createComputeFetchSolveFn` does). A transport with no raw payload to hand back leaves it\n\t * absent — see `values` on telling absent from unsupported.\n\t */\n\tsource?: TSource;\n\t/**\n\t * The input set that produced this result, stamped by the driver rather than the `SolveFn` — a\n\t * memo hit never calls the `SolveFn`, so a consumer reading values captured inside it would pair\n\t * the on-screen result with whatever solved last. Keeping the pair atomic is the point: a commit\n\t * path holding a `SolveResult` cannot mismatch artifact and inputs.\n\t *\n\t * Present only from a driver that owns a request/response pair (`createRequestResponseDriver`).\n\t * A push driver cannot attribute an incoming frame to a request and must leave this absent\n\t * rather than guess — so `undefined` means \"this transport does not supply it\", not \"no solve\n\t * yet\", and a consumer that needs the pair requires a request/response driver. See `SolveDriver`.\n\t */\n\tvalues?: Record<string, unknown>;\n}\n\n/** Implementations should listen to `signal` and abort/clean up when it fires. */\nexport type SolveFn<TMesh = unknown, TSource = unknown> = (\n\tvalues: Record<string, unknown>,\n\tsignal: AbortSignal\n) => Promise<SolveResult<TMesh, TSource>>;\n","// Rhino.Compute reports runtime messages as flat strings, so everything the UI needs to tell a\n// deliberate message from incidental noise has to be recovered from the text. This is the one\n// place that parsing happens.\n//\n// Two decorations are applied by the server, in this order:\n// \"<author's text>: component \\\"<Name>\\\" (<guid>)\" — LogRuntimeMessages, every message\n// \"<n>. Solution exception:<marker> ...\" — the numbered-error wrapper\n// The local WebSocket path carries the same facts structurally and never comes through here.\n\nimport {\n\tSOLVE_AUTHORED_MARKER,\n\tSOLVE_BLOCKED_MARKER,\n\tSOLVE_LOG_ONLY_MARKER,\n\ttype SolveDiagnostic\n} from './solve-fn.js';\n\n/**\n * Compute's per-message attribution suffix. Anchored to the end and requiring the parenthesised\n * GUID, so an author who writes `component \"x\"` mid-sentence keeps their text intact.\n */\nconst ATTRIBUTION = /:\\s*component\\s+\"([^\"]*)\"\\s*\\(([0-9a-fA-F-]{36}|\\.\\.\\.)\\)\\s*$/;\n\n/** The Selva Message component's own name, as `LogRuntimeMessages` reports it. */\nconst MESSAGE_COMPONENT_NAME = 'Message';\n\nexport interface ParsedComputeMessage {\n\t/** The author's text, with the attribution suffix and any marker removed. */\n\tmessage: string;\n\t/** Component name from the attribution suffix, when present. */\n\tsource?: string;\n\t/** Carried the block marker: a Message component refused the solve. */\n\tblocked: boolean;\n\t/** Carried any marker: a Message component wrote this. */\n\tauthored: boolean;\n\t/** The author chose Notify = Log: list it, but do not interrupt. */\n\tlogOnly: boolean;\n}\n\n/**\n * Splits one Compute message string into the author's text and its attribution.\n *\n * Leaves the text alone when the suffix is absent — an exception thrown by a script component\n * has no attribution and must not be truncated on a guess.\n */\nexport function parseComputeMessage(raw: string): ParsedComputeMessage {\n\tconst blocked = raw.includes(SOLVE_BLOCKED_MARKER);\n\tconst logOnly = !blocked && raw.includes(SOLVE_LOG_ONLY_MARKER);\n\tconst authored = blocked || logOnly || raw.includes(SOLVE_AUTHORED_MARKER);\n\tlet message = raw;\n\n\tlet source: string | undefined;\n\tconst attribution = ATTRIBUTION.exec(message);\n\tif (attribution) {\n\t\tsource = attribution[1] || undefined;\n\t\tmessage = message.slice(0, attribution.index);\n\t}\n\n\tif (authored) {\n\t\tmessage = message\n\t\t\t.replace(SOLVE_BLOCKED_MARKER, '')\n\t\t\t.replace(SOLVE_AUTHORED_MARKER, '')\n\t\t\t.replace(SOLVE_LOG_ONLY_MARKER, '')\n\t\t\t// The numbered wrapper reads as noise next to the author's own sentence. Dropped only\n\t\t\t// when a marker proves the text is one of ours.\n\t\t\t.replace(/^\\s*\\d+\\.\\s*Solution exception:\\s*/, '');\n\t}\n\n\treturn { message: message.replace(/\\s{2,}/g, ' ').trim(), source, blocked, authored, logOnly };\n}\n\n/**\n * Rebuilds structured diagnostics from Compute's `errors`/`warnings` arrays.\n *\n * `isGate` comes from the marker the component writes. The component-name fallback is for\n * definitions saved before the non-blocking marker existed; `obj.Name` is the component's type\n * name, not its nickname, so renaming one on the canvas cannot change how it behaves.\n */\nexport function parseComputeDiagnostics(\n\terrors: readonly string[],\n\twarnings: readonly string[]\n): SolveDiagnostic[] {\n\tconst build = (raw: string, level: 'error' | 'warning'): SolveDiagnostic => {\n\t\tconst { message, source, authored, logOnly } = parseComputeMessage(raw);\n\t\tconst isGate = (authored || source === MESSAGE_COMPONENT_NAME) && !logOnly;\n\t\treturn { level, message, ...(source ? { source } : {}), ...(isGate ? { isGate } : {}) };\n\t};\n\treturn [\n\t\t...errors.map((raw) => build(raw, 'error')),\n\t\t...warnings.map((raw) => build(raw, 'warning'))\n\t];\n}\n","// The one place a solve's verdict becomes a `SolveResult`. The plugin decides the verdict\n// (`SolveOutcome`: diagnostics, blocked, aborted) once, and both transports carry it: the local\n// `outputs` envelope and the VektorNode fork's `selva` block. Each driver decodes it here, so a\n// rule about what a verdict means is written once rather than once per transport.\n//\n// A Compute response without a `selva` block (stock server, older plugin) falls back to\n// `outcomeFromComputeMessages`, which recovers what it can from the flattened strings. It cannot\n// see remarks or an abort.\n\nimport type { SolveOutcome } from '@selvajs/schemas';\nimport { parseComputeDiagnostics } from './compute-diagnostics.js';\nimport { SOLVE_BLOCKED_MARKER, type SolveDiagnostic, type SolveResult } from './solve-fn.js';\n\nexport type { SolveOutcome };\n\nconst LEVELS = new Set(['error', 'warning', 'remark']);\n\n/**\n * Normalizes a wire outcome: unknown levels read as remarks, text is trimmed, empty and exactly\n * repeated messages are dropped, and `source`/`isGate` are omitted rather than null or false, so\n * the same verdict decodes to the same object from either transport. Null when `wire` is not an\n * outcome at all; the caller then falls back.\n */\nexport function decodeOutcome(wire: unknown): SolveOutcome | null {\n\tif (!wire || typeof wire !== 'object') return null;\n\tconst raw = wire as { diagnostics?: unknown; blocked?: unknown; aborted?: unknown };\n\tif (!Array.isArray(raw.diagnostics)) return null;\n\n\tconst seen = new Set<string>();\n\tconst diagnostics: SolveDiagnostic[] = [];\n\tfor (const entry of raw.diagnostics) {\n\t\tif (!entry || typeof entry !== 'object') continue;\n\t\tconst d = entry as Record<string, unknown>;\n\t\tconst message = typeof d.message === 'string' ? d.message.trim() : '';\n\t\tif (!message) continue;\n\t\tconst level = (LEVELS.has(d.level as string) ? d.level : 'remark') as SolveDiagnostic['level'];\n\t\tconst source = typeof d.source === 'string' && d.source ? d.source : undefined;\n\t\tconst isGate = d.isGate === true;\n\t\tconst key = `${level}\\u0000${message}\\u0000${source ?? ''}\\u0000${isGate}`;\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tdiagnostics.push({\n\t\t\tlevel,\n\t\t\tmessage,\n\t\t\t...(source ? { source } : {}),\n\t\t\t...(isGate ? { isGate } : {})\n\t\t});\n\t}\n\n\tconst aborted = raw.aborted === true;\n\treturn { diagnostics, blocked: raw.blocked === true || aborted, aborted };\n}\n\n/** The legacy verdict, read from Compute's flattened `errors`/`warnings` and their markers. */\nexport function outcomeFromComputeMessages(\n\terrors: readonly string[],\n\twarnings: readonly string[]\n): SolveOutcome {\n\treturn {\n\t\tdiagnostics: parseComputeDiagnostics(errors, warnings),\n\t\t// Substring, not prefix: the server numbers each entry (\"1. Solution exception: ...\").\n\t\tblocked: errors.some((e) => e.includes(SOLVE_BLOCKED_MARKER)),\n\t\taborted: false\n\t};\n}\n\n/**\n * Builds the result a driver reports. A blocked or aborted outcome is \"no result\" whatever the\n * transport sent along: no outputs, and an empty mesh list so the viewer clears. `errors` and\n * `warnings` are derived from the diagnostics for hosts that only want text.\n */\nexport function finalizeResult<TMesh, TSource>(\n\tpartial: { outputs: Record<string, unknown>; meshes?: TMesh[]; source?: TSource },\n\toutcome: SolveOutcome\n): SolveResult<TMesh, TSource> {\n\tconst rejected = outcome.blocked || outcome.aborted;\n\tconst meshes = rejected ? [] : partial.meshes;\n\treturn {\n\t\toutputs: rejected ? {} : partial.outputs,\n\t\t...(meshes !== undefined ? { meshes } : {}),\n\t\terrors: outcome.diagnostics.filter((d) => d.level === 'error').map((d) => d.message),\n\t\twarnings: outcome.diagnostics.filter((d) => d.level === 'warning').map((d) => d.message),\n\t\tdiagnostics: outcome.diagnostics,\n\t\tblocked: outcome.blocked,\n\t\taborted: outcome.aborted,\n\t\t...(partial.source !== undefined ? { source: partial.source } : {})\n\t};\n}\n"],"mappings":"AA0BA,MAAa,EAAuB,kBCN9B,EAAc,gEAwBpB,SAAgB,EAAoB,EAAmC,CACtE,IAAM,EAAU,EAAI,SAAS,CAAoB,EAC3C,EAAU,CAAC,GAAW,EAAI,SAAA,aAA8B,EACxD,EAAW,GAAW,GAAW,EAAI,SAAA,aAA8B,EACrE,EAAU,EAEV,EACE,EAAc,EAAY,KAAK,CAAO,EAgB5C,OAfI,IACH,EAAS,EAAY,IAAM,IAAA,GAC3B,EAAU,EAAQ,MAAM,EAAG,EAAY,KAAK,GAGzC,IACH,EAAU,EACR,QAAQ,EAAsB,EAAE,CAAC,CACjC,QAAQ,cAAuB,EAAE,CAAC,CAClC,QAAQ,cAAuB,EAAE,CAAC,CAGlC,QAAQ,qCAAsC,EAAE,GAG5C,CAAE,QAAS,EAAQ,QAAQ,UAAW,GAAG,CAAC,CAAC,KAAK,EAAG,SAAQ,UAAS,WAAU,SAAQ,CAC9F,CASA,SAAgB,EACf,EACA,EACoB,CACpB,IAAM,GAAS,EAAa,IAAgD,CAC3E,GAAM,CAAE,UAAS,SAAQ,WAAU,WAAY,EAAoB,CAAG,EAChE,GAAU,GAAY,IAAW,YAA2B,CAAC,EACnE,MAAO,CAAE,QAAO,UAAS,GAAI,EAAS,CAAE,QAAO,EAAI,CAAC,EAAI,GAAI,EAAS,CAAE,QAAO,EAAI,CAAC,CAAG,CACvF,EACA,MAAO,CACN,GAAG,EAAO,IAAK,GAAQ,EAAM,EAAK,OAAO,CAAC,EAC1C,GAAG,EAAS,IAAK,GAAQ,EAAM,EAAK,SAAS,CAAC,CAC/C,CACD,CC3EA,MAAM,EAAS,IAAI,IAAI,CAAC,QAAS,UAAW,QAAQ,CAAC,EAQrD,SAAgB,EAAc,EAAoC,CACjE,GAAI,CAAC,GAAQ,OAAO,GAAS,SAAU,OAAO,KAC9C,IAAM,EAAM,EACZ,GAAI,CAAC,MAAM,QAAQ,EAAI,WAAW,EAAG,OAAO,KAE5C,IAAM,EAAO,IAAI,IACX,EAAiC,CAAC,EACxC,IAAK,IAAM,KAAS,EAAI,YAAa,CACpC,GAAI,CAAC,GAAS,OAAO,GAAU,SAAU,SACzC,IAAM,EAAI,EACJ,EAAU,OAAO,EAAE,SAAY,SAAW,EAAE,QAAQ,KAAK,EAAI,GACnE,GAAI,CAAC,EAAS,SACd,IAAM,EAAS,EAAO,IAAI,EAAE,KAAe,EAAI,EAAE,MAAQ,SACnD,EAAS,OAAO,EAAE,QAAW,UAAY,EAAE,OAAS,EAAE,OAAS,IAAA,GAC/D,EAAS,EAAE,SAAW,GACtB,EAAM,GAAG,EAAM,QAAQ,EAAQ,QAAQ,GAAU,GAAG,QAAQ,IAC9D,EAAK,IAAI,CAAG,IAChB,EAAK,IAAI,CAAG,EACZ,EAAY,KAAK,CAChB,QACA,UACA,GAAI,EAAS,CAAE,QAAO,EAAI,CAAC,EAC3B,GAAI,EAAS,CAAE,QAAO,EAAI,CAAC,CAC5B,CAAC,EACF,CAEA,IAAM,EAAU,EAAI,UAAY,GAChC,MAAO,CAAE,cAAa,QAAS,EAAI,UAAY,IAAQ,EAAS,SAAQ,CACzE,CAGA,SAAgB,EACf,EACA,EACe,CACf,MAAO,CACN,YAAa,EAAwB,EAAQ,CAAQ,EAErD,QAAS,EAAO,KAAM,GAAM,EAAE,SAAS,CAAoB,CAAC,EAC5D,QAAS,EACV,CACD,CAOA,SAAgB,EACf,EACA,EAC8B,CAC9B,IAAM,EAAW,EAAQ,SAAW,EAAQ,QACtC,EAAS,EAAW,CAAC,EAAI,EAAQ,OACvC,MAAO,CACN,QAAS,EAAW,CAAC,EAAI,EAAQ,QACjC,GAAI,IAAW,IAAA,GAAyB,CAAC,EAAd,CAAE,QAAO,EACpC,OAAQ,EAAQ,YAAY,OAAQ,GAAM,EAAE,QAAU,OAAO,CAAC,CAAC,IAAK,GAAM,EAAE,OAAO,EACnF,SAAU,EAAQ,YAAY,OAAQ,GAAM,EAAE,QAAU,SAAS,CAAC,CAAC,IAAK,GAAM,EAAE,OAAO,EACvF,YAAa,EAAQ,YACrB,QAAS,EAAQ,QACjB,QAAS,EAAQ,QACjB,GAAI,EAAQ,SAAW,IAAA,GAAyC,CAAC,EAA9B,CAAE,OAAQ,EAAQ,MAAO,CAC7D,CACD"}
@@ -0,0 +1,2 @@
1
+ const e=`[Selva:blocked]`,t=/:\s*component\s+"([^"]*)"\s*\(([0-9a-fA-F-]{36}|\.\.\.)\)\s*$/;function n(n){let r=n.includes(e),i=!r&&n.includes(`[Selva:log]`),a=r||i||n.includes(`[Selva:msg]`),o=n,s,c=t.exec(o);return c&&(s=c[1]||void 0,o=o.slice(0,c.index)),a&&(o=o.replace(e,``).replace(`[Selva:msg]`,``).replace(`[Selva:log]`,``).replace(/^\s*\d+\.\s*Solution exception:\s*/,``)),{message:o.replace(/\s{2,}/g,` `).trim(),source:s,blocked:r,authored:a,logOnly:i}}function r(e,t){let r=(e,t)=>{let{message:r,source:i,authored:a,logOnly:o}=n(e),s=(a||i===`Message`)&&!o;return{level:t,message:r,...i?{source:i}:{},...s?{isGate:s}:{}}};return[...e.map(e=>r(e,`error`)),...t.map(e=>r(e,`warning`))]}const i=new Set([`error`,`warning`,`remark`]);function a(e){if(!e||typeof e!=`object`)return null;let t=e;if(!Array.isArray(t.diagnostics))return null;let n=new Set,r=[];for(let e of t.diagnostics){if(!e||typeof e!=`object`)continue;let t=e,a=typeof t.message==`string`?t.message.trim():``;if(!a)continue;let o=i.has(t.level)?t.level:`remark`,s=typeof t.source==`string`&&t.source?t.source:void 0,c=t.isGate===!0,l=`${o}\u0000${a}\u0000${s??``}\u0000${c}`;n.has(l)||(n.add(l),r.push({level:o,message:a,...s?{source:s}:{},...c?{isGate:c}:{}}))}let a=t.aborted===!0;return{diagnostics:r,blocked:t.blocked===!0||a,aborted:a}}function o(t,n){return{diagnostics:r(t,n),blocked:t.some(t=>t.includes(e)),aborted:!1}}function s(e,t){let n=t.blocked||t.aborted,r=n?[]:e.meshes;return{outputs:n?{}:e.outputs,...r===void 0?{}:{meshes:r},errors:t.diagnostics.filter(e=>e.level===`error`).map(e=>e.message),warnings:t.diagnostics.filter(e=>e.level===`warning`).map(e=>e.message),diagnostics:t.diagnostics,blocked:t.blocked,aborted:t.aborted,...e.source===void 0?{}:{source:e.source}}}export{e as i,s as n,o as r,a as t};
2
+ //# sourceMappingURL=outcome-H5XjAroQ.js.map