@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.
- package/README.md +230 -33
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,52 +1,249 @@
|
|
|
1
1
|
# `@selvajs/solve`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
173
|
+
### What the engine does on your behalf
|
|
23
174
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
186
|
+
Each layer is covered in [`src/server/README.md`](./src/server/README.md).
|
|
35
187
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
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.
|
|
66
|
-
"@selvajs/
|
|
67
|
-
"@selvajs/
|
|
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.
|
|
71
|
+
"@types/node": "^26.4.0",
|
|
72
72
|
"esbuild": "^0.28.2",
|
|
73
|
-
"eslint": "^10.
|
|
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.
|
|
80
|
-
"vitest": "^4.1.
|
|
79
|
+
"vite": "^8.2.2",
|
|
80
|
+
"vitest": "^4.1.11",
|
|
81
81
|
"@selvajs/config": "0.0.4"
|
|
82
82
|
},
|
|
83
83
|
"engines": {
|