@selvajs/solve 1.1.1 → 1.2.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcome-H5XjAroQ.js","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":"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"}
@@ -0,0 +1,96 @@
1
+ import { SolveDiagnostic, SolveOutcome } from "@selvajs/schemas";
2
+ //#region src/shared/solve-fn.d.ts
3
+ /**
4
+ * The solve contract — what a solve returns and what a caller supplies to run one.
5
+ *
6
+ * Lives in `shared/` because both halves of the package speak it: `client/` calls a `SolveFn` and
7
+ * stores its `SolveResult`; `server/` produces the payload one is built from.
8
+ */
9
+ /**
10
+ * `TMesh` is deliberately opaque and defaults to `unknown`: this package never inspects meshes, and
11
+ * typing them would drag a renderer dependency into a package whose whole point is not having one.
12
+ * The app that owns assembly (parse → `THREE.Object3D[]`) is the only place that knows the concrete
13
+ * type, and it narrows by writing `SolveResult<THREE.Object3D>` at its own seam.
14
+ */
15
+ /**
16
+ * Marker the Grasshopper Message component prepends to a blocking error.
17
+ *
18
+ * Rhino.Compute flattens runtime messages to plain strings with no component attribution, so
19
+ * this is the only way that path can tell a deliberate block from any other solve error. The
20
+ * local WebSocket path carries `isGate` structurally and does not need it — but the component
21
+ * writes the marker unconditionally, because the same definition runs on both.
22
+ *
23
+ * Changing this string breaks blocking on deployed Selva for every already-published
24
+ * definition, since the text is baked into the .gh file's solve output, not the plugin.
25
+ */
26
+ declare const SOLVE_BLOCKED_MARKER = "[Selva:blocked]";
27
+ interface SolveResult<TMesh = unknown, TSource = unknown> {
28
+ outputs: Record<string, unknown>;
29
+ meshes?: TMesh[];
30
+ errors?: string[];
31
+ warnings?: string[];
32
+ /** Structured form of `errors`/`warnings`, including remarks. Absent from transports that report neither. */
33
+ diagnostics?: SolveDiagnostic[];
34
+ /**
35
+ * The definition refused this solve — a guard in the definition raised an error and its
36
+ * outputs were withheld deliberately. Distinct from an errored solve that still returned
37
+ * values, and from a transport failure: the solve ran and its answer is "no". Consumers
38
+ * should show `errors` instead of treating the empty outputs as a result.
39
+ */
40
+ blocked?: boolean;
41
+ /**
42
+ * The solve was stopped before it finished. Always also `blocked`: what computed before
43
+ * the stop is not a result. Absent from a transport that cannot tell.
44
+ */
45
+ aborted?: boolean;
46
+ /**
47
+ * The unparsed payload this result was built from, passed through verbatim. Opaque here for the
48
+ * same reason as `TMesh`: a consumer that must persist or re-submit exactly what it showed the
49
+ * user narrows it at its own seam. Travels with the result through the driver's memo, so a
50
+ * cached hit carries the source that produced it. Unlike `meshes` it needs no ownership policy —
51
+ * it is inert data, not a GPU-backed handle.
52
+ *
53
+ * Supplied by the `SolveFn`, so it is present only when the one in use sets it
54
+ * (`createComputeFetchSolveFn` does). A transport with no raw payload to hand back leaves it
55
+ * absent — see `values` on telling absent from unsupported.
56
+ */
57
+ source?: TSource;
58
+ /**
59
+ * The input set that produced this result, stamped by the driver rather than the `SolveFn` — a
60
+ * memo hit never calls the `SolveFn`, so a consumer reading values captured inside it would pair
61
+ * the on-screen result with whatever solved last. Keeping the pair atomic is the point: a commit
62
+ * path holding a `SolveResult` cannot mismatch artifact and inputs.
63
+ *
64
+ * Present only from a driver that owns a request/response pair (`createRequestResponseDriver`).
65
+ * A push driver cannot attribute an incoming frame to a request and must leave this absent
66
+ * rather than guess — so `undefined` means "this transport does not supply it", not "no solve
67
+ * yet", and a consumer that needs the pair requires a request/response driver. See `SolveDriver`.
68
+ */
69
+ values?: Record<string, unknown>;
70
+ }
71
+ /** Implementations should listen to `signal` and abort/clean up when it fires. */
72
+ type SolveFn<TMesh = unknown, TSource = unknown> = (values: Record<string, unknown>, signal: AbortSignal) => Promise<SolveResult<TMesh, TSource>>;
73
+ //#endregion
74
+ //#region src/shared/outcome.d.ts
75
+ /**
76
+ * Normalizes a wire outcome: unknown levels read as remarks, text is trimmed, empty and exactly
77
+ * repeated messages are dropped, and `source`/`isGate` are omitted rather than null or false, so
78
+ * the same verdict decodes to the same object from either transport. Null when `wire` is not an
79
+ * outcome at all; the caller then falls back.
80
+ */
81
+ declare function decodeOutcome(wire: unknown): SolveOutcome | null;
82
+ /** The legacy verdict, read from Compute's flattened `errors`/`warnings` and their markers. */
83
+ declare function outcomeFromComputeMessages(errors: readonly string[], warnings: readonly string[]): SolveOutcome;
84
+ /**
85
+ * Builds the result a driver reports. A blocked or aborted outcome is "no result" whatever the
86
+ * transport sent along: no outputs, and an empty mesh list so the viewer clears. `errors` and
87
+ * `warnings` are derived from the diagnostics for hosts that only want text.
88
+ */
89
+ declare function finalizeResult<TMesh, TSource>(partial: {
90
+ outputs: Record<string, unknown>;
91
+ meshes?: TMesh[];
92
+ source?: TSource;
93
+ }, outcome: SolveOutcome): SolveResult<TMesh, TSource>;
94
+ //#endregion
95
+ export { SOLVE_BLOCKED_MARKER as a, SolveResult as c, outcomeFromComputeMessages as i, decodeOutcome as n, SolveDiagnostic as o, finalizeResult as r, SolveFn as s, SolveOutcome as t };
96
+ //# sourceMappingURL=outcome-ePNabW7y.d.cts.map
@@ -0,0 +1,96 @@
1
+ import { SolveDiagnostic, SolveOutcome } from "@selvajs/schemas";
2
+ //#region src/shared/solve-fn.d.ts
3
+ /**
4
+ * The solve contract — what a solve returns and what a caller supplies to run one.
5
+ *
6
+ * Lives in `shared/` because both halves of the package speak it: `client/` calls a `SolveFn` and
7
+ * stores its `SolveResult`; `server/` produces the payload one is built from.
8
+ */
9
+ /**
10
+ * `TMesh` is deliberately opaque and defaults to `unknown`: this package never inspects meshes, and
11
+ * typing them would drag a renderer dependency into a package whose whole point is not having one.
12
+ * The app that owns assembly (parse → `THREE.Object3D[]`) is the only place that knows the concrete
13
+ * type, and it narrows by writing `SolveResult<THREE.Object3D>` at its own seam.
14
+ */
15
+ /**
16
+ * Marker the Grasshopper Message component prepends to a blocking error.
17
+ *
18
+ * Rhino.Compute flattens runtime messages to plain strings with no component attribution, so
19
+ * this is the only way that path can tell a deliberate block from any other solve error. The
20
+ * local WebSocket path carries `isGate` structurally and does not need it — but the component
21
+ * writes the marker unconditionally, because the same definition runs on both.
22
+ *
23
+ * Changing this string breaks blocking on deployed Selva for every already-published
24
+ * definition, since the text is baked into the .gh file's solve output, not the plugin.
25
+ */
26
+ declare const SOLVE_BLOCKED_MARKER = "[Selva:blocked]";
27
+ interface SolveResult<TMesh = unknown, TSource = unknown> {
28
+ outputs: Record<string, unknown>;
29
+ meshes?: TMesh[];
30
+ errors?: string[];
31
+ warnings?: string[];
32
+ /** Structured form of `errors`/`warnings`, including remarks. Absent from transports that report neither. */
33
+ diagnostics?: SolveDiagnostic[];
34
+ /**
35
+ * The definition refused this solve — a guard in the definition raised an error and its
36
+ * outputs were withheld deliberately. Distinct from an errored solve that still returned
37
+ * values, and from a transport failure: the solve ran and its answer is "no". Consumers
38
+ * should show `errors` instead of treating the empty outputs as a result.
39
+ */
40
+ blocked?: boolean;
41
+ /**
42
+ * The solve was stopped before it finished. Always also `blocked`: what computed before
43
+ * the stop is not a result. Absent from a transport that cannot tell.
44
+ */
45
+ aborted?: boolean;
46
+ /**
47
+ * The unparsed payload this result was built from, passed through verbatim. Opaque here for the
48
+ * same reason as `TMesh`: a consumer that must persist or re-submit exactly what it showed the
49
+ * user narrows it at its own seam. Travels with the result through the driver's memo, so a
50
+ * cached hit carries the source that produced it. Unlike `meshes` it needs no ownership policy —
51
+ * it is inert data, not a GPU-backed handle.
52
+ *
53
+ * Supplied by the `SolveFn`, so it is present only when the one in use sets it
54
+ * (`createComputeFetchSolveFn` does). A transport with no raw payload to hand back leaves it
55
+ * absent — see `values` on telling absent from unsupported.
56
+ */
57
+ source?: TSource;
58
+ /**
59
+ * The input set that produced this result, stamped by the driver rather than the `SolveFn` — a
60
+ * memo hit never calls the `SolveFn`, so a consumer reading values captured inside it would pair
61
+ * the on-screen result with whatever solved last. Keeping the pair atomic is the point: a commit
62
+ * path holding a `SolveResult` cannot mismatch artifact and inputs.
63
+ *
64
+ * Present only from a driver that owns a request/response pair (`createRequestResponseDriver`).
65
+ * A push driver cannot attribute an incoming frame to a request and must leave this absent
66
+ * rather than guess — so `undefined` means "this transport does not supply it", not "no solve
67
+ * yet", and a consumer that needs the pair requires a request/response driver. See `SolveDriver`.
68
+ */
69
+ values?: Record<string, unknown>;
70
+ }
71
+ /** Implementations should listen to `signal` and abort/clean up when it fires. */
72
+ type SolveFn<TMesh = unknown, TSource = unknown> = (values: Record<string, unknown>, signal: AbortSignal) => Promise<SolveResult<TMesh, TSource>>;
73
+ //#endregion
74
+ //#region src/shared/outcome.d.ts
75
+ /**
76
+ * Normalizes a wire outcome: unknown levels read as remarks, text is trimmed, empty and exactly
77
+ * repeated messages are dropped, and `source`/`isGate` are omitted rather than null or false, so
78
+ * the same verdict decodes to the same object from either transport. Null when `wire` is not an
79
+ * outcome at all; the caller then falls back.
80
+ */
81
+ declare function decodeOutcome(wire: unknown): SolveOutcome | null;
82
+ /** The legacy verdict, read from Compute's flattened `errors`/`warnings` and their markers. */
83
+ declare function outcomeFromComputeMessages(errors: readonly string[], warnings: readonly string[]): SolveOutcome;
84
+ /**
85
+ * Builds the result a driver reports. A blocked or aborted outcome is "no result" whatever the
86
+ * transport sent along: no outputs, and an empty mesh list so the viewer clears. `errors` and
87
+ * `warnings` are derived from the diagnostics for hosts that only want text.
88
+ */
89
+ declare function finalizeResult<TMesh, TSource>(partial: {
90
+ outputs: Record<string, unknown>;
91
+ meshes?: TMesh[];
92
+ source?: TSource;
93
+ }, outcome: SolveOutcome): SolveResult<TMesh, TSource>;
94
+ //#endregion
95
+ export { SOLVE_BLOCKED_MARKER as a, SolveResult as c, outcomeFromComputeMessages as i, decodeOutcome as n, SolveDiagnostic as o, finalizeResult as r, SolveFn as s, SolveOutcome as t };
96
+ //# sourceMappingURL=outcome-ePNabW7y.d.ts.map
package/dist/server.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@selvajs/compute/grasshopper"),t=require("@selvajs/compute/core"),n=require("node:zlib"),r=require("node:util"),i=require("@selvajs/platform");function a(t,n){let r={id:t.id,name:t.nickname,nickname:t.nickname||null,description:t.description||``,paramType:t.paramType===`dynamicValueList`?`valueList`:t.paramType,treeAccess:t.inputStructure===`tree`,minimum:t.minimum??null,maximum:t.maximum??null,atLeast:1,atMost:1,stepSize:t.stepSize,default:n??t.default},i=(0,e.processInput)(r),a=n??t.default;return i.default==null&&a!=null&&(Array.isArray(a)||i.paramType===`ValueList`&&typeof a!=`object`)&&(i.default=a),i}const o=(0,r.promisify)(n.gzip),s=`X-Selva-Compute-Version`;function c(t,n){return e.TreeBuilder.fromInputParams(t.filter(e=>e.paramType).map(e=>a(e,n[e.id])))}async function l(e){let{client:n,signal:r}=e,i=performance.now(),a=e.inputTree??c(e.inputs,e.values),l=performance.now()-i,u=n.solveMeta.seq,f=n.rhinoTiming.seq,p,m=performance.now();try{p=await n.scheduler.solve(e.definitionSource,a,{signal:r})}catch(n){let i=performance.now()-m,a=n instanceof Error&&n.name===`AbortError`||n instanceof t.ComputeError&&n.code===t.ErrorCodes.ABORTED,o=n instanceof t.ComputeError&&n.code===t.ErrorCodes.TIMEOUT_ERROR;if(a||o)return a&&r.aborted?{kind:`client_abort`,durationMs:i}:{kind:`timeout`,durationMs:i,message:`Solve exceeded the ${Math.round(e.solveDeadlineMs/1e3)}s deadline.`};if(n instanceof t.ComputeError){if(n.code===t.ErrorCodes.QUEUE_FULL)return{kind:`shed`,durationMs:i,reason:`queue_full`,retryAfterSeconds:1,message:`Compute server is at capacity. Retry shortly.`};if(n.code===t.ErrorCodes.QUEUE_TIMEOUT)return{kind:`shed`,durationMs:i,reason:`queue_timeout`,retryAfterSeconds:1,message:`Compute request waited too long in the queue. Retry shortly.`}}return{kind:`compute_error`,durationMs:i,error:n}}let h=performance.now()-m,g=performance.now(),_;try{_=JSON.stringify(p)}catch(e){if(e instanceof RangeError)return{kind:`too_large`};throw e}if(_.length>e.responseMaxBytes)return{kind:`too_large`};let v=performance.now()-g,y=/\bgzip\b/i.test(e.acceptEncoding)&&_.length>1024,b=null,x=0;if(y){let e=performance.now();b=await o(Buffer.from(_)),x=performance.now()-e}let S=performance.now()-e.loadStartMs,C=n.solveMeta.seq===u+1?n.solveMeta.last:null,w=C&&!C.fromCache&&n.rhinoTiming.seq===f+1?n.rhinoTiming.last:null,T={"Content-Type":`application/json`,"Server-Timing":d({defLoadMs:e.defLoadMs,treeBuildMs:l,solveMs:h,serializeMs:v,gzipMs:x,serverTotalMs:S,rhino:w,settle:C,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[s]:`1`,Vary:`Accept-Encoding`},E={treeBuildMs:l,solveMs:h,serializeMs:v,gzipMs:x,serverTotalMs:S,serializedBytes:_.length,compressedBytes:b?.byteLength??null},D;return b?(T[`Content-Encoding`]=`gzip`,T[`Content-Length`]=String(b.byteLength),D={body:new Uint8Array(b),encoding:`gzip`,headers:T,result:p,metrics:E}):(T[`Content-Length`]=String(Buffer.byteLength(_)),D={body:_,headers:T,result:p,metrics:E}),{kind:`ok`,envelope:D,solveMs:h,errorCount:p.errors?.length??0,warningCount:p.warnings?.length??0}}function u(e,t){let r=/\bgzip\b/i.test(t);if(e.encoding===`gzip`&&!r){let t=(0,n.gunzipSync)(e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString(`utf8`),r={...e.headers};return delete r[`Content-Encoding`],r[`Content-Length`]=String(Buffer.byteLength(t)),{body:t,headers:r}}return{body:e.body,headers:e.headers}}function d(e){let t=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,n=e.rhino;if(n){let r=n.decode+n.solve+n.encode,i=Math.max(0,e.solveMs-r);t+=`, rhino_decode;dur=${n.decode.toFixed(1)}, rhino_solve;dur=${n.solve.toFixed(1)}, rhino_encode;dur=${n.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[n,r]of e.prepMarks)t+=`, p_${n};dur=${r.toFixed(1)}`;let r=e.settle;r&&(t+=`, selva_cache;dur=${+!!r.fromCache}`,r.definitionReuploaded!==void 0&&(t+=`, def_reupload;dur=${+!!r.definitionReuploaded}`));let i=e.byteRefOutcome;if(i){let e=i.loaded?i.fromCache?`hit`:`miss`:`skipped`;t+=`, def_bytes;desc=${e}`}return t}function f(e){return e.id}const p=5e3;function m(n){let r=n.maxWarmComputeServers??16,i=new Map,a=new Map,o=new Map;n.debug===`verbose`&&(0,t.enableDebugLogging)();let s=e=>{n.debug&&n.onDebugLog?.(e)};async function c(t,r){let i={last:null,seq:0},a={last:null,seq:0},o=n.debug===`verbose`,c={serverUrl:t.serverUrl,apiKey:t.apiKey,debug:o,cachesolve:n.cachesolve,cacheerroredsolves:n.cacheerroredsolves,onServerTiming:e=>{let t=e.decode??0,r=e.solve??0,a=e.encode??0;if(i.seq+=1,i.last={decode:t,solve:r,encode:a},!n.debug)return;let o=t<=1?`HIT (definition reused, no re-upload/parse)`:`miss (full parse)`,c=n.cachesolve&&r<=1?`HIT (cached result, solve skipped)`:n.cachesolve?`miss (solved fresh)`:`off`;s(`[Compute/rhino-cache] decode=${t}ms solve=${r}ms encode=${a}ms | definition-cache: ${o} | solve-cache (cachesolve): ${c}`)}};r&&(c.headers={"X-Selva-Definition":r});let l=await e.GrasshopperClient.create(c),u=1,d=0,f=!1;async function p(e){if(!f){f=!0;try{let t=await Promise.resolve(l.serverStats?.getActiveChildren({initialize:!1})).catch(()=>null);d=Date.now();let n=!t||t<=0?1:t;if(n===u)return;s(`[Compute/client-cache] concurrency ${u} → ${n} (server child count)`),u=n,e?.setMaxConcurrent(n)}finally{f=!1}}}await p();let m=l.createScheduler({mode:`queue`,maxConcurrent:u,maxQueueDepth:n.maxQueueDepth||void 0,queueWaitMs:n.queueWaitMs||void 0,timeoutMs:n.solveDeadlineMs,cache:n.responseCacheMaxBytes>0&&{maxBytes:n.responseCacheMaxBytes},reuseServerDefinitionCache:n.reuseServerDefinitionCache,onSettle:(e,t)=>{if(a.seq+=1,t.status===`success`&&(t.fromCache!==!0&&Date.now()-d>=3e5&&p(m),a.last={fromCache:t.fromCache===!0,definitionReuploaded:t.definitionReuploaded},n.debug&&(s(`[Compute/selva-cache] ${t.fromCache?`HIT — served from cache (no compute call)`:`miss — went to Rhino.Compute`} (${Math.round(t.durationMs)}ms)`),t.fromCache||s(t.definitionReuploaded===!0?`[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)`:t.definitionReuploaded===!1?`[Compute/def-cache] HIT — definition reused from server cache (no upload)`:`[Compute/def-cache] n/a — reuse disabled or non-reusable definition`),!t.fromCache))){let e=t.response?.errors,r=Array.isArray(e)?e.length:0;if(r>0){let t=n.cacheerroredsolves?`cached (cacheerroredsolves on)`:`not cached (cacheerroredsolves off) — re-run until fixed`;s(`[Compute/rhino-cache] ${r} GH error(s); ${t}. First: ${JSON.stringify(e[0])}`)}}}});return{client:l,scheduler:m,rhinoTiming:i,solveMeta:a}}return{async getClient(e,t){let n=e.id,l=i.get(n);if(l)return i.delete(n),i.set(n,l),l;let u=a.get(n);if(u)return u;let d=o.get(n);if(d){if(Date.now()-d.at<p)throw s(`[Compute/client-cache] server ${n} failed to build <${p}ms ago — replaying error`),d.error;o.delete(n)}s(`[Compute/client-cache] miss — building warm client for server ${n}`);let f=(async()=>{let a;try{a=await c(e,t?.definitionGuid)}catch(e){throw o.set(n,{error:e,at:Date.now()}),e}if(i.size>=r){let e=i.keys().next().value;e!==void 0&&(s(`[Compute/client-cache] LRU evicted warm client for server ${e} (cap ${r})`),i.get(e)?.scheduler.dispose(),i.delete(e))}return i.set(n,a),a})().finally(()=>a.delete(n));return a.set(n,f),f},evict(e){let t=e;o.delete(t);let n=i.get(t);n&&(s(`[Compute/client-cache] evicted warm client for server ${t} (config change)`),n.scheduler.dispose(),i.delete(t))},solveCacheStats(){let e={warmClients:i.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let t of i.values()){let n=t.scheduler.cacheStats();e.entries+=n.entries,e.bytes+=n.bytes,e.hits+=n.hits,e.misses+=n.misses,e.evictions+=n.evictions}return e},clearSolveCaches(){s(`[Compute/client-cache] cleared solve caches on ${i.size} warm client(s)`);for(let e of i.values())e.scheduler.clearCache()},disposeAll(){s(`[Compute/client-cache] disposing all ${i.size} warm client(s)`);for(let e of i.values())e.scheduler.dispose();i.clear(),o.clear()}}}function h(e){let t=Math.max(0,Math.floor(e)),n=new Map,r=0,i={hits:0,misses:0,evictions:0};function a(e,t){n.delete(e),n.set(e,t)}function o(){for(;r>t&&n.size>0;){let e=n.keys().next().value,t=n.get(e);n.delete(e),r-=t.bytes.byteLength,i.evictions+=1}}function s(e,i){if(t===0||i.byteLength>t)return;let a=n.get(e);a&&(r-=a.bytes.byteLength),n.set(e,{bytes:i}),r+=i.byteLength,o()}return{getOrLoad(e,t){let r={loaded:!1,fromCache:!1};return{key:e,outcome:r,load:async()=>{let o=n.get(e);if(o)return a(e,o),i.hits+=1,r.loaded=!0,r.fromCache=!0,o.bytes;i.misses+=1,r.loaded=!0,r.fromCache=!1;let c=await t();return s(e,c),c}}},stats(){return{hits:i.hits,misses:i.misses,evictions:i.evictions,entries:n.size,bytes:r}},clear(){n.clear(),r=0}}}function g(e={}){let t=new Map;return{run(n,r,i){let a=t.get(n);if(a)return e.onJoin?.(n),a.notifyOwner?.(),a.promise;let o={notifyOwner:i,promise:void 0},s=(async()=>r())().finally(()=>{o.notifyOwner=void 0,t.get(n)===o&&t.delete(n)});return o.promise=s,t.set(n,o),s},inFlight(){return t.size}}}function _(e){return typeof e==`object`&&!!e&&!(e instanceof Uint8Array)&&`outcome`in e}function v(e){return typeof e==`object`&&!!e&&!(e instanceof Uint8Array)&&!(`outcome`in e)&&!(`key`in e)&&`versionId`in e}var y=class{limits;clientCache;byteCache;singleFlight;constructor(e){this.limits=e.limits;let t=e.logger??new i.NoopLogger;this.clientCache=m({solveDeadlineMs:this.limits.solveDeadlineMs,cachesolve:this.limits.computeServerCachesolve,cacheerroredsolves:this.limits.computeCacheErroredSolves,reuseServerDefinitionCache:this.limits.computeReuseDefinitionCache,maxQueueDepth:this.limits.computeMaxQueueDepth,queueWaitMs:this.limits.computeQueueWaitMs,responseCacheMaxBytes:this.limits.computeSolveCacheBytes,debug:e.debug??!1,maxWarmComputeServers:e.maxWarmComputeServers,onDebugLog:e.onDebugLog??(e=>t.debug(e,{component:`Compute/client-cache`}))}),this.byteCache=h(this.limits.computeDefinitionCacheBytes),this.singleFlight=g({onJoin:e.onSolveCoalesced})}getClient(e,t){return this.clientCache.getClient(e,t)}definitionRef(e,t){return this.byteCache.getOrLoad(e,t)}evictServer(e){this.clientCache.evict(e)}clearSolveCaches(){this.clientCache.clearSolveCaches()}clearDefinitionCache(){this.byteCache.clear()}stats(){return{client:this.clientCache.solveCacheStats(),definitionBytes:this.byteCache.stats(),coalescing:{inFlight:this.singleFlight.inFlight()}}}async solve(t){let{definitionSource:n,byteRefOutcome:r,identity:i}=this.resolveDefinitionSource(t),a=await this.clientCache.getClient(t.server,{definitionGuid:t.definitionGuid}),o=c(t.inputs,t.values),s=`${i}:${t.server.id}:${(0,e.stableStringify)(o)}`,d=new AbortController,f=!1,p=()=>{f||d.abort()};t.signal.addEventListener(`abort`,p,{once:!0});let m=t.acceptEncoding??``,h;try{h=await this.singleFlight.run(s,()=>l({definitionSource:n,byteRefOutcome:r,inputs:t.inputs,values:t.values,inputTree:o,client:a,responseMaxBytes:this.limits.computeResponseMaxBytes,solveDeadlineMs:this.limits.solveDeadlineMs,acceptEncoding:m,signal:d.signal,loadStartMs:t.loadStartMs??performance.now(),defLoadMs:t.defLoadMs??0,prepMarks:t.prepMarks}),()=>{f=!0})}finally{t.signal.removeEventListener(`abort`,p)}if(h.kind!==`ok`)return h;let g=u(h.envelope,m);return{...h,envelope:{...h.envelope,body:g.body,headers:g.headers}}}toResponse(e,t){let n=(e,n)=>{t?.onError&&t.onError(e,n);let r={"Content-Type":`application/json`};return n.retryAfter!==void 0&&(r[`Retry-After`]=String(n.retryAfter)),{status:e,headers:r,body:JSON.stringify(n)}};switch(e.kind){case`ok`:return{status:200,headers:e.envelope.headers,body:e.envelope.body};case`timeout`:return n(504,{message:e.message});case`client_abort`:return n(499,{message:`Client closed request`});case`too_large`:return n(413,{message:`Solve result is too large to return. This usually means a file output exceeds the supported size.`});case`shed`:return n(503,{message:e.message,retryAfter:e.retryAfterSeconds});case`compute_error`:throw e.error}}toWebResponse(e){let t=this.toResponse(e);return new Response(typeof t.body==`string`?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(t){let n=t.definitionSource;if(v(n)){let e=this.byteCache.getOrLoad(n.versionId,n.load);return{definitionSource:e,byteRefOutcome:e.outcome,identity:e.key}}if(_(n))return{definitionSource:n,byteRefOutcome:n.outcome,identity:n.key};if((0,e.isDefinitionRef)(n))return{definitionSource:n,identity:n.key};if(!t.definitionKey)throw Error(`SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.`);return{definitionSource:n,identity:t.definitionKey}}};exports.COMPUTE_CONTRACT_VERSION=1,exports.COMPUTE_VERSION_HEADER=s,exports.SolveEngine=y,exports.adaptEnvelopeToEncoding=u,exports.buildSolveInputTree=c,exports.createClientCache=m,exports.createDefinitionByteCache=h,exports.createSolveCacheSingleFlight=g,Object.defineProperty(exports,"isDefinitionRef",{enumerable:!0,get:function(){return t.isDefinitionRef}}),exports.runSolvePipeline=l,exports.serverIdentity=f,exports.transformInputParameter=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@selvajs/compute/grasshopper"),t=require("@selvajs/compute/core"),n=require("node:zlib"),r=require("node:util"),i=require("@selvajs/platform");function a(t,n){let r={id:t.id,name:t.nickname,nickname:t.nickname||null,description:t.description||``,paramType:t.paramType===`dynamicValueList`?`valueList`:t.paramType,treeAccess:t.inputStructure===`tree`,minimum:t.minimum??null,maximum:t.maximum??null,atLeast:1,atMost:1,stepSize:t.stepSize,default:n??t.default},i=(0,e.processInput)(r),a=n??t.default;return i.default==null&&a!=null&&(Array.isArray(a)||i.paramType===`ValueList`&&typeof a!=`object`)&&(i.default=a),i}const o=(0,r.promisify)(n.gzip),s=`X-Selva-Compute-Version`;function c(t,n){return e.TreeBuilder.fromInputParams(t.filter(e=>e.paramType).map(e=>a(e,n[e.id])))}async function l(e){let{client:n,signal:r}=e,i=performance.now(),a=e.inputTree??c(e.inputs,e.values),l=performance.now()-i,u=n.solveMeta.seq,f=n.rhinoTiming.seq,p,m=performance.now();try{p=await n.scheduler.solve(e.definitionSource,a,{signal:r,selvaevents:e.selvaEvents})}catch(n){let i=performance.now()-m,a=n instanceof Error&&n.name===`AbortError`||n instanceof t.ComputeError&&n.code===t.ErrorCodes.ABORTED,o=n instanceof t.ComputeError&&n.code===t.ErrorCodes.TIMEOUT_ERROR;if(a||o)return a&&r.aborted?{kind:`client_abort`,durationMs:i}:{kind:`timeout`,durationMs:i,message:`Solve exceeded the ${Math.round(e.solveDeadlineMs/1e3)}s deadline.`};if(n instanceof t.ComputeError){if(n.code===t.ErrorCodes.QUEUE_FULL)return{kind:`shed`,durationMs:i,reason:`queue_full`,retryAfterSeconds:1,message:`Compute server is at capacity. Retry shortly.`};if(n.code===t.ErrorCodes.QUEUE_TIMEOUT)return{kind:`shed`,durationMs:i,reason:`queue_timeout`,retryAfterSeconds:1,message:`Compute request waited too long in the queue. Retry shortly.`}}return{kind:`compute_error`,durationMs:i,error:n}}let h=performance.now()-m,g=performance.now(),_;try{_=JSON.stringify(p)}catch(e){if(e instanceof RangeError)return{kind:`too_large`};throw e}if(_.length>e.responseMaxBytes)return{kind:`too_large`};let v=performance.now()-g,y=/\bgzip\b/i.test(e.acceptEncoding)&&_.length>1024,b=null,x=0;if(y){let e=performance.now();b=await o(Buffer.from(_)),x=performance.now()-e}let S=performance.now()-e.loadStartMs,C=n.solveMeta.seq===u+1?n.solveMeta.last:null,w=C&&!C.fromCache&&n.rhinoTiming.seq===f+1?n.rhinoTiming.last:null,T={"Content-Type":`application/json`,"Server-Timing":d({defLoadMs:e.defLoadMs,treeBuildMs:l,solveMs:h,serializeMs:v,gzipMs:x,serverTotalMs:S,rhino:w,settle:C,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[s]:`1`,Vary:`Accept-Encoding`},E={treeBuildMs:l,solveMs:h,serializeMs:v,gzipMs:x,serverTotalMs:S,serializedBytes:_.length,compressedBytes:b?.byteLength??null},D;return b?(T[`Content-Encoding`]=`gzip`,T[`Content-Length`]=String(b.byteLength),D={body:new Uint8Array(b),encoding:`gzip`,headers:T,result:p,metrics:E}):(T[`Content-Length`]=String(Buffer.byteLength(_)),D={body:_,headers:T,result:p,metrics:E}),{kind:`ok`,envelope:D,solveMs:h,errorCount:p.errors?.length??0,warningCount:p.warnings?.length??0}}function u(e,t){let r=/\bgzip\b/i.test(t);if(e.encoding===`gzip`&&!r){let t=(0,n.gunzipSync)(e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString(`utf8`),r={...e.headers};return delete r[`Content-Encoding`],r[`Content-Length`]=String(Buffer.byteLength(t)),{body:t,headers:r}}return{body:e.body,headers:e.headers}}function d(e){let t=`load;dur=${e.defLoadMs.toFixed(1)}, tree;dur=${e.treeBuildMs.toFixed(1)}, solve;dur=${e.solveMs.toFixed(1)}, serialize;dur=${e.serializeMs.toFixed(1)}, gzip;dur=${e.gzipMs.toFixed(1)}, total;dur=${e.serverTotalMs.toFixed(1)}`,n=e.rhino;if(n){let r=n.decode+n.solve+n.encode,i=Math.max(0,e.solveMs-r);t+=`, rhino_decode;dur=${n.decode.toFixed(1)}, rhino_solve;dur=${n.solve.toFixed(1)}, rhino_encode;dur=${n.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[n,r]of e.prepMarks)t+=`, p_${n};dur=${r.toFixed(1)}`;let r=e.settle;r&&(t+=`, selva_cache;dur=${+!!r.fromCache}`,r.definitionReuploaded!==void 0&&(t+=`, def_reupload;dur=${+!!r.definitionReuploaded}`));let i=e.byteRefOutcome;if(i){let e=i.loaded?i.fromCache?`hit`:`miss`:`skipped`;t+=`, def_bytes;desc=${e}`}return t}function f(e){return e.id}const p=5e3;function m(n){let r=n.maxWarmComputeServers??16,i=new Map,a=new Map,o=new Map;n.debug===`verbose`&&(0,t.enableDebugLogging)();let s=e=>{n.debug&&n.onDebugLog?.(e)};async function c(t,r){let i={last:null,seq:0},a={last:null,seq:0},o=n.debug===`verbose`,c={serverUrl:t.serverUrl,apiKey:t.apiKey,debug:o,cachesolve:n.cachesolve,cacheerroredsolves:n.cacheerroredsolves,onServerTiming:e=>{let t=e.decode??0,r=e.solve??0,a=e.encode??0;if(i.seq+=1,i.last={decode:t,solve:r,encode:a},!n.debug)return;let o=t<=1?`HIT (definition reused, no re-upload/parse)`:`miss (full parse)`,c=n.cachesolve&&r<=1?`HIT (cached result, solve skipped)`:n.cachesolve?`miss (solved fresh)`:`off`;s(`[Compute/rhino-cache] decode=${t}ms solve=${r}ms encode=${a}ms | definition-cache: ${o} | solve-cache (cachesolve): ${c}`)}};r&&(c.headers={"X-Selva-Definition":r});let l=await e.GrasshopperClient.create(c),u=1,d=0,f=!1;async function p(e){if(!f){f=!0;try{let t=await Promise.resolve(l.serverStats?.getActiveChildren({initialize:!1})).catch(()=>null);d=Date.now();let n=!t||t<=0?1:t;if(n===u)return;s(`[Compute/client-cache] concurrency ${u} → ${n} (server child count)`),u=n,e?.setMaxConcurrent(n)}finally{f=!1}}}await p();let m=l.createScheduler({mode:`queue`,maxConcurrent:u,maxQueueDepth:n.maxQueueDepth||void 0,queueWaitMs:n.queueWaitMs||void 0,timeoutMs:n.solveDeadlineMs,cache:n.responseCacheMaxBytes>0&&{maxBytes:n.responseCacheMaxBytes},reuseServerDefinitionCache:n.reuseServerDefinitionCache,onSettle:(e,t)=>{if(a.seq+=1,t.status===`success`&&(t.fromCache!==!0&&Date.now()-d>=3e5&&p(m),a.last={fromCache:t.fromCache===!0,definitionReuploaded:t.definitionReuploaded},n.debug&&(s(`[Compute/selva-cache] ${t.fromCache?`HIT — served from cache (no compute call)`:`miss — went to Rhino.Compute`} (${Math.round(t.durationMs)}ms)`),t.fromCache||s(t.definitionReuploaded===!0?`[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)`:t.definitionReuploaded===!1?`[Compute/def-cache] HIT — definition reused from server cache (no upload)`:`[Compute/def-cache] n/a — reuse disabled or non-reusable definition`),!t.fromCache))){let e=t.response?.errors,r=Array.isArray(e)?e.length:0;if(r>0){let t=n.cacheerroredsolves?`cached (cacheerroredsolves on)`:`not cached (cacheerroredsolves off) — re-run until fixed`;s(`[Compute/rhino-cache] ${r} GH error(s); ${t}. First: ${JSON.stringify(e[0])}`)}}}});return{client:l,scheduler:m,rhinoTiming:i,solveMeta:a}}return{async getClient(e,t){let n=e.id,l=i.get(n);if(l)return i.delete(n),i.set(n,l),l;let u=a.get(n);if(u)return u;let d=o.get(n);if(d){if(Date.now()-d.at<p)throw s(`[Compute/client-cache] server ${n} failed to build <${p}ms ago — replaying error`),d.error;o.delete(n)}s(`[Compute/client-cache] miss — building warm client for server ${n}`);let f=(async()=>{let a;try{a=await c(e,t?.definitionGuid)}catch(e){throw o.set(n,{error:e,at:Date.now()}),e}if(i.size>=r){let e=i.keys().next().value;e!==void 0&&(s(`[Compute/client-cache] LRU evicted warm client for server ${e} (cap ${r})`),i.get(e)?.scheduler.dispose(),i.delete(e))}return i.set(n,a),a})().finally(()=>a.delete(n));return a.set(n,f),f},evict(e){let t=e;o.delete(t);let n=i.get(t);n&&(s(`[Compute/client-cache] evicted warm client for server ${t} (config change)`),n.scheduler.dispose(),i.delete(t))},solveCacheStats(){let e={warmClients:i.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let t of i.values()){let n=t.scheduler.cacheStats();e.entries+=n.entries,e.bytes+=n.bytes,e.hits+=n.hits,e.misses+=n.misses,e.evictions+=n.evictions}return e},clearSolveCaches(){s(`[Compute/client-cache] cleared solve caches on ${i.size} warm client(s)`);for(let e of i.values())e.scheduler.clearCache()},disposeAll(){s(`[Compute/client-cache] disposing all ${i.size} warm client(s)`);for(let e of i.values())e.scheduler.dispose();i.clear(),o.clear()}}}function h(e){let t=Math.max(0,Math.floor(e)),n=new Map,r=0,i={hits:0,misses:0,evictions:0};function a(e,t){n.delete(e),n.set(e,t)}function o(){for(;r>t&&n.size>0;){let e=n.keys().next().value,t=n.get(e);n.delete(e),r-=t.bytes.byteLength,i.evictions+=1}}function s(e,i){if(t===0||i.byteLength>t)return;let a=n.get(e);a&&(r-=a.bytes.byteLength),n.set(e,{bytes:i}),r+=i.byteLength,o()}return{getOrLoad(e,t){let r={loaded:!1,fromCache:!1};return{key:e,outcome:r,load:async()=>{let o=n.get(e);if(o)return a(e,o),i.hits+=1,r.loaded=!0,r.fromCache=!0,o.bytes;i.misses+=1,r.loaded=!0,r.fromCache=!1;let c=await t();return s(e,c),c}}},stats(){return{hits:i.hits,misses:i.misses,evictions:i.evictions,entries:n.size,bytes:r}},clear(){n.clear(),r=0}}}function g(e={}){let t=new Map;return{run(n,r,i){let a=t.get(n);if(a)return e.onJoin?.(n),a.notifyOwner?.(),a.promise;let o={notifyOwner:i,promise:void 0},s=(async()=>r())().finally(()=>{o.notifyOwner=void 0,t.get(n)===o&&t.delete(n)});return o.promise=s,t.set(n,o),s},inFlight(){return t.size}}}function _(e){return typeof e==`object`&&!!e&&!(e instanceof Uint8Array)&&`outcome`in e}function v(e){return typeof e==`object`&&!!e&&!(e instanceof Uint8Array)&&!(`outcome`in e)&&!(`key`in e)&&`versionId`in e}var y=class{limits;clientCache;byteCache;singleFlight;constructor(e){this.limits=e.limits;let t=e.logger??new i.NoopLogger;this.clientCache=m({solveDeadlineMs:this.limits.solveDeadlineMs,cachesolve:this.limits.computeServerCachesolve,cacheerroredsolves:this.limits.computeCacheErroredSolves,reuseServerDefinitionCache:this.limits.computeReuseDefinitionCache,maxQueueDepth:this.limits.computeMaxQueueDepth,queueWaitMs:this.limits.computeQueueWaitMs,responseCacheMaxBytes:this.limits.computeSolveCacheBytes,debug:e.debug??!1,maxWarmComputeServers:e.maxWarmComputeServers,onDebugLog:e.onDebugLog??(e=>t.debug(e,{component:`Compute/client-cache`}))}),this.byteCache=h(this.limits.computeDefinitionCacheBytes),this.singleFlight=g({onJoin:e.onSolveCoalesced})}getClient(e,t){return this.clientCache.getClient(e,t)}definitionRef(e,t){return this.byteCache.getOrLoad(e,t)}evictServer(e){this.clientCache.evict(e)}clearSolveCaches(){this.clientCache.clearSolveCaches()}clearDefinitionCache(){this.byteCache.clear()}stats(){return{client:this.clientCache.solveCacheStats(),definitionBytes:this.byteCache.stats(),coalescing:{inFlight:this.singleFlight.inFlight()}}}async solve(t){let{definitionSource:n,byteRefOutcome:r,identity:i}=this.resolveDefinitionSource(t),a=await this.clientCache.getClient(t.server,{definitionGuid:t.definitionGuid}),o=c(t.inputs,t.values),s=`${i}:${t.server.id}:${(0,e.stableStringify)(o)}`,d=new AbortController,f=!1,p=()=>{f||d.abort()};t.signal.addEventListener(`abort`,p,{once:!0});let m=t.acceptEncoding??``,h;try{h=await this.singleFlight.run(s,()=>l({definitionSource:n,byteRefOutcome:r,inputs:t.inputs,values:t.values,inputTree:o,client:a,responseMaxBytes:this.limits.computeResponseMaxBytes,solveDeadlineMs:this.limits.solveDeadlineMs,acceptEncoding:m,selvaEvents:t.selvaEvents,signal:d.signal,loadStartMs:t.loadStartMs??performance.now(),defLoadMs:t.defLoadMs??0,prepMarks:t.prepMarks}),()=>{f=!0})}finally{t.signal.removeEventListener(`abort`,p)}if(h.kind!==`ok`)return h;let g=u(h.envelope,m);return{...h,envelope:{...h.envelope,body:g.body,headers:g.headers}}}toResponse(e,t){let n=(e,n)=>{t?.onError&&t.onError(e,n);let r={"Content-Type":`application/json`};return n.retryAfter!==void 0&&(r[`Retry-After`]=String(n.retryAfter)),{status:e,headers:r,body:JSON.stringify(n)}};switch(e.kind){case`ok`:return{status:200,headers:e.envelope.headers,body:e.envelope.body};case`timeout`:return n(504,{message:e.message});case`client_abort`:return n(499,{message:`Client closed request`});case`too_large`:return n(413,{message:`Solve result is too large to return. This usually means a file output exceeds the supported size.`});case`shed`:return n(503,{message:e.message,retryAfter:e.retryAfterSeconds});case`compute_error`:throw e.error}}toWebResponse(e){let t=this.toResponse(e);return new Response(typeof t.body==`string`?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(t){let n=t.definitionSource;if(v(n)){let e=this.byteCache.getOrLoad(n.versionId,n.load);return{definitionSource:e,byteRefOutcome:e.outcome,identity:e.key}}if(_(n))return{definitionSource:n,byteRefOutcome:n.outcome,identity:n.key};if((0,e.isDefinitionRef)(n))return{definitionSource:n,identity:n.key};if(!t.definitionKey)throw Error(`SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.`);return{definitionSource:n,identity:t.definitionKey}}};exports.COMPUTE_CONTRACT_VERSION=1,exports.COMPUTE_VERSION_HEADER=s,exports.SolveEngine=y,exports.adaptEnvelopeToEncoding=u,exports.buildSolveInputTree=c,exports.createClientCache=m,exports.createDefinitionByteCache=h,exports.createSolveCacheSingleFlight=g,Object.defineProperty(exports,"isDefinitionRef",{enumerable:!0,get:function(){return t.isDefinitionRef}}),exports.runSolvePipeline=l,exports.serverIdentity=f,exports.transformInputParameter=a;
2
2
  //# sourceMappingURL=server.cjs.map