@selvajs/solve 0.2.0-beta.0
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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/client.cjs +2 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +182 -0
- package/dist/client.d.ts +182 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/server.cjs +2 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +407 -0
- package/dist/server.d.ts +407 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/dist/shared.cjs +1 -0
- package/dist/shared.cjs.map +1 -0
- package/dist/shared.d.cts +3 -0
- package/dist/shared.d.ts +3 -0
- package/dist/shared.js +1 -0
- package/dist/shared.js.map +1 -0
- package/dist/solve-fn-DGzPCsDu.d.cts +22 -0
- package/dist/solve-fn-DGzPCsDu.d.ts +22 -0
- package/dist/solve-input-CqYtetTA.d.cts +14 -0
- package/dist/solve-input-CqYtetTA.d.ts +14 -0
- package/package.json +94 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { GrasshopperClient, SolveScheduler, GrasshopperComputeResponse, SolveDefinition, DataTree, InputParam } from '@selvajs/compute';
|
|
2
|
+
import { SchemaInput } from '@selvajs/schemas';
|
|
3
|
+
export { S as SolveInput } from './solve-input-CqYtetTA.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opaque identity of a compute server — construct only via `serverIdentity()`.
|
|
7
|
+
* Kept opaque so "a server is now a pool of URLs behind one id" stays additive.
|
|
8
|
+
*/
|
|
9
|
+
type ServerIdentity = string & {
|
|
10
|
+
readonly __brand: 'ServerIdentity';
|
|
11
|
+
};
|
|
12
|
+
/** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */
|
|
13
|
+
interface ResolvedServer {
|
|
14
|
+
id: string;
|
|
15
|
+
serverUrl: string;
|
|
16
|
+
/** Sent as `RhinoComputeKey`. */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Derive the opaque cache identity from a resolved server. Identity is the `id`. */
|
|
20
|
+
declare function serverIdentity(server: Pick<ResolvedServer, 'id'>): ServerIdentity;
|
|
21
|
+
interface CachedClient {
|
|
22
|
+
client: GrasshopperClient;
|
|
23
|
+
scheduler: SolveScheduler;
|
|
24
|
+
/**
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
rhinoTiming: {
|
|
33
|
+
last: {
|
|
34
|
+
decode: number;
|
|
35
|
+
solve: number;
|
|
36
|
+
encode: number;
|
|
37
|
+
} | null;
|
|
38
|
+
seq: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Same snapshot-and-attribute pattern as `rhinoTiming`, for the scheduler's
|
|
42
|
+
* onSettle cache verdict. `seq` increments on every settle (success or
|
|
43
|
+
* error); `last` is written only on success.
|
|
44
|
+
*/
|
|
45
|
+
solveMeta: {
|
|
46
|
+
last: {
|
|
47
|
+
fromCache: boolean;
|
|
48
|
+
definitionReuploaded?: boolean;
|
|
49
|
+
} | null;
|
|
50
|
+
seq: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Config injected by the consuming app, so this module stays env-agnostic and testable. */
|
|
54
|
+
interface ClientCacheConfig {
|
|
55
|
+
/** Per-solve timeout forwarded to the scheduler (`ComputeLimits.maxSolveDurationMs`). */
|
|
56
|
+
maxSolveDurationMs: number;
|
|
57
|
+
/** Ask Rhino.Compute to cache solve results and return them on identical repeats. */
|
|
58
|
+
cachesolve: boolean;
|
|
59
|
+
/** Also cache solves that reported GH errors (only meaningful with `cachesolve`). */
|
|
60
|
+
cacheerroredsolves: boolean;
|
|
61
|
+
/** Reference large definitions by server cache key (pointer) instead of re-uploading. */
|
|
62
|
+
reuseServerDefinitionCache: boolean;
|
|
63
|
+
/**
|
|
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`.
|
|
73
|
+
*/
|
|
74
|
+
maxQueueDepth: number;
|
|
75
|
+
/**
|
|
76
|
+
* Backpressure — max ms a solve may sit queued before executing; `0` = no
|
|
77
|
+
* deadline (`ComputeLimits.computeQueueWaitMs`). A too-long wait sheds with
|
|
78
|
+
* `QUEUE_TIMEOUT`.
|
|
79
|
+
*/
|
|
80
|
+
queueWaitMs: number;
|
|
81
|
+
/**
|
|
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`).
|
|
86
|
+
*/
|
|
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`). */
|
|
95
|
+
onDebugLog?: (message: string) => void;
|
|
96
|
+
}
|
|
97
|
+
interface ClientCache {
|
|
98
|
+
/**
|
|
99
|
+
* Get (or create) the warm client + scheduler for a resolved compute server,
|
|
100
|
+
* keyed by its `id`. `definitionGuid`, when present, is stamped as the
|
|
101
|
+
* `X-Selva-Definition` header on this client's outbound solve/IO requests —
|
|
102
|
+
* inert routing/telemetry metadata until a pool router exists.
|
|
103
|
+
*/
|
|
104
|
+
getClient(server: ResolvedServer, opts?: {
|
|
105
|
+
definitionGuid?: string;
|
|
106
|
+
}): Promise<CachedClient>;
|
|
107
|
+
/** Dispose and drop the warm client for `id`, so the next request rebuilds against fresh connection details. */
|
|
108
|
+
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
|
+
*/
|
|
114
|
+
solveCacheStats(): SolveCacheStats;
|
|
115
|
+
/** Dispose every warm client. Test seam / shutdown hook. */
|
|
116
|
+
disposeAll(): void;
|
|
117
|
+
}
|
|
118
|
+
/** Aggregate solve-cache counters across the warm clients (see `solveCacheStats`). */
|
|
119
|
+
interface SolveCacheStats {
|
|
120
|
+
warmClients: number;
|
|
121
|
+
entries: number;
|
|
122
|
+
bytes: number;
|
|
123
|
+
hits: number;
|
|
124
|
+
misses: number;
|
|
125
|
+
/** Entries dropped under size/byte pressure (not TTL expiry or replacement). */
|
|
126
|
+
evictions: number;
|
|
127
|
+
}
|
|
128
|
+
declare function createClientCache(config: ClientCacheConfig): ClientCache;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* In-process cache of `.gh` definition bytes, keyed by immutable **version id**.
|
|
132
|
+
*
|
|
133
|
+
* Turns an eager `storage.get(fileKey)` on every solve request into a lazy
|
|
134
|
+
* `DefinitionRef.load()`: the scheduler calls `load()` only when an upload is
|
|
135
|
+
* unavoidable (a pointer-known re-solve never touches storage or this cache).
|
|
136
|
+
*
|
|
137
|
+
* The key MUST be the version id, never a `fileKey` — a delete-latest-then-
|
|
138
|
+
* reupload can reuse a `fileKey` for different content, which would silently
|
|
139
|
+
* serve one version's bytes for another with no diagnostic. Callers pass
|
|
140
|
+
* `version.id`, and the returned `DefinitionRef.key` is that same id, so the
|
|
141
|
+
* scheduler's own result/pointer cache keys on the same immutable identity.
|
|
142
|
+
*
|
|
143
|
+
* Eviction is LRU by total byte budget, not entry count — definitions range
|
|
144
|
+
* from tens of KB to hundreds of MB, so a count cap would either pin
|
|
145
|
+
* gigabytes or evict uselessly. `COMPUTE_DEFINITION_CACHE_MB` sets the
|
|
146
|
+
* budget (see `createDefinitionByteCache`). No TTL: version ids are
|
|
147
|
+
* immutable, so a cached entry can never go stale. An entry larger than the
|
|
148
|
+
* whole budget is served but never retained.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* Per-`getOrLoad` outcome, observable after the scheduler has (or hasn't)
|
|
152
|
+
* called `load()` — lets the solve pipeline emit a `def_bytes` Server-Timing
|
|
153
|
+
* verdict (`skipped` / `hit` / `miss`).
|
|
154
|
+
*/
|
|
155
|
+
interface ByteRefOutcome {
|
|
156
|
+
loaded: boolean;
|
|
157
|
+
/** When `loaded`, whether the bytes came from a warm cache entry. */
|
|
158
|
+
fromCache: boolean;
|
|
159
|
+
}
|
|
160
|
+
/** A definition reference shaped for `@selvajs/compute`'s `DefinitionRef`. */
|
|
161
|
+
interface ByteCacheRef {
|
|
162
|
+
key: string;
|
|
163
|
+
load: () => Promise<Uint8Array>;
|
|
164
|
+
outcome: ByteRefOutcome;
|
|
165
|
+
}
|
|
166
|
+
/** Hit/miss/eviction counters for observability (Server-Timing, admin debug). */
|
|
167
|
+
interface ByteCacheStats {
|
|
168
|
+
hits: number;
|
|
169
|
+
misses: number;
|
|
170
|
+
/** Entries dropped by the byte-budget LRU. */
|
|
171
|
+
evictions: number;
|
|
172
|
+
entries: number;
|
|
173
|
+
bytes: number;
|
|
174
|
+
}
|
|
175
|
+
interface DefinitionByteCache {
|
|
176
|
+
/**
|
|
177
|
+
* `DefinitionRef` whose `load()` serves `versionId`'s bytes from the cache
|
|
178
|
+
* when warm, otherwise calls `load` and caches the result. Cheap to build
|
|
179
|
+
* and moves no bytes until `load()` runs.
|
|
180
|
+
*/
|
|
181
|
+
getOrLoad(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;
|
|
182
|
+
stats(): ByteCacheStats;
|
|
183
|
+
/** Test seam / eviction on definition delete if ever needed. */
|
|
184
|
+
clear(): void;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* @param maxBytes total retained-byte budget; `0` (or negative) disables caching
|
|
188
|
+
* entirely — every `load()` calls the loader and nothing is retained.
|
|
189
|
+
*/
|
|
190
|
+
declare function createDefinitionByteCache(maxBytes: number): DefinitionByteCache;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Transport-agnostic solve pipeline: given an already-resolved solve context
|
|
194
|
+
* (`.gh` bytes, input params + user values, a warm `SolveScheduler`), runs the
|
|
195
|
+
* framework-free half of a solve and returns a discriminated {@link SolveOutcome}
|
|
196
|
+
* instead of throwing for expected failures. Nothing here touches auth, the
|
|
197
|
+
* database, share tokens, rate limits, or metric sinks — those stay app policy
|
|
198
|
+
* in the route that calls this.
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/** Compute-response contract version. Bump (and document the change) whenever the envelope's shape changes in a way a consumer could observe. */
|
|
202
|
+
declare const COMPUTE_CONTRACT_VERSION: 1;
|
|
203
|
+
declare const COMPUTE_VERSION_HEADER = "X-Selva-Compute-Version";
|
|
204
|
+
type PipelineInput = SchemaInput & {
|
|
205
|
+
minimum?: number;
|
|
206
|
+
maximum?: number;
|
|
207
|
+
stepSize?: number;
|
|
208
|
+
};
|
|
209
|
+
interface SolvePipelineArgs {
|
|
210
|
+
/**
|
|
211
|
+
* The definition to solve. Either raw `.gh` bytes, or a `DefinitionRef` (from
|
|
212
|
+
* the definition-byte cache) whose bytes the scheduler materializes ONLY when
|
|
213
|
+
* an upload is unavoidable — a pointer-known solve of a `DefinitionRef` moves
|
|
214
|
+
* zero bytes.
|
|
215
|
+
*/
|
|
216
|
+
definitionSource: SolveDefinition;
|
|
217
|
+
/**
|
|
218
|
+
* When `definitionSource` is a byte-cache `DefinitionRef`, its mutable outcome
|
|
219
|
+
* so the pipeline can emit the `def_bytes` Server-Timing verdict. Omit for
|
|
220
|
+
* raw-bytes solves.
|
|
221
|
+
*/
|
|
222
|
+
byteRefOutcome?: ByteRefOutcome;
|
|
223
|
+
/** Persisted input params; only those with a `paramType` are sent to the solve. */
|
|
224
|
+
inputs: PipelineInput[];
|
|
225
|
+
/**
|
|
226
|
+
* A tree the caller already built with {@link buildSolveInputTree}. When present
|
|
227
|
+
* the pipeline skips its own build and solves this exact object.
|
|
228
|
+
*
|
|
229
|
+
* Needed whenever the caller coalesces concurrent solves: a single-flight key
|
|
230
|
+
* derived from raw `{inputs, values}` would split two requests that transform
|
|
231
|
+
* to the same tree, which is the identity the scheduler actually caches on.
|
|
232
|
+
*/
|
|
233
|
+
inputTree?: DataTree[];
|
|
234
|
+
/** User-chosen values keyed by input id; missing keys fall back to the schema default. */
|
|
235
|
+
values: Record<string, unknown>;
|
|
236
|
+
client: CachedClient;
|
|
237
|
+
responseMaxBytes: number;
|
|
238
|
+
/** Used only to phrase the timeout message — the scheduler enforces the deadline itself. */
|
|
239
|
+
maxSolveDurationMs: number;
|
|
240
|
+
/** Client's `Accept-Encoding`; gzip is applied only when it advertises `gzip`. */
|
|
241
|
+
acceptEncoding: string;
|
|
242
|
+
/**
|
|
243
|
+
* Request abort signal, forwarded to the scheduler so a client disconnect
|
|
244
|
+
* cancels the upstream compute call. Its `aborted` flag also disambiguates a
|
|
245
|
+
* client disconnect from the scheduler's own deadline firing.
|
|
246
|
+
*/
|
|
247
|
+
signal: AbortSignal;
|
|
248
|
+
/**
|
|
249
|
+
* Wall-clock origin (`performance.now()` captured at the top of the request)
|
|
250
|
+
* so the pipeline's `load`/`total` phase timings line up with the caller's
|
|
251
|
+
* pre-solve prep.
|
|
252
|
+
*/
|
|
253
|
+
loadStartMs: number;
|
|
254
|
+
/** Pre-solve "load" phase duration the caller already measured (auth + DB + fetch). */
|
|
255
|
+
defLoadMs: number;
|
|
256
|
+
/**
|
|
257
|
+
* Pre-solve prep sub-phase marks (`[label, ms]`), surfaced verbatim as `p_*`
|
|
258
|
+
* Server-Timing entries.
|
|
259
|
+
*/
|
|
260
|
+
prepMarks?: [string, number][];
|
|
261
|
+
}
|
|
262
|
+
/** Phase timings the pipeline measured; the caller uses them for debug logging. */
|
|
263
|
+
interface SolvePhaseMetrics {
|
|
264
|
+
treeBuildMs: number;
|
|
265
|
+
solveMs: number;
|
|
266
|
+
serializeMs: number;
|
|
267
|
+
gzipMs: number;
|
|
268
|
+
serverTotalMs: number;
|
|
269
|
+
serializedBytes: number;
|
|
270
|
+
/** Null when compression was skipped. */
|
|
271
|
+
compressedBytes: number | null;
|
|
272
|
+
}
|
|
273
|
+
/** A ready-to-send response: body + headers + the solve result + phase metrics. */
|
|
274
|
+
interface SolveEnvelope {
|
|
275
|
+
/** A gzip `Uint8Array` when `encoding === 'gzip'`, else the JSON string. */
|
|
276
|
+
body: string | Uint8Array;
|
|
277
|
+
encoding?: 'gzip';
|
|
278
|
+
headers: Record<string, string>;
|
|
279
|
+
result: GrasshopperComputeResponse;
|
|
280
|
+
metrics: SolvePhaseMetrics;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Discriminated result. `ok` carries the envelope; every other variant names an
|
|
284
|
+
* expected failure the transport maps to a status code (the app route maps
|
|
285
|
+
* timeout→504, client_abort→499, too_large→413, shed→503+Retry-After;
|
|
286
|
+
* `compute_error` re-surfaces the original error for the generic 500/503 path).
|
|
287
|
+
* `durationMs` on the error variants is the solve wall time up to the failure,
|
|
288
|
+
* for the metric record.
|
|
289
|
+
*/
|
|
290
|
+
type SolveOutcome = {
|
|
291
|
+
kind: 'ok';
|
|
292
|
+
envelope: SolveEnvelope;
|
|
293
|
+
solveMs: number;
|
|
294
|
+
errorCount: number;
|
|
295
|
+
warningCount: number;
|
|
296
|
+
} | {
|
|
297
|
+
kind: 'timeout';
|
|
298
|
+
durationMs: number;
|
|
299
|
+
message: string;
|
|
300
|
+
} | {
|
|
301
|
+
kind: 'client_abort';
|
|
302
|
+
durationMs: number;
|
|
303
|
+
} | {
|
|
304
|
+
kind: 'too_large';
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Scheduler backpressure shed the solve before it executed — the per-server
|
|
308
|
+
* queue was full (`QUEUE_FULL`) or it sat queued past the wait deadline
|
|
309
|
+
* (`QUEUE_TIMEOUT`). Retryable: the route maps this to 503 + `Retry-After`.
|
|
310
|
+
* `retryAfterSeconds` is a suggested backoff hint for the client.
|
|
311
|
+
*/
|
|
312
|
+
| {
|
|
313
|
+
kind: 'shed';
|
|
314
|
+
durationMs: number;
|
|
315
|
+
reason: 'queue_full' | 'queue_timeout';
|
|
316
|
+
retryAfterSeconds: number;
|
|
317
|
+
message: string;
|
|
318
|
+
} | {
|
|
319
|
+
kind: 'compute_error';
|
|
320
|
+
durationMs: number;
|
|
321
|
+
error: unknown;
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Build the transformed input tree — the exact object handed to the scheduler.
|
|
325
|
+
* `runSolvePipeline` calls this itself; a caller only needs it directly to see
|
|
326
|
+
* the tree before the pipeline runs (see {@link SolvePipelineArgs.inputTree}).
|
|
327
|
+
*/
|
|
328
|
+
declare function buildSolveInputTree(inputs: PipelineInput[], values: Record<string, unknown>): DataTree[];
|
|
329
|
+
declare function runSolvePipeline(args: SolvePipelineArgs): Promise<SolveOutcome>;
|
|
330
|
+
/**
|
|
331
|
+
* Re-key a coalesced envelope to a single waiter's `Accept-Encoding`.
|
|
332
|
+
*
|
|
333
|
+
* The single-flight coalescer runs ONE pipeline execution for N identical
|
|
334
|
+
* concurrent solves and hands every waiter the same {@link SolveEnvelope}. That
|
|
335
|
+
* envelope's wire form is baked from the FIRST caller's `Accept-Encoding`, so a
|
|
336
|
+
* later waiter with a different `Accept-Encoding` would otherwise get a body
|
|
337
|
+
* labelled with the wrong encoding (`Vary` can't help — this is one object
|
|
338
|
+
* shared across waiters, not a cache lookup). Encoding is deliberately not part
|
|
339
|
+
* of the coalesce key, so mixed clients still coalesce; this adapts the shared
|
|
340
|
+
* result to each waiter instead.
|
|
341
|
+
*
|
|
342
|
+
* Only the correctness-critical direction is adapted: a gzip envelope served to
|
|
343
|
+
* a non-gzip waiter is gunzipped back to JSON. The reverse (plain JSON to a
|
|
344
|
+
* gzip-capable waiter) is left uncompressed — correct, just not maximally small.
|
|
345
|
+
*/
|
|
346
|
+
declare function adaptEnvelopeToEncoding(envelope: SolveEnvelope, acceptEncoding: string): {
|
|
347
|
+
body: string | Uint8Array;
|
|
348
|
+
headers: Record<string, string>;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Adapt a persisted `SchemaInput` into `@selvajs/compute`'s raw `InputParamSchema`
|
|
353
|
+
* and let its `processInput` produce the typed `InputParam`. `values`/`acceptedFormats`
|
|
354
|
+
* are absent on `SchemaInput` (they live on the layout item's config), so
|
|
355
|
+
* valueList/file inputs fall back to carrying the selected value with no option
|
|
356
|
+
* list — the same value that reaches Grasshopper.
|
|
357
|
+
*/
|
|
358
|
+
declare function transformInputParameter(input: SchemaInput & {
|
|
359
|
+
minimum?: number;
|
|
360
|
+
maximum?: number;
|
|
361
|
+
stepSize?: number;
|
|
362
|
+
}, value: unknown): InputParam;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* In-process single-flight — dogpile protection for every solve.
|
|
366
|
+
*
|
|
367
|
+
* Deliberately NOT conditional on a result cache being configured: the dogpile is
|
|
368
|
+
* worst precisely when nothing else is caching (N concurrent identical solves each
|
|
369
|
+
* paying a full Rhino round trip — the scheduler has no in-flight coalescing of its
|
|
370
|
+
* own, and Rhino's `cachesolve` still costs a round trip per repeat). Gating this on
|
|
371
|
+
* cache configuration, as an earlier version did, turned it off in exactly the
|
|
372
|
+
* deployments most exposed to a cold-key stampede (hot public definition + a deploy).
|
|
373
|
+
*
|
|
374
|
+
* In-process only, sitting above `ISolveResultCache` (per app instance). A
|
|
375
|
+
* cross-instance lease (Redis `SET NX`) is a later backend capability.
|
|
376
|
+
*
|
|
377
|
+
* Correctness note: the shared promise resolves to ONE value for all waiters, so
|
|
378
|
+
* the wrapped work must return an immutable / independently-serializable result
|
|
379
|
+
* (the solve pipeline's envelope is safe to share by reference here because the
|
|
380
|
+
* app serializes it per response).
|
|
381
|
+
*/
|
|
382
|
+
interface SolveCacheSingleFlight {
|
|
383
|
+
/**
|
|
384
|
+
* Run `work` under `key`, coalescing concurrent identical calls: the first
|
|
385
|
+
* caller executes, overlapping callers for the same key await the same promise
|
|
386
|
+
* and result, and the key is freed as soon as it settles.
|
|
387
|
+
*
|
|
388
|
+
* `onWaiterJoined` fires on the OWNER's call each time another caller joins its
|
|
389
|
+
* flight — ownership changes what the owner's abort signal means (a solo run
|
|
390
|
+
* may cancel on its own client's disconnect, a shared one may not, or it would
|
|
391
|
+
* 499 every waiter). Never fires for a joining caller, and never after `work`
|
|
392
|
+
* settles.
|
|
393
|
+
*/
|
|
394
|
+
run<T>(key: string, work: () => Promise<T>, onWaiterJoined?: () => void): Promise<T>;
|
|
395
|
+
/** Number of keys currently in flight (observability / tests). */
|
|
396
|
+
inFlight(): number;
|
|
397
|
+
}
|
|
398
|
+
interface SolveCacheSingleFlightOptions {
|
|
399
|
+
/**
|
|
400
|
+
* Fired when a caller joins an already-in-flight key instead of running its
|
|
401
|
+
* own work — the coalescing win this module exists for, otherwise invisible.
|
|
402
|
+
*/
|
|
403
|
+
onJoin?: (key: string) => void;
|
|
404
|
+
}
|
|
405
|
+
declare function createSolveCacheSingleFlight(options?: SolveCacheSingleFlightOptions): SolveCacheSingleFlight;
|
|
406
|
+
|
|
407
|
+
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 };
|