@selvajs/solve 1.0.7 → 1.0.8

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.
Files changed (2) hide show
  1. package/README.md +230 -33
  2. package/package.json +8 -8
package/README.md CHANGED
@@ -1,52 +1,249 @@
1
1
  # `@selvajs/solve`
2
2
 
3
- One owner for the solve flow — **from an input change to a solve result, on both sides of the wire,
4
- with no transport and no UI.**
3
+ **User moves a slider → a Grasshopper definition runs → geometry comes back.**
4
+ This package owns everything between those two points. Nothing else.
5
5
 
6
- ## Layout
6
+ ```
7
+ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
8
+ │ BROWSER │ │ YOUR SERVER │ │ RHINO.COMPUTE │
9
+ │ │ │ │ │ │
10
+ │ width: 12 ──┼── POST ──┼─► SolveEngine ┼─────────►│ wall.gh │
11
+ │ height: 20 │ │ + caches │ │ runs │
12
+ │ │◄── JSON ─┼───────────────┼◄─────────┼── │
13
+ │ meshes ✦ │ │ │ │ │
14
+ └───────────────┘ └───────────────┘ └───────────────┘
15
+ solve/client solve/server
16
+ ```
17
+
18
+ | You are building… | You import… |
19
+ | ----------------------- | ----------------------- |
20
+ | A web page with sliders | `@selvajs/solve/client` |
21
+ | An API endpoint | `@selvajs/solve/server` |
22
+ | Just need the types | `@selvajs/solve/shared` |
23
+
24
+ There is no `.` export on purpose — see [Why no root export](#why-no-root-export).
25
+
26
+ ---
27
+
28
+ ## Quickstart: the browser half
29
+
30
+ Copy this. It is the whole client setup.
31
+
32
+ ```ts
33
+ import {
34
+ createComputeFetchSolveFn,
35
+ createRequestResponseDriver,
36
+ createSolveSession
37
+ } from '@selvajs/solve/client';
38
+
39
+ // 1. HOW to reach your server
40
+ const solveFn = createComputeFetchSolveFn({
41
+ endpoint: '/api/v1/compute',
42
+ definitionUrl: () => `local:${definitionGuid}`,
43
+ inputs: () => schema.inputs,
44
+ outputs: () => schema.outputs
45
+ });
46
+
47
+ // 2. WHEN to actually send (throttles drags, caches repeats)
48
+ const driver = createRequestResponseDriver(solveFn, () => session, {
49
+ solveDeadlineMs: 30_000
50
+ });
51
+
52
+ // 3. WHAT the UI reads and writes
53
+ const session = createSolveSession({ schema, scopeKey: definitionGuid, driver });
54
+ ```
55
+
56
+ After that, your UI only ever touches `session`:
57
+
58
+ ```ts
59
+ session.setValue('width', 12); // writes the value AND triggers a solve
60
+ session.values; // → { width: 12, height: 20 }
61
+ session.isSolving; // → true while in flight (spinner)
62
+ session.meshes; // → geometry to render
63
+ session.computeErrors; // → what Grasshopper complained about
64
+ ```
65
+
66
+ **You normally never call `solve()` yourself.** `setValue` does it for you. The exception is
67
+ manual mode:
68
+
69
+ | `schema.instanceSolve` | `setValue` does… | You call `solve()`… |
70
+ | ---------------------- | ------------------------------------------ | ---------------------- |
71
+ | `true` (default) | writes the value **and** solves | never |
72
+ | `false` | writes the value, sets `hasPendingChanges` | on your "Solve" button |
73
+
74
+ ```svelte
75
+ <!-- manual mode: the definition is slow, so the user decides when to run it -->
76
+ <button onclick={() => session.solve()} disabled={!session.hasPendingChanges}> Solve </button>
77
+ ```
78
+
79
+ ### Why three objects and not one
80
+
81
+ ```
82
+ your UI
83
+ │ setValue('width', 12)
84
+ ▼
85
+ ┌───────────┐ "solve these values" ┌──────────┐
86
+ │ SESSION │ ──────────────────────► │ DRIVER │
87
+ │ │ │ │
88
+ │ values │ │ throttle │ ← drops mid-drag values
89
+ │ isSolving │ │ memo │ ← repeat inputs, no network
90
+ │ meshes │ ◄────────────────────── │ abort │ ← cancels superseded solves
91
+ │ errors │ report(result) └────┬─────┘
92
+ └───────────┘ │ calls
93
+ ▼
94
+ ┌──────────┐
95
+ │ SolveFn │ → fetch → your server
96
+ └──────────┘
97
+ ```
98
+
99
+ Dragging a slider fires dozens of value changes a second. The **session** doesn't care — it just
100
+ records values. The **driver** is what stops you from sending dozens of requests.
101
+
102
+ Swap the `SolveFn` and the same session runs over a WebSocket, or against a stub in a test.
103
+
104
+ ### In Svelte: one extra step
105
+
106
+ The session exposes plain getters — no runes. Read them directly in a component and you get
107
+ **correct values that never re-render**. Use the wrapper from `@selvajs/ui` instead:
108
+
109
+ ```diff
110
+ - const session = createSolveSession({ schema, scopeKey, driver });
111
+ + const session = useSolveSession({ schema, scopeKey, driver }); // from '@selvajs/ui'
112
+ ```
113
+
114
+ Rendering geometry too? Two more lines on the driver:
115
+
116
+ ```ts
117
+ import { meshPolicy } from '@selvajs/visualization/parse';
7
118
 
119
+ const driver = createRequestResponseDriver(onSolve, () => session, {
120
+ solveDeadlineMs,
121
+ meshPolicy, // without this, a cached hit serves an already-disposed mesh
122
+ onChange: () => session.notify() // without this, the spinner never moves
123
+ });
8
124
  ```
9
- shared/ the vocabulary both halves speak — SolveResult, SolveFn, SolveInput, input keying
10
- client/ form state machine, auto/manual decision, throttle, result memo, driver seam
11
- server/ solve pipeline (tree build → solve → serialize → envelope), caches, single-flight
125
+
126
+ Both fail silently if you skip them. They are the only two client-side gotchas.
127
+
128
+ ---
129
+
130
+ ## Quickstart: the server half
131
+
132
+ **One engine per app**, at module scope — it holds warm connections and caches that are worthless
133
+ if rebuilt per request.
134
+
135
+ ```ts
136
+ // $lib/server/compute/engine.server.ts
137
+ import { SolveEngine } from '@selvajs/solve/server';
138
+
139
+ export const engine = new SolveEngine({ limits: computeLimits, logger });
12
140
  ```
13
141
 
14
- `client/` and `server/` both depend on `shared/`, and **never on each other**. See
15
- [`src/server/README.md`](./src/server/README.md#node-only-and-that-is-load-bearing) for how that's
16
- enforced.
142
+ ```ts
143
+ // routes/api/v1/compute/+server.ts
144
+ const outcome = await engine.solve({
145
+ server: serverConfig, // which Rhino.Compute
146
+ definitionSource, // the .gh file (bytes, URL, or cached ref)
147
+ inputs, // the definition's input params
148
+ values, // what the browser sent
149
+ signal: request.signal, // client disconnects → cancel upstream
150
+ acceptEncoding: request.headers.get('accept-encoding') ?? ''
151
+ });
17
152
 
18
- See [`src/client/README.md`](./src/client/README.md) for the session, the driver seam, and how to
19
- write a transport; [`src/server/README.md`](./src/server/README.md) for the pipeline and the cache
20
- tiers.
153
+ return engine.toWebResponse(outcome);
154
+ ```
155
+
156
+ `solve()` doesn't throw for an expected failure. It returns one of these, so you can branch before
157
+ handing it to `toWebResponse`:
158
+
159
+ | `outcome.kind` | What happened | Becomes |
160
+ | ----------------- | --------------------------- | ------------------------ |
161
+ | `'ok'` | Geometry is ready | `200` |
162
+ | `'timeout'` | Definition took too long | `504` |
163
+ | `'client_abort'` | User navigated away | `499` |
164
+ | `'too_large'` | Response over the size cap | `413` |
165
+ | `'shed'` | Queue full — retry later | `503` + `Retry-After` |
166
+ | `'compute_error'` | Rhino.Compute itself failed | rethrown to your handler |
167
+
168
+ ```ts
169
+ if (outcome.kind === 'ok') recordMetric('ok', { durationMs: outcome.solveMs });
170
+ return engine.toWebResponse(outcome);
171
+ ```
21
172
 
22
- ## What it must never know
173
+ ### What the engine does on your behalf
23
174
 
24
- - **No UI framework.** No Svelte, no runes, no DOM. `client/` is a state machine, not a component.
25
- - **No renderer.** No `three`. `SolveResult<TMesh>` never inspects a mesh — the app that parses a
26
- response into `THREE.Object3D[]` is the only place that knows the concrete type. This is what lets
27
- a headless CLI solve without dragging in a parse layer. See
28
- [`src/client/README.md`](./src/client/README.md#mesh-ownership-is-injected-not-known-here) for how
29
- the result memo handles mesh ownership without knowing what a mesh is.
30
- - **No authorization, orgs, projects, or share links.** App policy.
31
- - **No HTTP.** `client/` stops at a `SolveFn`; `server/` stops at a `SolveOutcome`. Mapping either to
32
- a status code is the app's job.
175
+ ```
176
+ engine.solve()
177
+ │
178
+ ├─ 1. warm client cache ····· reuse the open Rhino.Compute connection
179
+ ├─ 2. definition bytes ······ already uploaded? send a pointer, not the .gh
180
+ ├─ 3. single-flight ········· 5 users, same inputs → 1 actual solve
181
+ ├─ 4. build input tree ······ values → Grasshopper DataTree
182
+ ├─ 5. SOLVE ················· the only slow step
183
+ └─ 6. gzip + Server-Timing ·· ready-to-send envelope
184
+ ```
33
185
 
34
- ## No root barrel — on purpose
186
+ Each layer is covered in [`src/server/README.md`](./src/server/README.md).
35
187
 
36
- `@selvajs/solve` exports `./shared`, `./client` and `./server`. There is deliberately no `.` export:
37
- a root barrel re-exporting both halves would let a browser bundle reach server code — and server
38
- credentials — through one innocent-looking import, defeating every other guard. Adding one is a
39
- boundary change, not a convenience.
188
+ ---
189
+
190
+ ## The one type to know
191
+
192
+ Every solve, on either side of the wire, returns this:
40
193
 
41
194
  ```ts
42
- import type { SolveResult } from '@selvajs/solve/shared';
195
+ interface SolveResult {
196
+ outputs: Record<string, unknown>; // keyed by output id / nickname
197
+ meshes?: unknown[]; // geometry — opaque here, see below
198
+ errors?: string[];
199
+ warnings?: string[];
200
+ source?: unknown; // the raw compute payload, verbatim
201
+ values?: unknown; // the inputs that produced it
202
+ }
43
203
  ```
44
204
 
205
+ **Why `meshes` is `unknown`:** typing it means importing `three`, and this package is useful
206
+ precisely because it has no renderer. A viewer app narrows it at its own seam:
207
+ `SolveResult<THREE.Object3D>`.
208
+
209
+ **Why `values` rides along:** a cached hit never calls your `SolveFn`, so the driver stamps the
210
+ inputs onto the result. The pair stays atomic — a "save what I see" button can't mismatch geometry
211
+ and inputs.
212
+
213
+ And the one function you supply:
214
+
45
215
  ```ts
46
- import { createSolveSession, createRequestResponseDriver } from '@selvajs/solve/client';
47
- import { runSolvePipeline } from '@selvajs/solve/server';
216
+ type SolveFn = (values: Record<string, unknown>, signal: AbortSignal) => Promise<SolveResult>;
217
+ ```
218
+
219
+ `values` is just `{ width: 10, height: 20 }`, keyed by schema input id. That is the entire state a
220
+ solve needs.
221
+
222
+ ---
223
+
224
+ ## Why no root export
225
+
226
+ `server/` uses `node:zlib`, `node:crypto`, and reads compute-server credentials. A `.` barrel
227
+ joining both halves would let a browser bundle pull all of that in by accident.
228
+
229
+ ```
230
+ shared/ the types both sides speak
231
+ ╱ ╲
232
+ client/ server/ never import each other
48
233
  ```
49
234
 
50
- `@selvajs/server/compute` keeps only the HTTP request policy it owns (limits, rate limiting, the
51
- SSRF guard, remote-definition fetch) and does not re-export any of this — the two packages are
52
- independent.
235
+ Enforced by `no-restricted-imports` and a bundle-boundary test, not by convention.
236
+
237
+ ## Not in this package
238
+
239
+ | Concern | Lives in |
240
+ | ------------------------------------ | ------------------------------ |
241
+ | Svelte / React components | `@selvajs/ui` |
242
+ | `three`, mesh parsing, the viewer | `@selvajs/visualization` |
243
+ | Auth, orgs, share links, rate limits | your route + `@selvajs/server` |
244
+ | HTTP status codes | `engine.toWebResponse`, opt-in |
245
+
246
+ ## Going deeper
247
+
248
+ - [`src/client/README.md`](./src/client/README.md) — writing your own driver, the value map, mesh ownership
249
+ - [`src/server/README.md`](./src/server/README.md) — cache layers, coalescing, `runSolvePipeline`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/solve",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "One owner for the Selva solve flow — input change to solve result, on both sides of the wire, with no transport and no UI",
5
5
  "author": "VektorNode",
6
6
  "license": "MIT",
@@ -62,22 +62,22 @@
62
62
  ],
63
63
  "sideEffects": false,
64
64
  "dependencies": {
65
- "@selvajs/compute": "^4.1.0",
66
- "@selvajs/platform": "^0.20.1",
67
- "@selvajs/schemas": "5.0.1"
65
+ "@selvajs/compute": "^4.1.1",
66
+ "@selvajs/schemas": "5.0.2",
67
+ "@selvajs/platform": "^0.20.1"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@eslint/js": "^10.0.1",
71
- "@types/node": "^26.2.0",
71
+ "@types/node": "^26.4.0",
72
72
  "esbuild": "^0.28.2",
73
- "eslint": "^10.8.1",
73
+ "eslint": "^10.9.1",
74
74
  "eslint-config-prettier": "^10.1.8",
75
75
  "globals": "^17.11.0",
76
76
  "prettier": "^3.9.6",
77
77
  "tsdown": "^0.22.14",
78
78
  "typescript": "~6.0.3",
79
- "vite": "^8.2.1",
80
- "vitest": "^4.1.10",
79
+ "vite": "^8.2.2",
80
+ "vitest": "^4.1.11",
81
81
  "@selvajs/config": "0.0.4"
82
82
  },
83
83
  "engines": {