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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ This package owns everything between those two points. Nothing else.
19
19
  | ----------------------- | ----------------------- |
20
20
  | A web page with sliders | `@selvajs/solve/client` |
21
21
  | An API endpoint | `@selvajs/solve/server` |
22
- | Just need the types | `@selvajs/solve/shared` |
22
+ | Types and verdict logic | `@selvajs/solve/shared` |
23
23
 
24
24
  There is no `.` export on purpose — see [Why no root export](#why-no-root-export).
25
25
 
@@ -157,15 +157,17 @@ if rebuilt per request.
157
157
  // $lib/server/compute/engine.server.ts
158
158
  import { SolveEngine } from '@selvajs/solve/server';
159
159
 
160
- export const engine = new SolveEngine({ limits: computeLimits, logger });
160
+ export const engine = new SolveEngine({ limits: computeLimits });
161
161
  ```
162
162
 
163
163
  ```ts
164
164
  // routes/api/v1/compute/+server.ts
165
+ import { solveInputsFromSchema } from '@selvajs/solve/server';
166
+
165
167
  const outcome = await engine.solve({
166
168
  server: serverConfig, // which Rhino.Compute
167
169
  definitionSource, // the .gh file (bytes, URL, or cached ref)
168
- inputs, // the definition's input params
170
+ inputs: solveInputsFromSchema(storedSchema), // yours, never the request's
169
171
  values, // what the browser sent
170
172
  signal: request.signal, // client disconnects → cancel upstream
171
173
  acceptEncoding: request.headers.get('accept-encoding') ?? ''
@@ -174,6 +176,9 @@ const outcome = await engine.solve({
174
176
  return engine.toWebResponse(outcome);
175
177
  ```
176
178
 
179
+ Read `inputs` from the schema you stored. A request that supplies them picks the ids, types and
180
+ bounds that reach the definition: it can drop a limit or fill a slot your host reserved.
181
+
177
182
  `solve()` doesn't throw for an expected failure. It returns one of these, so you can branch before
178
183
  handing it to `toWebResponse`:
179
184
 
@@ -197,11 +202,11 @@ return engine.toWebResponse(outcome);
197
202
  engine.solve()
198
203
  │
199
204
  ├─ 1. warm client cache ····· reuse the open Rhino.Compute connection
200
- ├─ 2. definition bytes ······ already uploaded? send a pointer, not the .gh
205
+ ├─ 2. build input tree ······ values → Grasshopper DataTree
201
206
  ├─ 3. single-flight ········· 5 users, same inputs → 1 actual solve
202
- ├─ 4. build input tree ······ values → Grasshopper DataTree
207
+ ├─ 4. definition bytes ······ already uploaded? send a pointer, not the .gh
203
208
  ├─ 5. SOLVE ················· the only slow step
204
- └─ 6. gzip + Server-Timing ·· ready-to-send envelope
209
+ └─ 6. gzip + Server-Timing ·· one envelope per caller
205
210
  ```
206
211
 
207
212
  Each layer is covered in [`src/server/README.md`](./src/server/README.md).
@@ -244,8 +249,8 @@ solve needs.
244
249
 
245
250
  ## Why no root export
246
251
 
247
- `server/` uses `node:zlib`, `node:crypto`, and reads compute-server credentials. A `.` barrel
248
- joining both halves would let a browser bundle pull all of that in by accident.
252
+ `server/` uses `node:zlib` and `undici`, and holds compute-server credentials. A `.` barrel joining
253
+ both halves would let a browser bundle pull all of that in by accident.
249
254
 
250
255
  ```
251
256
  shared/ the types both sides speak
@@ -253,7 +258,8 @@ joining both halves would let a browser bundle pull all of that in by accident.
253
258
  client/ server/ never import each other
254
259
  ```
255
260
 
256
- Enforced by `no-restricted-imports` and a bundle-boundary test, not by convention.
261
+ Enforced by `no-restricted-imports`, a `tsc` pass per half and a bundle-boundary test, not by
262
+ convention.
257
263
 
258
264
  ## Not in this package
259
265
 
@@ -267,4 +273,4 @@ Enforced by `no-restricted-imports` and a bundle-boundary test, not by conventio
267
273
  ## Going deeper
268
274
 
269
275
  - [`src/client/README.md`](./src/client/README.md) — writing your own driver, the value map, mesh ownership
270
- - [`src/server/README.md`](./src/server/README.md) — cache layers, coalescing, `runSolvePipeline`
276
+ - [`src/server/README.md`](./src/server/README.md) — cache layers, coalescing, encoding
@@ -0,0 +1,13 @@
1
+ // src/shared/abort.ts
2
+ /** `promise`'s outcome, or a rejection with `signal`'s reason if that fires first. */
3
+ function raceAbort(promise, signal) {
4
+ return new Promise((resolve, reject) => {
5
+ const onAbort = () => reject(signal.reason);
6
+ if (signal.aborted) onAbort();
7
+ signal.addEventListener("abort", onAbort, { once: true });
8
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
9
+ });
10
+ }
11
+ export { raceAbort as t };
12
+
13
+ //# sourceMappingURL=abort-CNJ8Sn4g.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort-CNJ8Sn4g.js","names":[],"sources":["../src/shared/abort.ts"],"sourcesContent":["/** `promise`'s outcome, or a rejection with `signal`'s reason if that fires first. */\nexport function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst onAbort = () => reject(signal.reason);\n\t\tif (signal.aborted) onAbort();\n\t\tsignal.addEventListener('abort', onAbort, { once: true });\n\t\tpromise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));\n\t});\n}\n"],"mappings":";;AACA,SAAgB,UAAa,SAAqB,QAAiC;CAClF,OAAO,IAAI,SAAS,SAAS,WAAW;EACvC,MAAM,gBAAgB,OAAO,OAAO,MAAM;EAC1C,IAAI,OAAO,SAAS,QAAQ;EAC5B,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EACxD,QAAQ,KAAK,SAAS,MAAM,CAAC,CAAC,cAAc,OAAO,oBAAoB,SAAS,OAAO,CAAC;CACzF,CAAC;AACF"}
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as SolveResult, n as decodeOutcome, o as SolveDiagnostic, r as finalizeResult, s as SolveFn, t as SolveOutcome } from "./outcome-DsNmx27P.js";
2
- import { ProgressPayload, SolveEvent, UISchema } from "@selvajs/schemas";
1
+ import { a as SolveFn, i as SolveDiagnostic, n as decodeOutcome, o as SolveResult, r as finalizeResult, t as SolveOutcome } from "./outcome-Bk9mnMi0.js";
2
+ import { ProgressPayload, SolveEndedKind, SolveEvent, UISchema } from "@selvajs/schemas";
3
3
  import { GrasshopperComputeResponse } from "@selvajs/compute/grasshopper";
4
4
  /**
5
5
  * Transport behind a solve session. `solve()` never returns outputs directly — results
@@ -31,11 +31,12 @@ interface SolveDriver {
31
31
  */
32
32
  readonly hasQueued?: boolean;
33
33
  /**
34
- * Drops cached solve results. Optional — only the request/response driver has a
35
- * client-side memo to clear. Call on rebuild: a definition swap must not serve a
36
- * stale result from the prior definition's input space.
34
+ * Forgets the current definition: stops its running solve, drops its cached results, and
35
+ * never reports for it again. The session calls this on rebuild and dispose, where a late
36
+ * result would land in the wrong definition. Optional: a push transport leaves it out, because
37
+ * the solve it would stop belongs to the Grasshopper canvas, not to this session.
37
38
  */
38
- clearCache?(): void;
39
+ reset?(): void;
39
40
  /**
40
41
  * Live events from the running solve, when the transport carries them (a WebSocket frame
41
42
  * locally, an SSE stream in cloud mode). Returns the unsubscribe. A driver without a live
@@ -69,9 +70,9 @@ interface LiveSolveState {
69
70
  readonly diagnostics: readonly SolveDiagnostic[];
70
71
  /** The newest `progress` per source. A report without a source is keyed by `''`. */
71
72
  readonly progress: ReadonlyMap<string, ProgressPayload>;
72
- /** `solveEnded`'s payload once it arrives: `ok`, `blocked`, `aborted` or `error`. */
73
+ /** `solveEnded`'s payload once it arrives. */
73
74
  readonly ended: {
74
- readonly kind: string;
75
+ readonly kind: SolveEndedKind;
75
76
  } | null;
76
77
  /** Events of kinds this package does not know, oldest first. */
77
78
  readonly other: readonly SolveEvent[];
@@ -79,7 +80,6 @@ interface LiveSolveState {
79
80
  readonly lastSeq: number;
80
81
  }
81
82
  export declare const EMPTY_LIVE_SOLVE: LiveSolveState;
82
- export declare function reduceLiveSolve(state: LiveSolveState, event: SolveEvent): LiveSolveState;
83
83
  interface ExternalValueRef {
84
84
  scopeKey: string;
85
85
  inputId: string;
@@ -105,79 +105,7 @@ export declare function getExternalInputs(schema: UISchema): ExternalInput[];
105
105
  * live meshes stay on `session.meshes`.
106
106
  */
107
107
  type RetainedSolveResult<TSource = unknown> = Omit<SolveResult<never, TSource>, 'meshes'>;
108
- interface SolveSessionState {
109
- values: Record<string, unknown>;
110
- error: string;
111
- computeErrors: string[];
112
- computeWarnings: string[];
113
- /**
114
- * The same messages with their level, source and `isGate`. What decides whether a solve
115
- * interrupts the user; the two string arrays above stay the plain display lists.
116
- */
117
- diagnostics: SolveDiagnostic[];
118
- /**
119
- * The last solve was refused by a guard in the definition. Separate from `computeErrors`
120
- * being non-empty: a solve can report errors and still return usable outputs, and the UI
121
- * needs to tell "solved, with complaints" from "would not solve".
122
- */
123
- blocked: boolean;
124
- /**
125
- * The last solve raised messages the user has not acknowledged yet. While true the result
126
- * is withheld: `meshes` and the solve's outputs stay at their previous values until
127
- * `acknowledge()` releases them.
128
- */
129
- awaitingAck: boolean;
130
- /**
131
- * The withheld result, kept whole so `acknowledge()` can apply it without a re-solve.
132
- * Null whenever `awaitingAck` is false. A newer solve replaces it: the user is always
133
- * confirming the most recent result, never a stale one.
134
- */
135
- heldResult: SolveResult | null;
136
- meshes: unknown[];
137
- /** Values changed since the last solve, in manual (instanceSolve === false) mode. */
138
- pendingValues: Record<string, unknown>;
139
- hasPendingChanges: boolean;
140
- hasNeverSolved: boolean;
141
- /**
142
- * The last result reported, minus its meshes. Null before the first solve.
143
- *
144
- * Lags `values` by design: nothing updates it on `setValue` or `loadValues`, so between a
145
- * value change and the next report it describes the *previous* input set. That is what a
146
- * commit gate wants — what is on screen, not what is pending — so `lastResult.values`
147
- * disagreeing with `state.values` mid-scrub is the feature, not a bug.
148
- */
149
- lastResult: RetainedSolveResult | null;
150
- }
151
- type ExternalReader = (ref: ExternalValueRef) => unknown | undefined;
152
- /**
153
- * Client-sourced inputs are hydrated from `read` and left absent when no stored value
154
- * exists, so the missing-inputs panel can detect them. Other inputs fall back to their
155
- * declared default, then the paramType default.
156
- */
157
- export declare function buildInitialValues(schema: UISchema, scopeKey: string, read: ExternalReader): Record<string, unknown>;
158
- /** Manual-solve schemas (instanceSolve === false) start dirty; auto-solve schemas start clean. */
159
- export declare function makeInitialFlags(instanceSolve: boolean | undefined): {
160
- hasPendingChanges: boolean;
161
- hasNeverSolved: boolean;
162
- };
163
- /** Manual mode defers (records the pending value, raises the dirty flag) instead of solving. */
164
- export declare function applyValueChange(state: SolveSessionState, id: string, value: unknown, instanceSolve: boolean | undefined): {
165
- state: SolveSessionState;
166
- shouldSolve: boolean;
167
- };
168
- /**
169
- * Solve outputs live in the same `values` map as inputs (so widgets like dynamic value
170
- * lists can read them) but must not be echoed back to the driver: a measured 6.4 MB
171
- * options-list output showed this can re-upload a multi-MB payload no backend reads.
172
- */
173
- export declare function pickInputValues(schema: UISchema | undefined, values: Record<string, unknown>): Record<string, unknown>;
174
- export declare function applySolveResult(state: SolveSessionState, result: SolveResult): SolveSessionState;
175
108
  type SolvePhase = 'idle' | 'solving' | 'review' | 'blocked';
176
- /**
177
- * One value for what a host should show. Solving wins: a new solve supersedes a held or refused
178
- * result. `isSolving` is passed in because the driver owns it.
179
- */
180
- export declare function solvePhase(state: SolveSessionState, isSolving: boolean): SolvePhase;
181
109
  /**
182
110
  * How a solve ended, once nothing about it can change any more: a held result counts only
183
111
  * after the user accepts or rejects it, and a result a queued solve is about to replace never
@@ -188,7 +116,7 @@ type SolveSettlement = {
188
116
  result: RetainedSolveResult;
189
117
  diagnostics: SolveDiagnostic[];
190
118
  } |
191
- /** The user rejected a held result; the screen still shows the one before it. */
119
+ /** A held result the user rejected or a newer solve replaced; the screen still shows the one before it. */
192
120
  {
193
121
  status: 'discarded';
194
122
  diagnostics: SolveDiagnostic[];
@@ -262,10 +190,10 @@ interface SolveSession {
262
190
  abort(): void;
263
191
  readonly meshes: unknown[];
264
192
  /**
265
- * The last reported result without its meshes — what the viewer is currently showing,
266
- * including the `source`/`values` pair a commit path needs. Null before the first solve,
267
- * and again after `rebuild()`. Survives `reportError`: a failed solve leaves the last good
268
- * geometry on screen, so a host committing what it sees should still see what produced it.
193
+ * What the viewer is currently showing, including the `source`/`values` pair a commit path
194
+ * needs. Null before the first solve, and again after `rebuild()`. Survives `reportError`: a
195
+ * failed solve leaves the last good geometry on screen, so a host committing what it sees
196
+ * should still see what produced it.
269
197
  */
270
198
  readonly lastResult: RetainedSolveResult | null;
271
199
  readonly hasPendingChanges: boolean;
@@ -282,7 +210,8 @@ interface SolveSession {
282
210
  /**
283
211
  * The result for the current inputs, solving only when it has to: right away from the result
284
212
  * on screen when nothing changed, after the running or held solve when there is one, else
285
- * after a new solve. For an action that needs a fresh result, such as an export.
213
+ * after a new solve. For an action that needs a fresh result, such as an export. Inputs whose
214
+ * result the user just discarded settle as `discarded` rather than ask them again.
286
215
  */
287
216
  ensureSolved(): Promise<SolveSettlement>;
288
217
  /** Fires once per settled solve, the ones input changes start included. Returns the unsubscribe. */
@@ -298,9 +227,9 @@ interface SolveSession {
298
227
  /** For driver-owned state the session only forwards: `isSolving` lives on the driver, so wire this to its `onChange`. */
299
228
  notify(): void;
300
229
  /**
301
- * Stops listening to the driver's live events and drops every subscriber. Call when the
302
- * session is discarded: the event source can outlive it (the cloud stream belongs to the tab),
303
- * and would otherwise keep feeding a session nobody reads.
230
+ * Resets the driver, stops listening to its live events and drops every subscriber. Call when
231
+ * the session is discarded: the event source can outlive it (the cloud stream belongs to the
232
+ * tab), and would otherwise keep feeding a session nobody reads. Inert afterwards.
304
233
  */
305
234
  dispose(): void;
306
235
  }
@@ -312,9 +241,13 @@ interface SolveSessionArgs {
312
241
  export declare function createSolveSession(args: SolveSessionArgs): SolveSession;
313
242
  /**
314
243
  * Deterministic string key for a set of input values. Sorts object keys at every level so
315
- * two logically-equal inputs built in different key order collide.
244
+ * two logically-equal inputs built in different key order collide. Otherwise matches what
245
+ * `JSON.stringify` sends: `toJSON` is honoured and an `undefined` property is absent, which a
246
+ * server treats differently from `null`.
247
+ *
248
+ * Throws on a `bigint`, as `JSON.stringify` does.
316
249
  */
317
- export declare function stableInputKey(values: Record<string, unknown>): string;
250
+ export declare function stableInputKey(input: unknown): string;
318
251
  /**
319
252
  * Omit when results are mesh-free or nothing takes ownership of the meshes — the memo then
320
253
  * stores and serves them by reference, which is safe only because nothing frees them behind
@@ -326,17 +259,6 @@ interface MeshPolicy<TMesh> {
326
259
  /** Must not free anything shared with live content (e.g. materials another object still uses). */
327
260
  release(meshes: TMesh[]): void;
328
261
  }
329
- interface SolveMemoOptions<TMesh> {
330
- /** Entry cap, not a byte cap. Kept small on purpose: this targets the slider-scrub loop, not a durable cache. */
331
- max?: number;
332
- meshPolicy?: MeshPolicy<TMesh>;
333
- }
334
- interface SolveMemo<TMesh = unknown> {
335
- get(values: Record<string, unknown>): SolveResult<TMesh> | undefined;
336
- set(values: Record<string, unknown>, result: SolveResult<TMesh>): void;
337
- clear(): void;
338
- }
339
- export declare function createSolveMemo<TMesh = unknown>(options?: SolveMemoOptions<TMesh>): SolveMemo<TMesh>;
340
262
  interface RequestResponseDriverOptions<TMesh> {
341
263
  /** How long one solve may take before the client aborts it (ms). Pass the server's `COMPUTE_SOLVE_DEADLINE_MS` so the two agree. */
342
264
  solveDeadlineMs: number;
@@ -363,42 +285,16 @@ interface SolveEventStream extends SolveEventSource {
363
285
  readonly streamId: string;
364
286
  /** The solve the server last reported as started and not yet ended; null between solves. */
365
287
  readonly currentSolveId: string | null;
288
+ /** Final: the stream does not reopen, and a later subscriber hears nothing. */
366
289
  close(): void;
367
290
  }
368
291
  export declare function createSolveEventStream(options: SolveEventStreamOptions): SolveEventStream;
369
- /**
370
- * Single-in-flight, latest-wins throttle for any async operation: only one run at a
371
- * time, a newly triggered value overwrites the single pending slot (no queue), and a
372
- * stale run can be aborted via `AbortController` or timeout.
373
- *
374
- * Framework-free: `isRunning` is a plain getter, not a store. A reactive host must
375
- * subscribe via `onChange` (see `@selvajs/ui`'s rune adapter) — polling the getter
376
- * directly reads correct values but won't trigger a re-render.
377
- */
378
- interface AsyncThrottleOptions {
379
- /**
380
- * How long one run may take before it is aborted (ms). Required and deliberately
381
- * undefaulted: this is a generic throttle, so it has no basis for guessing what
382
- * the caller's work costs. Callers driving a Selva solve pass the deadline the
383
- * server enforces (`COMPUTE_SOLVE_DEADLINE_MS`) — a shorter local guess would abort
384
- * solves the server would have finished.
385
- */
386
- runDeadlineMs: number;
387
- onChange?: () => void;
388
- }
389
- interface AsyncThrottle<T> {
390
- trigger: (values: T) => void;
391
- readonly isRunning: boolean;
392
- readonly hasPending: boolean;
393
- cancel: () => void;
394
- }
395
- export declare function createAsyncThrottle<T>(run: (values: T, signal: AbortSignal) => Promise<void>, options: AsyncThrottleOptions): AsyncThrottle<T>;
396
292
  interface ComputeFetchSolveFnOptions<TMesh = unknown> {
397
293
  /** The compute endpoint to POST to, e.g. `/api/compute`. */
398
294
  endpoint: string;
399
295
  /** Definition URL (or `local:<guid>`) to solve — read per-solve so it can track a route param. */
400
296
  definitionUrl: () => string;
401
- /** Schema input params, sent as the request body's `inputs`. */
297
+ /** Schema input params, sent as the request body's `inputs`. A stored definition's server ignores them. */
402
298
  inputs: () => Array<{
403
299
  id: string;
404
300
  }>;
@@ -415,15 +311,18 @@ interface ComputeFetchSolveFnOptions<TMesh = unknown> {
415
311
  meshes?: {
416
312
  extract: (response: GrasshopperComputeResponse, opts: {
417
313
  debug: boolean;
314
+ signal: AbortSignal;
418
315
  }) => TMesh[] | Promise<TMesh[]>;
419
316
  };
420
317
  /** Gates ALL console telemetry (timing, cache verdicts, whales, heap). Default `false`. */
421
318
  debug?: boolean;
422
- /** Called on a 429. Default: throws a cooldown-aware Error; the cooldown itself is tracked either way. */
319
+ /** Called on a 429 and on a solve refused during its cooldown, before the rate-limit Error. */
423
320
  onRateLimited?: (retryAfterSeconds: number) => void;
424
- /** Called on a 401 or a login-page redirect. Default: throws an actionable "reload to sign in again" Error. */
321
+ /** Called on a 401 or a login-page redirect, before the "sign in again" Error. */
425
322
  onSessionExpired?: () => void;
323
+ /** Where the server redirects an expired session. Default `/login`. */
324
+ loginPath?: string;
426
325
  }
427
326
  export declare function createComputeFetchSolveFn<TMesh = unknown>(opts: ComputeFetchSolveFnOptions<TMesh>): SolveFn<TMesh, GrasshopperComputeResponse>;
428
- 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, type SolveSettlement, decodeOutcome, finalizeResult };
327
+ export { type ComputeFetchSolveFnOptions, type ExternalInput, type ExternalValueRef, type LiveSolveState, type MeshPolicy, type RequestResponseDriverOptions, type RetainedSolveResult, type SolveDiagnostic, type SolveDriver, type SolveEventSource, type SolveEventStream, type SolveEventStreamOptions, type SolveFn, type SolveOutcome, type SolvePhase, type SolveReporter, type SolveResult, type SolveSession, type SolveSessionArgs, type SolveSettlement, decodeOutcome, finalizeResult };
429
328
  //# sourceMappingURL=client.d.ts.map