@selvajs/solve 0.2.0-beta.1 → 0.2.0-beta.4
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 +10 -14
- package/dist/client.cjs +1 -1
- package/dist/client.d.cts +71 -32
- package/dist/client.d.ts +71 -32
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +190 -134
- package/dist/server.d.ts +190 -134
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/shared.d.cts +1 -1
- package/dist/shared.d.ts +1 -1
- package/dist/solve-fn-0hPOtZVD.d.cts +37 -0
- package/dist/solve-fn-0hPOtZVD.d.ts +37 -0
- package/package.json +1 -1
- package/dist/solve-fn-DGzPCsDu.d.cts +0 -22
- package/dist/solve-fn-DGzPCsDu.d.ts +0 -22
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, SolveDefinition, DataTree, InputParam } from '@selvajs/compute';
|
|
1
|
+
import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, SolveDefinition, DataTree, InputParam, DefinitionRef } from '@selvajs/compute';
|
|
2
|
+
export { DefinitionRef, SolveDefinition, isDefinitionRef } from '@selvajs/compute';
|
|
2
3
|
import { SchemaInput } from '@selvajs/schemas';
|
|
4
|
+
import { ILogger } from '@selvajs/platform';
|
|
3
5
|
export { S as SolveInput } from './solve-input-CqYtetTA.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -13,21 +15,20 @@ type ServerIdentity = string & {
|
|
|
13
15
|
interface ResolvedServer {
|
|
14
16
|
id: string;
|
|
15
17
|
serverUrl: string;
|
|
16
|
-
/** Sent as `RhinoComputeKey
|
|
18
|
+
/** Sent as the `RhinoComputeKey` header. */
|
|
17
19
|
apiKey?: string;
|
|
18
20
|
}
|
|
19
|
-
/** Derive the opaque cache identity from a resolved server. Identity is the `id`. */
|
|
20
21
|
declare function serverIdentity(server: Pick<ResolvedServer, 'id'>): ServerIdentity;
|
|
21
22
|
interface CachedClient {
|
|
22
23
|
client: GrasshopperClient;
|
|
23
24
|
scheduler: SolveScheduler;
|
|
24
25
|
/**
|
|
25
|
-
* Last Server-Timing decode/solve/encode, written by onServerTiming
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Last Server-Timing decode/solve/encode, written by `onServerTiming`. The
|
|
27
|
+
* scheduler runs up to `maxConcurrent` solves at once, so `last` alone can't
|
|
28
|
+
* be trusted per-request: callers snapshot `seq` before their solve and only
|
|
29
|
+
* attribute `last` to themselves if exactly one write happened since (see
|
|
30
|
+
* the guard in `runSolvePipeline`), dropping it otherwise rather than risk
|
|
31
|
+
* misattributing another request's timing.
|
|
31
32
|
*/
|
|
32
33
|
rhinoTiming: {
|
|
33
34
|
last: {
|
|
@@ -39,7 +40,7 @@ interface CachedClient {
|
|
|
39
40
|
};
|
|
40
41
|
/**
|
|
41
42
|
* Same snapshot-and-attribute pattern as `rhinoTiming`, for the scheduler's
|
|
42
|
-
* onSettle cache verdict. `seq` increments on every settle (success or
|
|
43
|
+
* `onSettle` cache verdict. `seq` increments on every settle (success or
|
|
43
44
|
* error); `last` is written only on success.
|
|
44
45
|
*/
|
|
45
46
|
solveMeta: {
|
|
@@ -50,48 +51,44 @@ interface CachedClient {
|
|
|
50
51
|
seq: number;
|
|
51
52
|
};
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Debug verbosity: `false` is silent, `true` gives concise cache/timing logs
|
|
56
|
+
* (`onDebugLog`), `'verbose'` also enables full lib-level request/response
|
|
57
|
+
* dumps (incl. geometry) — a superset of `true`, not an independent setting.
|
|
58
|
+
*/
|
|
59
|
+
type ClientCacheDebug = boolean | 'verbose';
|
|
53
60
|
/** Config injected by the consuming app, so this module stays env-agnostic and testable. */
|
|
54
61
|
interface ClientCacheConfig {
|
|
55
62
|
/** Per-solve timeout forwarded to the scheduler (`ComputeLimits.maxSolveDurationMs`). */
|
|
56
63
|
maxSolveDurationMs: number;
|
|
57
|
-
/** Ask Rhino.Compute to cache solve results and return them on identical repeats. */
|
|
58
64
|
cachesolve: boolean;
|
|
59
|
-
/**
|
|
65
|
+
/** Only meaningful with `cachesolve`. */
|
|
60
66
|
cacheerroredsolves: boolean;
|
|
61
|
-
/** Reference large definitions by server cache key
|
|
67
|
+
/** Reference large definitions by server cache key instead of re-uploading. */
|
|
62
68
|
reuseServerDefinitionCache: boolean;
|
|
63
69
|
/**
|
|
64
|
-
* Max in
|
|
65
|
-
*
|
|
66
|
-
* (`ComputeLimits.
|
|
67
|
-
|
|
68
|
-
maxConcurrentSolves: number;
|
|
69
|
-
/**
|
|
70
|
-
* Backpressure — max solves that may WAIT in the FIFO queue (excludes the
|
|
71
|
-
* in-flight `maxConcurrentSolves`). `0` = unbounded (`ComputeLimits.
|
|
72
|
-
* computeMaxQueueDepth`); a full queue sheds new solves with `QUEUE_FULL`.
|
|
70
|
+
* Max solves that may wait in the FIFO queue, excluding in-flight solves
|
|
71
|
+
* (capped at `maxConcurrent`, itself driven by the server's probed child
|
|
72
|
+
* count). `0` = unbounded (`ComputeLimits.computeMaxQueueDepth`); a full
|
|
73
|
+
* queue rejects new solves with `QUEUE_FULL`.
|
|
73
74
|
*/
|
|
74
75
|
maxQueueDepth: number;
|
|
75
76
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
77
|
+
* Max ms a solve may sit queued before executing; `0` = no deadline
|
|
78
|
+
* (`ComputeLimits.computeQueueWaitMs`). Too long a wait rejects with
|
|
78
79
|
* `QUEUE_TIMEOUT`.
|
|
79
80
|
*/
|
|
80
81
|
queueWaitMs: number;
|
|
81
82
|
/**
|
|
82
|
-
* Byte budget for this client's in-process solve cache
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).
|
|
83
|
+
* Byte budget for this client's in-process solve cache. Applies per warm
|
|
84
|
+
* client — worst-case heap is this × `maxWarmComputeServers`. `0` disables
|
|
85
|
+
* the cache (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).
|
|
86
86
|
*/
|
|
87
87
|
responseCacheMaxBytes: number;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
/** Max distinct warm clients before the LRU evicts the oldest. Default 16. */
|
|
93
|
-
maxCachedClients?: number;
|
|
94
|
-
/** Sink for the concise debug lines (the app wires `console.log`). */
|
|
88
|
+
debug: ClientCacheDebug;
|
|
89
|
+
/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */
|
|
90
|
+
maxWarmComputeServers?: number;
|
|
91
|
+
/** Sink for the debug lines. `onDebugLog` only fires when `debug` is not `false`. */
|
|
95
92
|
onDebugLog?: (message: string) => void;
|
|
96
93
|
}
|
|
97
94
|
interface ClientCache {
|
|
@@ -106,11 +103,7 @@ interface ClientCache {
|
|
|
106
103
|
}): Promise<CachedClient>;
|
|
107
104
|
/** Dispose and drop the warm client for `id`, so the next request rebuilds against fresh connection details. */
|
|
108
105
|
evict(id: string | ServerIdentity): void;
|
|
109
|
-
/**
|
|
110
|
-
* Solve-cache counters summed across every warm client. `warmClients` is
|
|
111
|
-
* reported alongside since each client owns its own cache. Counters die
|
|
112
|
-
* with the client that owns them, so totals can fall over time.
|
|
113
|
-
*/
|
|
106
|
+
/** Solve-cache counters summed across every warm client. Counters die with the client that owns them, so totals can fall over time. */
|
|
114
107
|
solveCacheStats(): SolveCacheStats;
|
|
115
108
|
/**
|
|
116
109
|
* Drop every retained solve result, keeping the warm clients (and their
|
|
@@ -123,7 +116,6 @@ interface ClientCache {
|
|
|
123
116
|
/** Dispose every warm client. Test seam / shutdown hook. */
|
|
124
117
|
disposeAll(): void;
|
|
125
118
|
}
|
|
126
|
-
/** Aggregate solve-cache counters across the warm clients (see `solveCacheStats`). */
|
|
127
119
|
interface SolveCacheStats {
|
|
128
120
|
warmClients: number;
|
|
129
121
|
entries: number;
|
|
@@ -162,7 +154,6 @@ declare function createClientCache(config: ClientCacheConfig): ClientCache;
|
|
|
162
154
|
*/
|
|
163
155
|
interface ByteRefOutcome {
|
|
164
156
|
loaded: boolean;
|
|
165
|
-
/** When `loaded`, whether the bytes came from a warm cache entry. */
|
|
166
157
|
fromCache: boolean;
|
|
167
158
|
}
|
|
168
159
|
/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */
|
|
@@ -171,24 +162,16 @@ interface ByteCacheRef {
|
|
|
171
162
|
load: () => Promise<Uint8Array>;
|
|
172
163
|
outcome: ByteRefOutcome;
|
|
173
164
|
}
|
|
174
|
-
/** Hit/miss/eviction counters for observability (Server-Timing, admin debug). */
|
|
175
165
|
interface ByteCacheStats {
|
|
176
166
|
hits: number;
|
|
177
167
|
misses: number;
|
|
178
|
-
/** Entries dropped by the byte-budget LRU. */
|
|
179
168
|
evictions: number;
|
|
180
169
|
entries: number;
|
|
181
170
|
bytes: number;
|
|
182
171
|
}
|
|
183
172
|
interface DefinitionByteCache {
|
|
184
|
-
/**
|
|
185
|
-
* `DefinitionRef` whose `load()` serves `versionId`'s bytes from the cache
|
|
186
|
-
* when warm, otherwise calls `load` and caches the result. Cheap to build
|
|
187
|
-
* and moves no bytes until `load()` runs.
|
|
188
|
-
*/
|
|
189
173
|
getOrLoad(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;
|
|
190
174
|
stats(): ByteCacheStats;
|
|
191
|
-
/** Test seam / eviction on definition delete if ever needed. */
|
|
192
175
|
clear(): void;
|
|
193
176
|
}
|
|
194
177
|
/**
|
|
@@ -200,10 +183,9 @@ declare function createDefinitionByteCache(maxBytes: number): DefinitionByteCach
|
|
|
200
183
|
/**
|
|
201
184
|
* Transport-agnostic solve pipeline: given an already-resolved solve context
|
|
202
185
|
* (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* in the route that calls this.
|
|
186
|
+
* solve and returns a discriminated {@link SolveOutcome} instead of throwing
|
|
187
|
+
* for expected failures. Auth, the database, share tokens, rate limits, and
|
|
188
|
+
* metric sinks are the calling route's job, not this file's.
|
|
207
189
|
*/
|
|
208
190
|
|
|
209
191
|
/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */
|
|
@@ -216,25 +198,20 @@ type PipelineInput = SchemaInput & {
|
|
|
216
198
|
};
|
|
217
199
|
interface SolvePipelineArgs {
|
|
218
200
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* zero bytes.
|
|
201
|
+
* Raw `.gh` bytes, or a byte-cache `DefinitionRef` whose bytes the scheduler
|
|
202
|
+
* materializes only when an upload is unavoidable — a pointer-known solve
|
|
203
|
+
* moves zero bytes.
|
|
223
204
|
*/
|
|
224
205
|
definitionSource: SolveDefinition;
|
|
225
|
-
/**
|
|
226
|
-
* When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome
|
|
227
|
-
* so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for
|
|
228
|
-
* raw-bytes solves.
|
|
229
|
-
*/
|
|
206
|
+
/** Mutable load outcome for a `DefinitionRef` source, so the `def_bytes` Server-Timing verdict can be emitted. Omit for raw-bytes solves. */
|
|
230
207
|
byteRefOutcome?: ByteRefOutcome;
|
|
231
208
|
/** Persisted input params; only those with a `paramType` are sent to the solve. */
|
|
232
209
|
inputs: PipelineInput[];
|
|
233
210
|
/**
|
|
234
|
-
* A tree the caller already built with {@link buildSolveInputTree}
|
|
235
|
-
*
|
|
211
|
+
* A tree the caller already built with {@link buildSolveInputTree}; skips the
|
|
212
|
+
* pipeline's own build and solves this object directly.
|
|
236
213
|
*
|
|
237
|
-
*
|
|
214
|
+
* A caller coalescing concurrent solves needs this: a single-flight key
|
|
238
215
|
* derived from raw `{inputs, values}` would split two requests that transform
|
|
239
216
|
* to the same tree, which is the identity the scheduler actually caches on.
|
|
240
217
|
*/
|
|
@@ -243,28 +220,21 @@ interface SolvePipelineArgs {
|
|
|
243
220
|
values: Record<string, unknown>;
|
|
244
221
|
client: CachedClient;
|
|
245
222
|
responseMaxBytes: number;
|
|
246
|
-
/**
|
|
223
|
+
/** Only phrases the timeout message — the scheduler enforces the deadline itself. */
|
|
247
224
|
maxSolveDurationMs: number;
|
|
248
225
|
/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */
|
|
249
226
|
acceptEncoding: string;
|
|
250
227
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
228
|
+
* Forwarded to the scheduler so a client disconnect cancels the upstream
|
|
229
|
+
* compute call. Its `aborted` flag also distinguishes a client disconnect
|
|
230
|
+
* from the scheduler's own deadline firing.
|
|
254
231
|
*/
|
|
255
232
|
signal: AbortSignal;
|
|
256
|
-
/**
|
|
257
|
-
* Wall-clock origin (`performance.now()` captured at the top of the request)
|
|
258
|
-
* so the pipeline's `load`/`total` phase timings line up with the caller's
|
|
259
|
-
* pre-solve prep.
|
|
260
|
-
*/
|
|
233
|
+
/** Wall-clock origin (`performance.now()` at the top of the request) so `load`/`total` phase timings line up with the caller's pre-solve prep. */
|
|
261
234
|
loadStartMs: number;
|
|
262
235
|
/** Pre-solve "load" phase duration the caller already measured (auth + DB + fetch). */
|
|
263
236
|
defLoadMs: number;
|
|
264
|
-
/**
|
|
265
|
-
* Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`
|
|
266
|
-
* Server-Timing entries.
|
|
267
|
-
*/
|
|
237
|
+
/** Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*` Server-Timing entries. */
|
|
268
238
|
prepMarks?: [string, number][];
|
|
269
239
|
}
|
|
270
240
|
/** Phase timings the pipeline measured; the caller uses them for debug logging. */
|
|
@@ -288,12 +258,11 @@ interface SolveEnvelope {
|
|
|
288
258
|
metrics: SolvePhaseMetrics;
|
|
289
259
|
}
|
|
290
260
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* `
|
|
295
|
-
*
|
|
296
|
-
* for the metric record.
|
|
261
|
+
* `ok` carries the envelope; every other variant names an expected failure the
|
|
262
|
+
* calling route maps to a status code (timeout→504, client_abort→499,
|
|
263
|
+
* too_large→413, shed→503+Retry-After, compute_error→generic 500/503).
|
|
264
|
+
* `durationMs` on error variants is the solve wall time up to the failure, for
|
|
265
|
+
* the metric record.
|
|
297
266
|
*/
|
|
298
267
|
type SolveOutcome = {
|
|
299
268
|
kind: 'ok';
|
|
@@ -311,12 +280,7 @@ type SolveOutcome = {
|
|
|
311
280
|
} | {
|
|
312
281
|
kind: 'too_large';
|
|
313
282
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Scheduler backpressure shed the solve before it executed — the per-server
|
|
316
|
-
* queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline
|
|
317
|
-
* (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.
|
|
318
|
-
* `retryAfterSeconds` is a suggested backoff hint for the client.
|
|
319
|
-
*/
|
|
283
|
+
/** Scheduler backpressure rejected the solve before it ran: queue was full (`queue_full`) or the wait exceeded the deadline (`queue_timeout`). Retryable — `retryAfterSeconds` is a backoff hint. */
|
|
320
284
|
| {
|
|
321
285
|
kind: 'shed';
|
|
322
286
|
durationMs: number;
|
|
@@ -328,24 +292,19 @@ type SolveOutcome = {
|
|
|
328
292
|
durationMs: number;
|
|
329
293
|
error: unknown;
|
|
330
294
|
};
|
|
331
|
-
/**
|
|
332
|
-
* Build the transformed input tree — the exact object handed to the scheduler.
|
|
333
|
-
* `runSolvePipeline` calls this itself; a caller only needs it directly to see
|
|
334
|
-
* the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).
|
|
335
|
-
*/
|
|
295
|
+
/** The transformed input tree exactly as handed to the scheduler; `runSolvePipeline` calls this itself unless the caller supplies {@link SolvePipelineArgs.inputTree}. */
|
|
336
296
|
declare function buildSolveInputTree(inputs: PipelineInput[], values: Record<string, unknown>): DataTree[];
|
|
337
297
|
declare function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome>;
|
|
338
298
|
/**
|
|
339
299
|
* Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.
|
|
340
300
|
*
|
|
341
301
|
* The single-flight coalescer runs ONE pipeline execution for N identical
|
|
342
|
-
* concurrent solves and hands every waiter the same {@link SolveEnvelope}
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* result to each waiter instead.
|
|
302
|
+
* concurrent solves and hands every waiter the same {@link SolveEnvelope},
|
|
303
|
+
* baked from the FIRST caller's `Accept-Encoding`. A later waiter with a
|
|
304
|
+
* different `Accept-Encoding` would otherwise get a body labelled with the
|
|
305
|
+
* wrong encoding — `Vary` can't help here, since it's one shared object, not a
|
|
306
|
+
* cache lookup. Encoding is deliberately left out of the coalesce key so mixed
|
|
307
|
+
* clients still coalesce; this adapts the shared result to each waiter instead.
|
|
349
308
|
*
|
|
350
309
|
* Only the correctness-critical direction is adapted: a gzip envelope served to
|
|
351
310
|
* a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a
|
|
@@ -357,11 +316,8 @@ declare function adaptEnvelopeToEncoding(envelope: SolveEnvelope, acceptEncoding
|
|
|
357
316
|
};
|
|
358
317
|
|
|
359
318
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* are absent on `SchemaInput` (they live on the layout item's config), so
|
|
363
|
-
* valueList/file inputs fall back to carrying the selected value with no option
|
|
364
|
-
* list — the same value that reaches Grasshopper.
|
|
319
|
+
* `SchemaInput` has no `values`/`acceptedFormats` (those live on the layout item's config), so
|
|
320
|
+
* valueList/file inputs here carry just the selected value with no option list.
|
|
365
321
|
*/
|
|
366
322
|
declare function transformInputParameter(input: SchemaInput & {
|
|
367
323
|
minimum?: number;
|
|
@@ -372,44 +328,144 @@ declare function transformInputParameter(input: SchemaInput & {
|
|
|
372
328
|
/**
|
|
373
329
|
* In-process single-flight — dogpile protection for every solve.
|
|
374
330
|
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
331
|
+
* Always on, not gated behind a result cache being configured: the dogpile is worst
|
|
332
|
+
* when nothing else is caching, since N concurrent identical solves each pay a full
|
|
333
|
+
* Rhino round trip (the scheduler doesn't coalesce in-flight requests itself, and
|
|
334
|
+
* Rhino's `cachesolve` still costs a round trip per repeat). An earlier version
|
|
335
|
+
* gated this on cache config and so disabled it in exactly the deployments most
|
|
336
|
+
* exposed to a cold-key stampede — hot public definition plus a deploy.
|
|
381
337
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
338
|
+
* Per app instance, above `ISolveResultCache`. No cross-instance lease (Redis
|
|
339
|
+
* `SET NX`) yet.
|
|
384
340
|
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* app serializes it per response).
|
|
341
|
+
* The shared promise resolves to one value for every waiter, so `work` must return
|
|
342
|
+
* something safe to share by reference — the solve pipeline's envelope qualifies
|
|
343
|
+
* because the app serializes it per response.
|
|
389
344
|
*/
|
|
390
345
|
interface SolveCacheSingleFlight {
|
|
391
346
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
347
|
+
* Coalesces concurrent calls under the same key: the first caller runs `work`,
|
|
348
|
+
* later callers for that key await the same promise, and the key frees as soon
|
|
349
|
+
* as it settles.
|
|
395
350
|
*
|
|
396
|
-
* `onWaiterJoined` fires on the
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* settles.
|
|
351
|
+
* `onWaiterJoined` fires only on the owner's call, only while `work` is still
|
|
352
|
+
* running, each time another caller joins. Ownership changes what the owner's
|
|
353
|
+
* abort signal should mean: a solo run can cancel on its own client's
|
|
354
|
+
* disconnect, but a shared run can't without 499-ing every waiter.
|
|
401
355
|
*/
|
|
402
356
|
run<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T>;
|
|
403
|
-
/** Number of keys currently in flight (observability / tests). */
|
|
404
357
|
inFlight(): number;
|
|
405
358
|
}
|
|
406
359
|
interface SolveCacheSingleFlightOptions {
|
|
407
|
-
/**
|
|
408
|
-
* Fired when a caller joins an already-in-flight key instead of running its
|
|
409
|
-
* own work — the coalescing win this module exists for, otherwise invisible.
|
|
410
|
-
*/
|
|
411
360
|
onJoin?: (key: string) => void;
|
|
412
361
|
}
|
|
413
362
|
declare function createSolveCacheSingleFlight(options?: SolveCacheSingleFlightOptions): SolveCacheSingleFlight;
|
|
414
363
|
|
|
415
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Facade over the warm-client cache, definition-byte cache, single-flight
|
|
366
|
+
* coalescing, and the solve pipeline — the primitives an app route would
|
|
367
|
+
* otherwise wire up by hand.
|
|
368
|
+
*
|
|
369
|
+
* Doesn't read env itself: `@selvajs/server` owns `resolveComputeLimits` and
|
|
370
|
+
* depends on `@selvajs/solve`, so calling it from here would be a circular
|
|
371
|
+
* import. Callers resolve their own limits and pass in the fields below.
|
|
372
|
+
*
|
|
373
|
+
* Stays out of auth, DB reads, share tokens, rate limiting, metrics, and
|
|
374
|
+
* compute-server selection — all app policy, supplied per-call via
|
|
375
|
+
* `SolveEngineSolveArgs.server`.
|
|
376
|
+
*/
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs.
|
|
380
|
+
* Pass the resolved object through as-is — extra fields are ignored.
|
|
381
|
+
*/
|
|
382
|
+
interface SolveEngineLimits {
|
|
383
|
+
maxSolveDurationMs: number;
|
|
384
|
+
computeResponseMaxBytes: number;
|
|
385
|
+
computeReuseDefinitionCache: boolean;
|
|
386
|
+
computeServerCachesolve: boolean;
|
|
387
|
+
computeCacheErroredSolves: boolean;
|
|
388
|
+
computeMaxQueueDepth: number;
|
|
389
|
+
computeQueueWaitMs: number;
|
|
390
|
+
computeDefinitionCacheBytes: number;
|
|
391
|
+
computeSolveCacheBytes: number;
|
|
392
|
+
}
|
|
393
|
+
interface SolveEngineOptions {
|
|
394
|
+
limits: SolveEngineLimits;
|
|
395
|
+
logger?: ILogger;
|
|
396
|
+
/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */
|
|
397
|
+
maxWarmComputeServers?: number;
|
|
398
|
+
/** Concise cache/timing logs when truthy; `'verbose'` also dumps full lib-level requests/responses. Default off. */
|
|
399
|
+
debug?: ClientCacheDebug;
|
|
400
|
+
onDebugLog?: (message: string) => void;
|
|
401
|
+
/** Fired when a caller joins an already-in-flight solve instead of running its own. */
|
|
402
|
+
onSolveCoalesced?: (key: string) => void;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Accepts every form `runSolvePipeline` does, plus `{versionId, load}` sugar
|
|
406
|
+
* that builds (and caches) a `ByteCacheRef` internally. A `ByteCacheRef`
|
|
407
|
+
* obtained ahead of time from `engine.definitionRef()` (e.g. to read bytes for
|
|
408
|
+
* schema extraction before solving) is passed through as-is — detected by its
|
|
409
|
+
* `outcome` field, which a plain `DefinitionRef` never has.
|
|
410
|
+
*/
|
|
411
|
+
type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef | {
|
|
412
|
+
versionId: string;
|
|
413
|
+
load: () => Promise<Uint8Array>;
|
|
414
|
+
};
|
|
415
|
+
interface SolveEngineSolveArgs {
|
|
416
|
+
server: ResolvedServer;
|
|
417
|
+
definitionSource: SolveEngineDefinitionSource;
|
|
418
|
+
/**
|
|
419
|
+
* Coalesce-key identity for a raw `Uint8Array`/`string` source, which has
|
|
420
|
+
* no natural identity the way a `DefinitionRef`'s `.key` does. Required in
|
|
421
|
+
* that case — `solve()` throws rather than silently hashing the bytes.
|
|
422
|
+
* Ignored for the other source forms.
|
|
423
|
+
*/
|
|
424
|
+
definitionKey?: string;
|
|
425
|
+
inputs: PipelineInput[];
|
|
426
|
+
values: Record<string, unknown>;
|
|
427
|
+
signal: AbortSignal;
|
|
428
|
+
acceptEncoding?: string;
|
|
429
|
+
/** Stamped as `X-Selva-Definition` on this solve's client. */
|
|
430
|
+
definitionGuid?: string;
|
|
431
|
+
loadStartMs?: number;
|
|
432
|
+
defLoadMs?: number;
|
|
433
|
+
prepMarks?: [string, number][];
|
|
434
|
+
}
|
|
435
|
+
interface FrameworkAgnosticResponse {
|
|
436
|
+
status: number;
|
|
437
|
+
headers: Record<string, string>;
|
|
438
|
+
body: string | Uint8Array;
|
|
439
|
+
}
|
|
440
|
+
interface SolveEngineStats {
|
|
441
|
+
client: SolveCacheStats;
|
|
442
|
+
definitionBytes: ByteCacheStats;
|
|
443
|
+
coalescing: {
|
|
444
|
+
inFlight: number;
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
declare class SolveEngine {
|
|
448
|
+
private readonly limits;
|
|
449
|
+
private readonly clientCache;
|
|
450
|
+
private readonly byteCache;
|
|
451
|
+
private readonly singleFlight;
|
|
452
|
+
constructor(options: SolveEngineOptions);
|
|
453
|
+
getClient(server: ResolvedServer, opts?: {
|
|
454
|
+
definitionGuid?: string;
|
|
455
|
+
}): Promise<CachedClient>;
|
|
456
|
+
definitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;
|
|
457
|
+
evictServer(id: string): void;
|
|
458
|
+
clearSolveCaches(): void;
|
|
459
|
+
stats(): SolveEngineStats;
|
|
460
|
+
solve(args: SolveEngineSolveArgs): Promise<SolveOutcome>;
|
|
461
|
+
toResponse(outcome: SolveOutcome, opts?: {
|
|
462
|
+
onError?: (status: number, body: {
|
|
463
|
+
message: string;
|
|
464
|
+
retryAfter?: number;
|
|
465
|
+
}) => never;
|
|
466
|
+
}): FrameworkAgnosticResponse;
|
|
467
|
+
toWebResponse(outcome: SolveOutcome): Response;
|
|
468
|
+
private resolveDefinitionSource;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export { type ByteCacheRef, type ByteCacheStats, type ByteRefOutcome, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type CachedClient, type ClientCache, type ClientCacheConfig, type DefinitionByteCache, type FrameworkAgnosticResponse, type PipelineInput, type ResolvedServer, type ServerIdentity, type SolveCacheSingleFlight, type SolveCacheSingleFlightOptions, type SolveCacheStats, SolveEngine, type SolveEngineDefinitionSource, type SolveEngineLimits, type SolveEngineOptions, type SolveEngineSolveArgs, type SolveEngineStats, type SolveEnvelope, type SolveOutcome, type SolvePhaseMetrics, type SolvePipelineArgs, adaptEnvelopeToEncoding, buildSolveInputTree, createClientCache, createDefinitionByteCache, createSolveCacheSingleFlight, runSolvePipeline, serverIdentity, transformInputParameter };
|
package/dist/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ErrorCodes as R,RhinoComputeError as I,TreeBuilder as O}from"@selvajs/compute";import{gzip as A,gunzipSync as _}from"zlib";import{promisify as L}from"util";import{processInput as z}from"@selvajs/compute";function T(e,r){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:e.minimum??null,maximum:e.maximum??null,atLeast:1,atMost:1,stepSize:e.stepSize,default:r??e.default},s=z(t),o=r??e.default;return s.default==null&&o!=null&&(Array.isArray(o)||s.paramType==="ValueList"&&typeof o!="object")&&(s.default=o),s}var $=L(A),P=1,E="X-Selva-Compute-Version";function B(e,r){return O.fromInputParams(e.filter(t=>t.paramType).map(t=>T(t,r[t.id])))}async function D(e){let{client:r,signal:t}=e,s=performance.now(),o=e.inputTree??B(e.inputs,e.values),u=performance.now()-s,i=r.solveMeta.seq,l=r.rhinoTiming.seq,n,a=performance.now();try{n=await r.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let b=performance.now()-a;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:b}:{kind:"timeout",durationMs:b,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof I){if(v.code===R.QUEUE_FULL)return{kind:"shed",durationMs:b,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===R.QUEUE_TIMEOUT)return{kind:"shed",durationMs:b,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:b,error:v}}let c=performance.now()-a,h=performance.now(),d;try{d=JSON.stringify(n)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,y=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,f=null,g=0;if(y){let v=performance.now();f=await $(Buffer.from(d)),g=performance.now()-v}let S=performance.now()-e.loadStartMs,M=r.solveMeta.seq===i+1?r.solveMeta.last:null,k=M&&!M.fromCache&&r.rhinoTiming.seq===l+1?r.rhinoTiming.last:null,C={"Content-Type":"application/json","Server-Timing":U({defLoadMs:e.defLoadMs,treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,rhino:k,settle:M,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[E]:String(P),Vary:"Accept-Encoding"},w={treeBuildMs:u,solveMs:c,serializeMs:p,gzipMs:g,serverTotalMs:S,serializedBytes:d.length,compressedBytes:f?.byteLength??null},x;return f?(C["Content-Encoding"]="gzip",C["Content-Length"]=String(f.byteLength),x={body:new Uint8Array(f),encoding:"gzip",headers:C,result:n,metrics:w}):(C["Content-Length"]=String(Buffer.byteLength(d)),x={body:d,headers:C,result:n,metrics:w}),{kind:"ok",envelope:x,solveMs:c,errorCount:n.errors?.length??0,warningCount:n.warnings?.length??0}}function F(e,r){let t=/\bgzip\b/i.test(r);if(e.encoding==="gzip"&&!t){let s=_(e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(s)),{body:s,headers:o}}return{body:e.body,headers:e.headers}}function U(e){let r=`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)}`,t=e.rhino;if(t){let u=t.decode+t.solve+t.encode,i=Math.max(0,e.solveMs-u);r+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${i.toFixed(1)}`}for(let[u,i]of e.prepMarks)r+=`, p_${u};dur=${i.toFixed(1)}`;let s=e.settle;s&&(r+=`, selva_cache;dur=${s.fromCache?1:0}`,s.definitionReuploaded!==void 0&&(r+=`, def_reupload;dur=${s.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let u=o.loaded?o.fromCache?"hit":"miss":"skipped";r+=`, def_bytes;desc=${u}`}return r}import{GrasshopperClient as q,enableDebugLogging as G}from"@selvajs/compute";function V(e){return e.id}var N=16;function j(e){let r=e.maxCachedClients??N,t=new Map,s=new Map;e.debugVerbose&&G();let o=i=>{e.debug&&e.onDebugLog?.(i)};async function u(i,l){let n={last:null,seq:0},a={last:null,seq:0},c={serverUrl:i.serverUrl,apiKey:i.apiKey,debug:e.debugVerbose,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:p=>{let m=p.decode??0,y=p.solve??0,f=p.encode??0;if(n.seq+=1,n.last={decode:m,solve:y,encode:f},!e.debug)return;let g=m<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",S=e.cachesolve&&y<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${m}ms solve=${y}ms encode=${f}ms | definition-cache: ${g} | solve-cache (cachesolve): ${S}`)}};l&&(c.headers={"X-Selva-Definition":l});let h=await q.create(c),d=h.createScheduler({mode:"queue",maxConcurrent:e.maxConcurrentSolves,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(p,m)=>{if(a.seq+=1,m.status==="success"&&(a.last={fromCache:m.fromCache===!0,definitionReuploaded:m.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${m.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(m.durationMs)}ms)`),m.fromCache||o(m.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":m.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!m.fromCache))){let y=m.response?.errors,f=Array.isArray(y)?y.length:0;if(f>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${f} GH error(s); ${g}. First: ${JSON.stringify(y[0])}`)}}}});return{client:h,scheduler:d,rhinoTiming:n,solveMeta:a}}return{async getClient(i,l){let n=i.id,a=t.get(n);if(a)return t.delete(n),t.set(n,a),a;let c=s.get(n);if(c)return c;o(`[Compute/client-cache] miss \u2014 building warm client for server ${n}`);let h=(async()=>{let d=await u(i,l?.definitionGuid);if(t.size>=r){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${r})`),t.get(p)?.scheduler.dispose(),t.delete(p))}return t.set(n,d),d})().finally(()=>s.delete(n));return s.set(n,h),h},evict(i){let l=i,n=t.get(l);n&&(o(`[Compute/client-cache] evicted warm client for server ${l} (config change)`),n.scheduler.dispose(),t.delete(l))},solveCacheStats(){let i={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let l of t.values()){let n=l.scheduler.cacheStats();i.entries+=n.entries,i.bytes+=n.bytes,i.hits+=n.hits,i.misses+=n.misses,i.evictions+=n.evictions}return i},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let i of t.values())i.scheduler.dispose();t.clear()}}}function H(e){let r=Math.max(0,Math.floor(e)),t=new Map,s=0,o={hits:0,misses:0,evictions:0};function u(n,a){t.delete(n),t.set(n,a)}function i(){for(;s>r&&t.size>0;){let n=t.keys().next().value,a=t.get(n);t.delete(n),s-=a.bytes.byteLength,o.evictions+=1}}function l(n,a){if(r===0||a.byteLength>r)return;let c=t.get(n);c&&(s-=c.bytes.byteLength),t.set(n,{bytes:a}),s+=a.byteLength,i()}return{getOrLoad(n,a){let c={loaded:!1,fromCache:!1};return{key:n,outcome:c,load:async()=>{let h=t.get(n);if(h)return u(n,h),o.hits+=1,c.loaded=!0,c.fromCache=!0,h.bytes;o.misses+=1,c.loaded=!0,c.fromCache=!1;let d=await a();return l(n,d),d}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:s}},clear(){t.clear(),s=0}}}function W(e={}){let r=new Map;return{run(t,s,o){let u=r.get(t);if(u)return e.onJoin?.(t),u.notifyOwner?.(),u.promise;let i={notifyOwner:o,promise:void 0},l=(async()=>s())().finally(()=>{i.notifyOwner=void 0,r.get(t)===i&&r.delete(t)});return i.promise=l,r.set(t,i),l},inFlight(){return r.size}}}export{P as COMPUTE_CONTRACT_VERSION,E as COMPUTE_VERSION_HEADER,F as adaptEnvelopeToEncoding,B as buildSolveInputTree,j as createClientCache,H as createDefinitionByteCache,W as createSolveCacheSingleFlight,D as runSolvePipeline,V as serverIdentity,T as transformInputParameter};
|
|
1
|
+
import{ErrorCodes as I,RhinoComputeError as U,TreeBuilder as q}from"@selvajs/compute";import{gzip as W,gunzipSync as G}from"zlib";import{promisify as N}from"util";import{processInput as $}from"@selvajs/compute";function A(e,n){let t={id:e.id,name:e.nickname,nickname:e.nickname||null,description:e.description||"",paramType:e.paramType==="dynamicValueList"?"valueList":e.paramType,treeAccess:e.inputStructure==="tree",minimum:e.minimum??null,maximum:e.maximum??null,atLeast:1,atMost:1,stepSize:e.stepSize,default:n??e.default},r=$(t),o=n??e.default;return r.default==null&&o!=null&&(Array.isArray(o)||r.paramType==="ValueList"&&typeof o!="object")&&(r.default=o),r}var j=N(W),F=1,_="X-Selva-Compute-Version";function E(e,n){return q.fromInputParams(e.filter(t=>t.paramType).map(t=>A(t,n[t.id])))}async function B(e){let{client:n,signal:t}=e,r=performance.now(),o=e.inputTree??E(e.inputs,e.values),c=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,l=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let x=performance.now()-l;if(v instanceof Error&&v.name==="AbortError")return t.aborted?{kind:"client_abort",durationMs:x}:{kind:"timeout",durationMs:x,message:`Solve exceeded the ${Math.round(e.maxSolveDurationMs/1e3)}s deadline.`};if(v instanceof U){if(v.code===I.QUEUE_FULL)return{kind:"shed",durationMs:x,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===I.QUEUE_TIMEOUT)return{kind:"shed",durationMs:x,reason:"queue_timeout",retryAfterSeconds:1,message:"Compute request waited too long in the queue. Retry shortly."}}return{kind:"compute_error",durationMs:x,error:v}}let d=performance.now()-l,h=performance.now(),a;try{a=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(a.length>e.responseMaxBytes)return{kind:"too_large"};let p=performance.now()-h,M=/\bgzip\b/i.test(e.acceptEncoding)&&a.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(a)),S=performance.now()-v}let y=performance.now()-e.loadStartMs,u=n.solveMeta.seq===s+1?n.solveMeta.last:null,f=u&&!u.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,g={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,rhino:f,settle:u,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[_]:String(F),Vary:"Accept-Encoding"},R={treeBuildMs:c,solveMs:d,serializeMs:p,gzipMs:S,serverTotalMs:y,serializedBytes:a.length,compressedBytes:C?.byteLength??null},D;return C?(g["Content-Encoding"]="gzip",g["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:g,result:i,metrics:R}):(g["Content-Length"]=String(Buffer.byteLength(a)),D={body:a,headers:g,result:i,metrics:R}),{kind:"ok",envelope:D,solveMs:d,errorCount:i.errors?.length??0,warningCount:i.warnings?.length??0}}function T(e,n){let t=/\bgzip\b/i.test(n);if(e.encoding==="gzip"&&!t){let r=G(e.body instanceof Uint8Array?e.body:Buffer.from(e.body)).toString("utf8"),o={...e.headers};return delete o["Content-Encoding"],o["Content-Length"]=String(Buffer.byteLength(r)),{body:r,headers:o}}return{body:e.body,headers:e.headers}}function V(e){let n=`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)}`,t=e.rhino;if(t){let c=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-c);n+=`, rhino_decode;dur=${t.decode.toFixed(1)}, rhino_solve;dur=${t.solve.toFixed(1)}, rhino_encode;dur=${t.encode.toFixed(1)}, compute_link;dur=${s.toFixed(1)}`}for(let[c,s]of e.prepMarks)n+=`, p_${c};dur=${s.toFixed(1)}`;let r=e.settle;r&&(n+=`, selva_cache;dur=${r.fromCache?1:0}`,r.definitionReuploaded!==void 0&&(n+=`, def_reupload;dur=${r.definitionReuploaded?1:0}`));let o=e.byteRefOutcome;if(o){let c=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${c}`}return n}import{GrasshopperClient as K,enableDebugLogging as Q}from"@selvajs/compute";function H(e){return e.id}var J=16,z=1,X=300*1e3;function k(e){let n=e.maxWarmComputeServers??J,t=new Map,r=new Map;e.debug==="verbose"&&Q();let o=s=>{e.debug&&e.onDebugLog?.(s)};async function c(s,m){let i={last:null,seq:0},l={last:null,seq:0},d=e.debug==="verbose",h={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:d,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:y=>{let u=y.decode??0,f=y.solve??0,b=y.encode??0;if(i.seq+=1,i.last={decode:u,solve:f,encode:b},!e.debug)return;let g=u<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",R=e.cachesolve&&f<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${u}ms solve=${f}ms encode=${b}ms | definition-cache: ${g} | solve-cache (cachesolve): ${R}`)}};m&&(h.headers={"X-Selva-Definition":m});let a=await K.create(h),p=z,w=0,M=!1;async function C(y){if(!M){M=!0;try{let u=await Promise.resolve(a.serverStats?.getActiveChildren({initialize:!1})).catch(()=>null);w=Date.now();let f=!u||u<=0?z:u;if(f===p)return;o(`[Compute/client-cache] concurrency ${p} \u2192 ${f} (server child count)`),p=f,y?.setMaxConcurrent(f)}finally{M=!1}}}await C();let S=a.createScheduler({mode:"queue",maxConcurrent:p,maxQueueDepth:e.maxQueueDepth||void 0,queueWaitMs:e.queueWaitMs||void 0,timeoutMs:e.maxSolveDurationMs,cache:e.responseCacheMaxBytes>0?{maxBytes:e.responseCacheMaxBytes}:!1,reuseServerDefinitionCache:e.reuseServerDefinitionCache,onSettle:(y,u)=>{if(l.seq+=1,u.status==="success"&&(u.fromCache!==!0&&Date.now()-w>=X&&C(S),l.last={fromCache:u.fromCache===!0,definitionReuploaded:u.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${u.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(u.durationMs)}ms)`),u.fromCache||o(u.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":u.definitionReuploaded===!1?"[Compute/def-cache] HIT \u2014 definition reused from server cache (no upload)":"[Compute/def-cache] n/a \u2014 reuse disabled or non-reusable definition"),!u.fromCache))){let f=u.response?.errors,b=Array.isArray(f)?f.length:0;if(b>0){let g=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${g}. First: ${JSON.stringify(f[0])}`)}}}});return{client:a,scheduler:S,rhinoTiming:i,solveMeta:l}}return{async getClient(s,m){let i=s.id,l=t.get(i);if(l)return t.delete(i),t.set(i,l),l;let d=r.get(i);if(d)return d;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let h=(async()=>{let a=await c(s,m?.definitionGuid);if(t.size>=n){let p=t.keys().next().value;p!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${p} (cap ${n})`),t.get(p)?.scheduler.dispose(),t.delete(p))}return t.set(i,a),a})().finally(()=>r.delete(i));return r.set(i,h),h},evict(s){let m=s,i=t.get(m);i&&(o(`[Compute/client-cache] evicted warm client for server ${m} (config change)`),i.scheduler.dispose(),t.delete(m))},solveCacheStats(){let s={warmClients:t.size,entries:0,bytes:0,hits:0,misses:0,evictions:0};for(let m of t.values()){let i=m.scheduler.cacheStats();s.entries+=i.entries,s.bytes+=i.bytes,s.hits+=i.hits,s.misses+=i.misses,s.evictions+=i.evictions}return s},clearSolveCaches(){o(`[Compute/client-cache] cleared solve caches on ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.clearCache()},disposeAll(){o(`[Compute/client-cache] disposing all ${t.size} warm client(s)`);for(let s of t.values())s.scheduler.dispose();t.clear()}}}function P(e){let n=Math.max(0,Math.floor(e)),t=new Map,r=0,o={hits:0,misses:0,evictions:0};function c(i,l){t.delete(i),t.set(i,l)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,l=t.get(i);t.delete(i),r-=l.bytes.byteLength,o.evictions+=1}}function m(i,l){if(n===0||l.byteLength>n)return;let d=t.get(i);d&&(r-=d.bytes.byteLength),t.set(i,{bytes:l}),r+=l.byteLength,s()}return{getOrLoad(i,l){let d={loaded:!1,fromCache:!1};return{key:i,outcome:d,load:async()=>{let h=t.get(i);if(h)return c(i,h),o.hits+=1,d.loaded=!0,d.fromCache=!0,h.bytes;o.misses+=1,d.loaded=!0,d.fromCache=!1;let a=await l();return m(i,a),a}}},stats(){return{hits:o.hits,misses:o.misses,evictions:o.evictions,entries:t.size,bytes:r}},clear(){t.clear(),r=0}}}function O(e={}){let n=new Map;return{run(t,r,o){let c=n.get(t);if(c)return e.onJoin?.(t),c.notifyOwner?.(),c.promise;let s={notifyOwner:o,promise:void 0},m=(async()=>r())().finally(()=>{s.notifyOwner=void 0,n.get(t)===s&&n.delete(t)});return s.promise=m,n.set(t,s),m},inFlight(){return n.size}}}import{isDefinitionRef as Y,stableStringify as Z}from"@selvajs/compute";import{NoopLogger as ee}from"@selvajs/platform";function te(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&"outcome"in e}function ne(e){return typeof e=="object"&&e!==null&&!(e instanceof Uint8Array)&&!("outcome"in e)&&!("key"in e)&&"versionId"in e}var L=class{limits;clientCache;byteCache;singleFlight;constructor(n){this.limits=n.limits;let t=n.logger??new ee;this.clientCache=k({maxSolveDurationMs:this.limits.maxSolveDurationMs,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:n.debug??!1,maxWarmComputeServers:n.maxWarmComputeServers,onDebugLog:n.onDebugLog??(r=>t.debug(r,{component:"Compute/client-cache"}))}),this.byteCache=P(this.limits.computeDefinitionCacheBytes),this.singleFlight=O({onJoin:n.onSolveCoalesced})}getClient(n,t){return this.clientCache.getClient(n,t)}definitionRef(n,t){return this.byteCache.getOrLoad(n,t)}evictServer(n){this.clientCache.evict(n)}clearSolveCaches(){this.clientCache.clearSolveCaches()}stats(){return{client:this.clientCache.solveCacheStats(),definitionBytes:this.byteCache.stats(),coalescing:{inFlight:this.singleFlight.inFlight()}}}async solve(n){let{definitionSource:t,byteRefOutcome:r,identity:o}=this.resolveDefinitionSource(n),c=await this.clientCache.getClient(n.server,{definitionGuid:n.definitionGuid}),s=E(n.inputs,n.values),m=`${o}:${n.server.id}:${Z(s)}`,i=new AbortController,l=!1,d=()=>{l||i.abort()};n.signal.addEventListener("abort",d,{once:!0});let h=n.acceptEncoding??"",a;try{a=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:c,responseMaxBytes:this.limits.computeResponseMaxBytes,maxSolveDurationMs:this.limits.maxSolveDurationMs,acceptEncoding:h,signal:i.signal,loadStartMs:n.loadStartMs??performance.now(),defLoadMs:n.defLoadMs??0,prepMarks:n.prepMarks}),()=>{l=!0})}finally{n.signal.removeEventListener("abort",d)}if(a.kind!=="ok")return a;let p=T(a.envelope,h);return{...a,envelope:{...a.envelope,body:p.body,headers:p.headers}}}toResponse(n,t){let r=(o,c)=>{t?.onError&&t.onError(o,c);let s={"Content-Type":"application/json"};return c.retryAfter!==void 0&&(s["Retry-After"]=String(c.retryAfter)),{status:o,headers:s,body:JSON.stringify(c)}};switch(n.kind){case"ok":return{status:200,headers:n.envelope.headers,body:n.envelope.body};case"timeout":return r(504,{message:n.message});case"client_abort":return r(499,{message:"Client closed request"});case"too_large":return r(413,{message:"Solve result is too large to return. This usually means a file output exceeds the supported size."});case"shed":return r(503,{message:n.message,retryAfter:n.retryAfterSeconds});case"compute_error":throw n.error}}toWebResponse(n){let t=this.toResponse(n);return new Response(typeof t.body=="string"?t.body:new Uint8Array(t.body),{status:t.status,headers:t.headers})}resolveDefinitionSource(n){let t=n.definitionSource;if(ne(t)){let r=this.byteCache.getOrLoad(t.versionId,t.load);return{definitionSource:r,byteRefOutcome:r.outcome,identity:r.key}}if(te(t))return{definitionSource:t,byteRefOutcome:t.outcome,identity:t.key};if(Y(t))return{definitionSource:t,identity:t.key};if(!n.definitionKey)throw new Error("SolveEngine.solve(): definitionKey is required when definitionSource is raw bytes or a string.");return{definitionSource:t,identity:n.definitionKey}}};import{isDefinitionRef as Te}from"@selvajs/compute";export{F as COMPUTE_CONTRACT_VERSION,_ as COMPUTE_VERSION_HEADER,L as SolveEngine,T as adaptEnvelopeToEncoding,E as buildSolveInputTree,k as createClientCache,P as createDefinitionByteCache,O as createSolveCacheSingleFlight,Te as isDefinitionRef,B as runSolvePipeline,H as serverIdentity,A as transformInputParameter};
|
|
2
2
|
//# sourceMappingURL=server.js.map
|