@selvajs/compute 4.3.0-beta.1 → 5.0.0-beta.10

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
@@ -4,7 +4,6 @@
4
4
  [![npm version](https://img.shields.io/npm/v/@selvajs/compute.svg)](https://www.npmjs.com/package/@selvajs/compute)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/@selvajs/compute.svg)](https://www.npmjs.com/package/@selvajs/compute)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/)
8
7
  [![Node.js](https://img.shields.io/badge/Node.js-24+-green.svg)](https://nodejs.org/)
9
8
  [![GitHub Repository](https://img.shields.io/badge/GitHub-VektorNode/selva-blue?logo=github)](https://github.com/VektorNode/selva)
10
9
 
@@ -15,28 +14,27 @@
15
14
  A TypeScript client for Rhino Compute and Grasshopper definitions: typed definition IO, data trees,
16
15
  and a scheduler that handles cancellation, retries and caching.
17
16
 
18
- Pure solve/data — no rendering layer, no `three` dependency. To turn a solve response into Three.js
17
+ Pure solve/data: no rendering layer, no `three` dependency. To turn a solve response into Three.js
19
18
  objects, use [`@selvajs/visualization`](https://www.npmjs.com/package/@selvajs/visualization).
20
19
 
21
20
  ```bash
22
21
  npm install @selvajs/compute
23
22
  ```
24
23
 
25
- Three entrypoints: `@selvajs/compute/grasshopper` (client, data trees, IO parsing),
26
- `@selvajs/compute/core` (low-level fetch, errors, config). The root is empty on purpose — import a
27
- subpath.
24
+ The package is ESM only, with two entrypoints: `/core` and `/grasshopper`.
28
25
 
29
- > **This is not a job queue.** For solves longer than a couple of minutes, run this library
30
- > server-side behind your own queue (BullMQ / SQS / Cloud Tasks) and expose a status endpoint to
31
- > the browser.
26
+ Two entrypoints: `@selvajs/compute/grasshopper` (client, scheduler, data trees, IO parsing) and
27
+ `@selvajs/compute/core` (low-level fetch, errors, config). There is no root entry: import a
28
+ subpath.
32
29
 
33
30
  ## Quickstart
34
31
 
35
- Every solve in `@selvajs/compute` goes through a **scheduler**. The scheduler
36
- handles cancellation, retries, loading state, and (optionally) a response cache
37
- — things every real app needs and shouldn't have to rebuild.
32
+ `client.solve` sends one request. A scheduler adds cancellation, retries, loading state and an
33
+ optional response cache on top; use one for any UI that fires solves.
38
34
 
39
35
  ```ts
36
+ import { readFile } from 'node:fs/promises';
37
+ import { ErrorCodes } from '@selvajs/compute/core';
40
38
  import {
41
39
  GrasshopperClient,
42
40
  TreeBuilder,
@@ -48,15 +46,15 @@ const client = await GrasshopperClient.create({
48
46
  apiKey: 'your-api-key'
49
47
  });
50
48
 
51
- // Configure the scheduler for your workload (see "Configuring the scheduler" below).
52
49
  const scheduler = client.createScheduler({ mode: 'latest-wins', timeoutMs: 30_000 });
53
50
 
54
- // Inspect the definition's inputs once, build a data tree.
55
- const io = await client.getIO('my-definition.gh');
51
+ // A URL, or the .gh bytes: a bare file name would be sent as the definition text.
52
+ const definition = new Uint8Array(await readFile('my-definition.gh'));
53
+
54
+ const io = await client.getIO(definition);
56
55
  const inputTree = TreeBuilder.fromInputParams(io.inputs);
57
56
 
58
- // Solve. Returns a Promise — call it as often as you like.
59
- const result = await scheduler.solve('my-definition.gh', inputTree);
57
+ const result = await scheduler.solve(definition, inputTree);
60
58
  const { values } = new GrasshopperResponseProcessor(result).getValues();
61
59
  ```
62
60
 
@@ -69,128 +67,156 @@ scheduler.subscribe(() => {
69
67
  });
70
68
  ```
71
69
 
72
- And handle expected cancellations gracefully — when newer values supersede an
73
- in-flight solve, or when the user aborts:
70
+ New here? [How the client works](https://selva.dev/docs/packages/build/how-it-works) explains the pieces, and [Common tasks](https://selva.dev/docs/packages/build/common-tasks) has copy-ready examples.
74
71
 
75
- ```ts
76
- scheduler.solve(definition, inputTree).catch((err) => {
77
- if (/superseded|aborted/i.test(err.message)) return; // expected, not an error
78
- showError(err);
79
- });
80
- ```
72
+ > `client.solve` throws on solver errors; `scheduler.solve` resolves with them. See [Errors](#errors).
81
73
 
82
74
  ## Configuring the scheduler
83
75
 
84
- The scheduler is one API with two knobs that matter — `mode` and `timeoutMs` —
85
- plus a couple of optional ones. Pick the row that matches what the user is
86
- doing in your UI:
76
+ Pick the row that matches what the user is doing:
87
77
 
88
- | Workload | `mode` | `timeoutMs` | `retry` | Notes |
89
- | --------------------------------- | --------------- | ---------------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
90
- | **Slider scrubs / live previews** | `'latest-wins'` | `30_000` | default | Aborts in-flight solves when newer values arrive. Add `cache: { ttlMs: 60_000 }` for instant repeats. |
91
- | **Submit / long-running jobs** | `'queue'` | `0` (no timeout) | `{ attempts: 1 }` | Serial queue. Pass a caller `signal` so users can hit Cancel. Bump proxy idle timeouts (see below). |
92
- | **Background / batch parallel** | `'parallel'` | `60_000` | `{ attempts: 2 }` | Fires solves concurrently up to `maxConcurrent` (default 4). |
78
+ | Workload | `mode` | `timeoutMs` | `retry` | Notes |
79
+ | --------------------------------- | --------------- | ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
80
+ | **Slider scrubs / live previews** | `'latest-wins'` | `30_000` | default | Aborts in-flight solves when newer values arrive. Add `cache: { maxBytes: 64 * 1024 * 1024 }` for instant repeats. |
81
+ | **Submit / long-running jobs** | `'queue'` | `0` (no timeout) | default | Serial queue. Pass a caller `signal` so users can hit Cancel. See [Long jobs behind a proxy](#long-jobs-behind-a-proxy). |
82
+ | **Background / batch parallel** | `'queue'` | `60_000` | `{ attempts: 2 }` | Set `maxConcurrent` (e.g. 4) to run that many solves at once; queue mode defaults to 1. |
93
83
 
94
- You can create multiple schedulers from one client — typically one per UI
95
- surface. They share the connection pool but their queues, cancel scopes, and
96
- caches are independent:
84
+ Create one scheduler per UI surface: their queues, cancel scopes and caches are independent.
97
85
 
98
- ```ts
99
- const previewScheduler = client.createScheduler({ mode: 'latest-wins', timeoutMs: 30_000 });
100
- const submitScheduler = client.createScheduler({
101
- mode: 'queue',
102
- timeoutMs: 0,
103
- retry: { attempts: 1 }
104
- });
105
- ```
86
+ `config.retry` covers solves. The liveness probe `GrasshopperClient.create` runs has its own
87
+ ladder: pass `{ probe: { attempts, baseDelayMs, maxDelayMs } }` as its second argument.
88
+
89
+ `scheduler.solveDetailed()` resolves `{ response, fromCache, definitionReuploaded, durationMs }`
90
+ for that one call, and takes a per-call `onServerTiming`. Use it to record how each solve went;
91
+ `onSettle` fires for every solve on the scheduler.
92
+
93
+ Don't mutate a `Uint8Array` definition after solving it. The scheduler hashes a buffer once and
94
+ remembers the hash by identity, so a changed buffer is served the old content's cached responses.
106
95
 
107
96
  ### Cancellation
108
97
 
109
- Pass a per-call `signal` to cancel just that solve, or call `cancelAll()` to
110
- cancel everything (e.g. on route change or component unmount):
98
+ A superseded or cancelled solve rejects. Filter those by code:
111
99
 
112
100
  ```ts
113
101
  const ctrl = new AbortController();
114
- scheduler.solve(definition, tree, { signal: ctrl.signal });
102
+ scheduler.solve(definition, inputTree, { signal: ctrl.signal }).catch((err) => {
103
+ if (err.code === ErrorCodes.SUPERSEDED || err.code === ErrorCodes.ABORTED) return;
104
+ showError(err);
105
+ });
115
106
 
116
- // Later:
117
- ctrl.abort(); // cancel just this call
118
- scheduler.cancelAll(); // cancel everything in flight + pending
119
- scheduler.dispose(); // cancel everything and tear down the scheduler
107
+ ctrl.abort(); // cancel this call
108
+ scheduler.cancelAll(); // cancel everything in flight and pending, e.g. on unmount
109
+ scheduler.dispose(); // cancel everything and tear the scheduler down
120
110
  ```
121
111
 
122
112
  ### Long jobs behind a proxy
123
113
 
124
- Cloudflare's default idle timeout is 100s; AWS ALB's is 60s; nginx is 60s.
125
- If your Compute server is behind any of them, those values must be bumped
126
- before you can run long solves through the browser — the library cannot work
127
- around proxy timeouts.
128
-
129
- For solves longer than ~2 minutes, the safer architecture is to run this
130
- library **server-side** behind your own job queue (BullMQ / SQS / Cloud Tasks)
114
+ A proxy in front of Compute closes idle connections after its own timeout, and the library can't
115
+ work around that: raise the proxy's idle timeout before running long solves from a browser. For
116
+ solves longer than a couple of minutes, run this library server-side behind your own job queue
131
117
  and expose a status endpoint to the browser.
132
118
 
133
- ## Requirements
119
+ On Node, undici stops waiting for response headers at 300 s, and neither `timeoutMs` nor `0` lifts
120
+ that: the solve fails with `NETWORK_ERROR` while it is still running on the server, and a retry
121
+ would run it a second time. Past 300 s, keep `retry` off and pass a `fetch` with a longer limit:
134
122
 
135
- **Node.js >= 24.**
123
+ ```ts
124
+ import { Agent, fetch as undiciFetch } from 'undici';
136
125
 
137
- The [official McNeel Rhino Compute](https://github.com/mcneel/compute.rhino3d) handles plain
138
- Grasshopper solving. Some features need more:
126
+ const dispatcher = new Agent({ headersTimeout: 0, bodyTimeout: 0 });
127
+ const client = await GrasshopperClient.create({
128
+ serverUrl,
129
+ fetch: (url, init) => undiciFetch(url, { ...init, dispatcher })
130
+ });
131
+ ```
139
132
 
140
- - The [VektorNode compute fork](https://github.com/VektorNode/compute.rhino3d) adds input grouping
141
- (`groupName`), persistent input ids keyed on Grasshopper object GUIDs, file export, and block
142
- instance support.
143
- - The [Selva Rhino plugin](https://www.food4rhino.com/en/app/selva?lang=en) supplies the Display
144
- and file-export components those features read.
133
+ ## Errors
145
134
 
146
- Anything requiring either is marked where it's documented.
135
+ Every failure is a `ComputeError`. Branch on `err.code`, one of `ErrorCodes`, not on the message.
147
136
 
148
- ## Troubleshooting
137
+ `client.solve` and `scheduler.solve` differ on a response that carries Grasshopper `errors`.
138
+ `client.solve` throws `COMPUTATION_ERROR`, with the outputs that did compute in
139
+ `err.context.values`. `scheduler.solve` resolves with the response, so check it yourself:
149
140
 
150
- ### `Network error: Failed to fetch`
141
+ ```ts
142
+ import { readField } from '@selvajs/compute/core';
151
143
 
152
- The browser couldn't reach the server. Check, in order:
144
+ const response = await scheduler.solve(definition, inputTree);
145
+ // Read case-insensitively: stock Rhino Compute sends `Errors`.
146
+ const solveErrors = readField<string[]>(response, 'errors');
147
+ if (solveErrors?.length) showError(solveErrors.join('\n'));
148
+ ```
153
149
 
154
- 1. **Server is running** — `curl http://localhost:6500/healthcheck` should return
155
- a 200.
156
- 2. **CORS** — if your Compute server is on a different origin than your app,
157
- the server must send `Access-Control-Allow-Origin`. Standard Rhino Compute
158
- does **not** ship with CORS enabled; you'll need to put it behind a proxy
159
- that adds the headers, or use the [VektorNode custom branch](https://github.com/VektorNode/compute.rhino3d).
160
- 3. **Mixed content** — an HTTPS app can't fetch from an HTTP server. Either
161
- serve Compute over HTTPS or develop locally on HTTP.
162
- 4. **API key** — you'll see the same error if your `apiKey` is missing for a
163
- server that requires one (the server typically returns 401 with no CORS
164
- headers, which the browser surfaces as a network error).
150
+ ## Data trees
165
151
 
166
- ### Solves timing out before the server finishes (502 / 504 / aborted)
152
+ `TreeBuilder.fromInputParams` builds one tree per input from its default, keyed by the input's
153
+ **nickname**. `TreeBuilder.fromValues` builds them from your own values instead: it coerces
154
+ Number, Integer and Boolean items, clamps numbers to `minimum`/`maximum`, and drops what it can't
155
+ coerce, so Grasshopper keeps its own value for that input. `replaceTreeValue` is copy-on-write: it
156
+ returns a new array and leaves yours untouched, so assign the result or the solve sends the
157
+ defaults.
167
158
 
168
- A proxy in front of Compute, not the library — see [Long jobs behind a
169
- proxy](#long-jobs-behind-a-proxy).
159
+ ```ts
160
+ let inputTree = TreeBuilder.fromInputParams(io.inputs);
161
+ inputTree = TreeBuilder.replaceTreeValue(inputTree, 'Radius', 10);
162
+ inputTree = TreeBuilder.replaceTreeValue(inputTree, 'Counts', { '{0}': [1, 2], '{0;1}': [3] });
163
+ ```
170
164
 
171
- ### `Definition URL/content is required`
165
+ Read outputs through `GrasshopperResponseProcessor`, or a whole tree with
166
+ `TreeBuilder.getTree(response.values, 'Points')`: a `Map` from branch path (`'{0;1}'`) to items.
167
+ `getTreeValue` is the shortcut for a one-branch param: it unwraps a single item and throws when
168
+ the param has more than one branch.
172
169
 
173
- You called `client.solve('', tree)` or passed a `Uint8Array` of length 0.
174
- Validate your input before calling.
170
+ Pass a rhino3dm module as `{ rhino }` in the `getValues` / `getValue` options to decode geometry.
171
+ Its objects live on the WASM heap, which garbage collection never frees: release them with
172
+ `disposeRhinoObjects`. Install `rhino3dm` yourself; this package doesn't depend on it.
175
173
 
176
- ### 401 vs 403
174
+ ## File outputs
177
175
 
178
- - **401 Unauthorized** — `apiKey` (`RhinoComputeKey` header) is missing or
179
- invalid. Standard Rhino Compute uses this scheme.
180
- - **403 Forbidden** — your `authToken` (Bearer) was rejected by an upstream
181
- proxy/API gateway. The Compute server itself almost never returns 403.
176
+ File outputs need the Selva plugin and the VektorNode fork (see [Requirements](#requirements));
177
+ standard Rhino Compute drops them. Read one with `getValue` and it comes back as `FileData`.
182
178
 
183
- The error message includes the response body excerpt so you usually get a hint
184
- from the server itself.
179
+ Each file component has a `Sub Folder` input, where `/` or `::` nests (as in Rhino layer names). The
180
+ first segment is the root: `downloadFileDataByRoot` from `/core` turns each root into its own
181
+ archive and puts files without one in the fallback archive. With `'export'` as the fallback name:
185
182
 
186
- ### "Superseded by newer solve" errors flooding my console
183
+ | `Sub Folder` | Archive | Path in the archive |
184
+ | --------------------- | ------------ | ------------------------ |
185
+ | _(empty)_ | `export.zip` | `model.3dm` |
186
+ | `Panels` | `Panels.zip` | `model.3dm` |
187
+ | `ROOT::Panels` | `ROOT.zip` | `Panels/model.3dm` |
188
+ | `ROOT::First::Second` | `ROOT.zip` | `First/Second/model.3dm` |
187
189
 
188
- The scheduler doing its job in `latest-wins` mode — every aborted slider solve rejects with this
189
- message. Filter it as shown in the [Quickstart](#quickstart).
190
+ `groupFilesByRoot` returns the same grouping without the DOM, for writing files in Node.
191
+ `processor.getAndDownloadFiles(name)` skips the split: one `name.zip`, each file at its full
192
+ `Sub Folder` path.
190
193
 
191
- ## Acknowledgement
194
+ ## Requirements
195
+
196
+ The [official McNeel Rhino Compute](https://github.com/mcneel/compute.rhino3d) handles plain
197
+ Grasshopper solving. The [VektorNode compute fork](https://github.com/VektorNode/compute.rhino3d)
198
+ adds input grouping (`groupName`), persistent input ids keyed on Grasshopper object GUIDs, file
199
+ export, and block instance support. The
200
+ [Selva Rhino plugin](https://www.food4rhino.com/en/app/selva?lang=en) supplies the Display and
201
+ file-export components those features read.
202
+
203
+ ## Troubleshooting
204
+
205
+ **`CORS_ERROR` (`Request failed: Failed to fetch`)** means the browser got no response, which it
206
+ reports the same way for a CORS block and for an unreachable server. Outside a browser the same
207
+ failure is `NETWORK_ERROR`. Check that
208
+ `curl http://localhost:6500/` returns 200; that a server on another origin sends
209
+ `Access-Control-Allow-Origin` (standard Rhino Compute doesn't: put a proxy in front that adds it,
210
+ or use the [VektorNode fork](https://github.com/VektorNode/compute.rhino3d)); that an HTTPS app
211
+ isn't calling an HTTP server; and that the API key is set, since a 401 without CORS headers reads
212
+ as a network error too.
192
213
 
193
- Where code is adapted from McNeel's repositories, it is marked in the relevant files.
214
+ **401** means `apiKey` (the `RhinoComputeKey` header) is missing or wrong. **403** usually comes
215
+ from a proxy or API gateway rejecting `authToken`, which is sent verbatim as the `Authorization`
216
+ header, so include the scheme (`Bearer …`). The error message carries an excerpt of the response
217
+ body.
218
+
219
+ ## Acknowledgement
194
220
 
195
221
  **Key references:**
196
222
 
package/dist/core.d.ts CHANGED
@@ -1,2 +1,190 @@
1
- import { A as RetryPolicy, C as getLogger, D as fetchCompute, E as setResponseWireSize, F as ErrorCodes, M as ServerTiming, N as ComputeError, O as ComputeAuth, P as ErrorCode, S as enableDebugLogging, T as getResponseWireSize, _ as isDefinitionRef, a as extractFilesFromComputeResponse, b as readField, c as ProcessedFile, d as classifyProbeFailure, f as DEFAULT_BLOCKED_HOST, g as SolveDefinition, h as DefinitionRef, i as downloadFileDataByRoot, j as ServerErrorCodeMap, k as ComputeConfig, l as ProbeFailure, m as validateServerUrl, n as subFolderSegments, o as FileBaseInfo, p as ValidateServerUrlOptions, r as downloadFileData, s as FileData, t as groupFilesByRoot, u as ProbeVerdict, v as decodeBase64ToBinary, w as setLogger, x as Logger, y as hasField } from "./index-BR_XZvjj.js";
2
- export { type ComputeAuth, type ComputeConfig, ComputeError, DEFAULT_BLOCKED_HOST, type DefinitionRef, type ErrorCode, ErrorCodes, type FileBaseInfo, type FileData, type Logger, type ProbeFailure, type ProbeVerdict, type ProcessedFile, type RetryPolicy, type ServerErrorCodeMap, type ServerTiming, type SolveDefinition, type ValidateServerUrlOptions, classifyProbeFailure, decodeBase64ToBinary, downloadFileData, downloadFileDataByRoot, enableDebugLogging, extractFilesFromComputeResponse, fetchCompute, getLogger, getResponseWireSize, groupFilesByRoot, hasField, isDefinitionRef, readField, setLogger, setResponseWireSize, subFolderSegments, validateServerUrl };
1
+ import { a as DefinitionRef, c as ComputeAuth, d as ServerErrorCodeMap, f as ServerTiming, h as ErrorCodes, i as ProcessedFile, l as ComputeConfig, m as ErrorCode, n as FileBaseInfo, o as SolveDefinition, p as ComputeError, r as FileData, s as isDefinitionRef, t as DownloadOptions, u as RetryPolicy } from "./types-Dm9QFVvA.js";
2
+ /**
3
+ * POST `args` as JSON to `<serverUrl>/<endpoint>` with retry, timeout and abort handling, and
4
+ * return the parsed response. Most callers want a higher-level API.
5
+ *
6
+ * `R` names the expected response shape; nothing checks it, so the default `unknown` forces a
7
+ * narrowing.
8
+ *
9
+ * @example
10
+ * const response = await fetchCompute('grasshopper', args, {
11
+ * serverUrl: 'https://my-server.com',
12
+ * timeoutMs: 30_000,
13
+ * retry: { attempts: 2 },
14
+ * signal: controller.signal
15
+ * });
16
+ */
17
+ export declare function fetchCompute<R = unknown>(endpoint: string, args: Record<string, any>, config: ComputeConfig): Promise<R>;
18
+ interface Logger {
19
+ debug(message: string, ...args: unknown[]): void;
20
+ info(message: string, ...args: unknown[]): void;
21
+ warn(message: string, ...args: unknown[]): void;
22
+ error(message: string, ...args: unknown[]): void;
23
+ }
24
+ export declare function getLogger(): Logger;
25
+ /**
26
+ * Route the package's logs to `logger`; `null` silences them (the default).
27
+ *
28
+ * @throws {ComputeError} `INVALID_CONFIG` if the logger lacks one of the four methods, rather
29
+ * than a "debug is not a function" later at an unrelated call site.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { setLogger } from '@selvajs/compute/core';
34
+ *
35
+ * setLogger(console);
36
+ * setLogger({
37
+ * debug: (msg, ...args) => myLogger.debug(msg, ...args),
38
+ * info: (msg, ...args) => myLogger.info(msg, ...args),
39
+ * warn: (msg, ...args) => myLogger.warn(msg, ...args),
40
+ * error: (msg, ...args) => myLogger.error(msg, ...args)
41
+ * });
42
+ * setLogger(null);
43
+ * ```
44
+ */
45
+ export declare function setLogger(logger: Logger | Console | null): void;
46
+ /** Shorthand for `setLogger(console)`. */
47
+ export declare function enableDebugLogging(): void;
48
+ /**
49
+ * Case-insensitive field reads for wire payloads.
50
+ *
51
+ * Rhino Compute branches serialize the same field in different casings. mcneel 8.x/9.x send the IO
52
+ * schema PascalCase (`ParamType`, `Default`, `InnerTree`): those C# classes carry no
53
+ * `[JsonProperty]`. The VektorNode fork sends camelCase, except inside the `default` DataTree
54
+ * (`ParamName`, `InnerTree`), an external type it can't attribute. So one response can mix casings.
55
+ * Read the fields you need through here and leave the rest verbatim: rewriting every key corrupts
56
+ * user-authored ones such as value-list labels.
57
+ */
58
+ /**
59
+ * Read `name` in any casing, preferring an exact-case match. `undefined` when no own key matches or
60
+ * `obj` isn't an object.
61
+ */
62
+ export declare function readField<T = unknown>(obj: unknown, name: string): T | undefined;
63
+ /** Whether `name` is present in any casing, even with a null or undefined value. */
64
+ export declare function hasField(obj: unknown, name: string): boolean;
65
+ /**
66
+ * Decode base64 to bytes, normalizing and validating the same way in Node and browsers.
67
+ * `packages/visualization/src/shared/encoding.ts` keeps a copy that throws its own error class;
68
+ * change both together.
69
+ *
70
+ * @throws {ComputeError} `ENCODING_ERROR` if invalid, or `ENVIRONMENT_ERROR` if the runtime has no decoder
71
+ */
72
+ export declare function decodeBase64ToBinary(base64File: string): Uint8Array;
73
+ /**
74
+ * The public McNeel endpoint, blocked by default: users must point at their own server. Its raw IP
75
+ * isn't blocked: it sits behind a load balancer with no stable address to pin.
76
+ */
77
+ export declare const DEFAULT_BLOCKED_HOST = "compute.rhino3d.com";
78
+ interface ValidateServerUrlOptions {
79
+ /**
80
+ * Hostnames rejected as a `serverUrl` — a backend's shared public endpoint,
81
+ * which callers must not point at. Defaults to `[DEFAULT_BLOCKED_HOST]`; pass
82
+ * `[]` to block nothing. Compared lowercased with any trailing dot stripped.
83
+ */
84
+ blockedHosts?: readonly string[];
85
+ }
86
+ /**
87
+ * Validate a compute `serverUrl` and return it trimmed, without trailing slashes.
88
+ *
89
+ * @throws {ComputeError} `INVALID_CONFIG` when the URL is empty, isn't http(s), doesn't parse,
90
+ * embeds credentials, has a query or fragment, or names a blocked host.
91
+ */
92
+ export declare function validateServerUrl(raw: string, options?: ValidateServerUrlOptions): string;
93
+ /**
94
+ * Why a liveness probe failed, and whether waiting can change the answer.
95
+ *
96
+ * `retryable` is the whole point: a powered-off VM and a booting one both read
97
+ * as "offline" from a single probe, but only one of them will ever come up. A
98
+ * caller that can't tell them apart has to assume the optimistic case and burn
99
+ * its full retry window on a machine that is simply off.
100
+ */
101
+ type ProbeVerdict =
102
+ /** Nothing is listening on the port — the host answered, and said no. */
103
+ 'refused' |
104
+ /** The hostname does not resolve. */
105
+ 'dns' |
106
+ /** No answer within the timeout: a booting host swallows packets rather than refusing them. */
107
+ 'timeout' |
108
+ /** The server answered, but rejected the probe's credentials (401/403). */
109
+ 'unauthorized' |
110
+ /** The server answered with a non-2xx that isn't an auth rejection. */
111
+ 'http_error' |
112
+ /** Connection failed in a way we can't attribute. */
113
+ 'unknown';
114
+ interface ProbeFailure {
115
+ verdict: ProbeVerdict;
116
+ /** Whether retrying the same probe could plausibly succeed later. */
117
+ retryable: boolean;
118
+ /** Operator-facing sentence. Never includes the API key. */
119
+ summary: string;
120
+ }
121
+ /**
122
+ * Classify a `ComputeServerStats.probeServer` result.
123
+ *
124
+ * A DNS failure is treated as retryable even though a missing record won't fix
125
+ * itself: `EAI_AGAIN` is a *timeout* talking to the resolver, and the two are
126
+ * not distinguishable from the error alone. Retrying a genuinely-wrong hostname
127
+ * costs one window; giving up on a resolver hiccup breaks a server that is fine.
128
+ *
129
+ * @param probe - The `{ online, status?, error? }` a probe returned. An `online`
130
+ * probe is not a failure and yields `null`.
131
+ */
132
+ export declare function classifyProbeFailure(probe: {
133
+ online: boolean;
134
+ status?: number;
135
+ error?: string;
136
+ }): ProbeFailure | null;
137
+ /**
138
+ * Decode the files in a compute response, plus any external `additionalFiles`, without
139
+ * downloading them. Never throws: an unnamed or undecodable item and a failed external fetch are
140
+ * logged and dropped, the rest still resolve. Colliding archive paths are renamed
141
+ * (`model.txt` → `model-2.txt`), so a consumer keying by `path` loses nothing.
142
+ */
143
+ export declare const extractFilesFromComputeResponse: (downloadableFiles: FileData[], additionalFiles?: FileBaseInfo[] | FileBaseInfo | null) => Promise<ProcessedFile[]>;
144
+ /** Downloads files from a compute response as a ZIP archive. */
145
+ export declare const downloadFileData: (downloadableFiles: FileData[], fileFoldername: string, additionalFiles?: FileBaseInfo[] | FileBaseInfo | null, options?: DownloadOptions) => Promise<void>;
146
+ /**
147
+ * Download one archive per `Sub Folder` root (see {@link groupFilesByRoot}). Files with no root go
148
+ * into `fallbackName`, so a definition that never sets `Sub Folder` downloads as one archive.
149
+ *
150
+ * Archives are saved one at a time: browsers discard concurrent downloads issued in the same tick.
151
+ * More than one save per gesture may prompt the user for permission.
152
+ *
153
+ * @param downloadableFiles - `FileData` items from the compute response.
154
+ * @param fallbackName - Archive name for files with no `Sub Folder` root.
155
+ * @param additionalFiles - Extra files to package; they carry no root and join the fallback archive.
156
+ * @param options - `saveBlob` replaces the anchor download for every archive.
157
+ */
158
+ export declare const downloadFileDataByRoot: (downloadableFiles: FileData[], fallbackName: string, additionalFiles?: FileBaseInfo[] | FileBaseInfo | null, options?: DownloadOptions) => Promise<void>;
159
+ /**
160
+ * The `Sub Folder` convention, as authored on Selva's Grasshopper file components.
161
+ *
162
+ * `::` nests, matching Rhino's layer separator (`ROOT::Panels`), and `/` and `\` are accepted
163
+ * because people type them out of habit. The plugin normalizes to `/` before sending, so these
164
+ * helpers also cover payloads from an older plugin that still emit `::`.
165
+ *
166
+ * The first segment is the **root**, which names the archive rather than becoming a folder inside
167
+ * it: `ROOT::Panels` downloads as `ROOT.zip` containing `Panels/…`. Files sharing a root travel
168
+ * together; distinct roots produce separate archives.
169
+ */
170
+ /**
171
+ * Split a `Sub Folder` value into folder segments; `/`, `\` and `::` all separate.
172
+ *
173
+ * Exported because anything rendering a folder tree has to agree with the archive on where the
174
+ * boundaries are — splitting on `/` alone leaves `Main::Panels` as one literal segment and shows
175
+ * a folder named after the separator.
176
+ */
177
+ export declare const subFolderSegments: (subFolder: string | undefined) => string[];
178
+ /**
179
+ * Group files by their `Sub Folder` root, preserving encounter order.
180
+ *
181
+ * Rootless files group under `''` — a caller naming archives supplies its own fallback for that
182
+ * bucket. Useful beyond downloading: a consumer writing to disk gets the same grouping without
183
+ * touching the DOM.
184
+ */
185
+ export declare const groupFilesByRoot: <T extends Pick<FileData, "subFolder">>(files: readonly T[]) => Array<{
186
+ root: string;
187
+ files: T[];
188
+ }>;
189
+ export { type ComputeAuth, type ComputeConfig, ComputeError, type DefinitionRef, type DownloadOptions, type ErrorCode, ErrorCodes, type FileBaseInfo, type FileData, type Logger, type ProbeFailure, type ProbeVerdict, type ProcessedFile, type RetryPolicy, type ServerErrorCodeMap, type ServerTiming, type SolveDefinition, type ValidateServerUrlOptions, isDefinitionRef };
190
+ //# sourceMappingURL=core.d.ts.map
package/dist/core.js CHANGED
@@ -1 +1 @@
1
- import{C as e,S as t,_ as n,a as r,b as i,c as a,d as o,g as s,i as c,l,m as u,n as d,o as f,r as p,s as m,t as h,v as g,w as _,x as v,y}from"./core-rp3fyoca.js";export{e as ComputeError,s as DEFAULT_BLOCKED_HOST,_ as ErrorCodes,u as classifyProbeFailure,o as decodeBase64ToBinary,h as downloadFileData,d as downloadFileDataByRoot,i as enableDebugLogging,p as extractFilesFromComputeResponse,m as fetchCompute,v as getLogger,a as getResponseWireSize,c as groupFilesByRoot,g as hasField,f as isDefinitionRef,y as readField,t as setLogger,l as setResponseWireSize,r as subFolderSegments,n as validateServerUrl};
1
+ export { P as ComputeError, T as DEFAULT_BLOCKED_HOST, F as ErrorCodes, d as classifyProbeFailure, y as decodeBase64ToBinary, t as downloadFileData, n as downloadFileDataByRoot, j as enableDebugLogging, r as extractFilesFromComputeResponse, l as fetchCompute, M as getLogger, a as groupFilesByRoot, k as hasField, s as isDefinitionRef, A as readField, N as setLogger, o as subFolderSegments, E as validateServerUrl } from "./handle-files-D-4X4Uh_.js";