@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/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
- * The scheduler runs up to `maxConcurrentSolves` at once, so `last` alone
27
- * can't be trusted per-request: callers snapshot `seq` before their solve
28
- * and only attribute `last` to themselves if exactly one write happened
29
- * since (see the guard in `runSolvePipeline`), dropping it otherwise
30
- * rather than risk misattributing another request's timing.
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
- /** Also cache solves that reported GH errors (only meaningful with `cachesolve`). */
65
+ /** Only meaningful with `cachesolve`. */
60
66
  cacheerroredsolves: boolean;
61
- /** Reference large definitions by server cache key (pointer) instead of re-uploading. */
67
+ /** Reference large definitions by server cache key instead of re-uploading. */
62
68
  reuseServerDefinitionCache: boolean;
63
69
  /**
64
- * Max in-flight solves per compute server (scheduler `maxConcurrent`; excess
65
- * FIFO-queues). Size to the server's `compute.geometry` child count
66
- * (`ComputeLimits.computeMaxConcurrentSolves`).
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
- * Backpressure — max ms a solve may sit queued before executing; `0` = no
77
- * deadline (`ComputeLimits.computeQueueWaitMs`). A too-long wait sheds with
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, evicted LRU alongside
83
- * its entry-count cap. Applies per warm client — total worst-case heap is this
84
- * × `maxCachedClients`. `0` disables the cache entirely
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
- /** Concise cache/timing logs. When false, `onDebugLog` is never invoked. */
89
- debug: boolean;
90
- /** VERBOSE lib-level logging (full solve request/response incl. geometry). */
91
- debugVerbose: boolean;
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
- * framework-free half of a solve and returns a discriminated {@link SolveOutcome}
204
- * instead of throwing for expected failures. Nothing here touches auth, the
205
- * database, share tokens, rate limits, or metric sinks — those stay app policy
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
- * The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from
220
- * the definition-byte cache) whose bytes the scheduler materializes ONLY when
221
- * an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves
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}. When present
235
- * the pipeline skips its own build and solves this exact object.
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
- * Needed whenever the caller coalesces concurrent solves: a single-flight key
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
- /** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */
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
- * Request abort signal, forwarded to the scheduler so a client disconnect
252
- * cancels the upstream compute call. Its `aborted` flag also disambiguates a
253
- * client disconnect from the scheduler's own deadline firing.
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
- * Discriminated result. `ok` carries the envelope; every other variant names an
292
- * expected failure the transport maps to a status code (the app route maps
293
- * timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;
294
- * `compute_error` re-surfaces the original error for the generic 500/503 path).
295
- * `durationMs` on the error variants is the solve wall time up to the failure,
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}. That
343
- * envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a
344
- * later waiter with a different `Accept-Encoding` would otherwise get a body
345
- * labelled with the wrong encoding (`Vary` can't help — this is one object
346
- * shared across waiters, not a cache lookup). Encoding is deliberately not part
347
- * of the coalesce key, so mixed clients still coalesce; this adapts the shared
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
- * Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`
361
- * and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`
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
- * Deliberately NOT conditional on a result cache being configured: the dogpile is
376
- * worst precisely when nothing else is caching (N concurrent identical solves each
377
- * paying a full Rhino round trip — the scheduler has no in-flight coalescing of its
378
- * own, and Rhino's `cachesolve` still costs a round trip per repeat). Gating this on
379
- * cache configuration, as an earlier version did, turned it off in exactly the
380
- * deployments most exposed to a cold-key stampede (hot public definition + a deploy).
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
- * In-process only, sitting above `ISolveResultCache` (per app instance). A
383
- * cross-instance lease (Redis `SET NX`) is a later backend capability.
338
+ * Per app instance, above `ISolveResultCache`. No cross-instance lease (Redis
339
+ * `SET NX`) yet.
384
340
  *
385
- * Correctness note: the shared promise resolves to ONE value for all waiters, so
386
- * the wrapped work must return an immutable / independently-serializable result
387
- * (the solve pipeline's envelope is safe to share by reference here because the
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
- * Run `work` under `key`, coalescing concurrent identical calls: the first
393
- * caller executes, overlapping callers for the same key await the same promise
394
- * and result, and the key is freed as soon as it settles.
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 OWNER's call each time another caller joins its
397
- * flight — ownership changes what the owner's abort signal means (a solo run
398
- * may cancel on its own client's disconnect, a shared one may not, or it would
399
- * 499 every waiter). Never fires for a joining caller, and never after `work`
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
- export { type ByteCacheRef, type ByteCacheStats, type ByteRefOutcome, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type CachedClient, type ClientCache, type ClientCacheConfig, type DefinitionByteCache, type PipelineInput, type ResolvedServer, type ServerIdentity, type SolveCacheSingleFlight, type SolveCacheSingleFlightOptions, type SolveCacheStats, type SolveEnvelope, type SolveOutcome, type SolvePhaseMetrics, type SolvePipelineArgs, adaptEnvelopeToEncoding, buildSolveInputTree, createClientCache, createDefinitionByteCache, createSolveCacheSingleFlight, runSolvePipeline, serverIdentity, transformInputParameter };
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