@selvajs/solve 0.2.0-beta.0 → 0.2.0-beta.2
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/client.cjs +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +53 -1
- package/dist/client.d.ts +53 -1
- 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 +144 -18
- package/dist/server.d.ts +144 -18
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/shared.cjs.map +1 -1
- package/package.json +3 -3
package/dist/server.d.cts
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.cjs';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -23,7 +25,7 @@ interface CachedClient {
|
|
|
23
25
|
scheduler: SolveScheduler;
|
|
24
26
|
/**
|
|
25
27
|
* Last Server-Timing decode/solve/encode, written by onServerTiming.
|
|
26
|
-
* The scheduler runs up to `
|
|
28
|
+
* The scheduler runs up to `maxConcurrent` solves at once, so `last` alone
|
|
27
29
|
* can't be trusted per-request: callers snapshot `seq` before their solve
|
|
28
30
|
* and only attribute `last` to themselves if exactly one write happened
|
|
29
31
|
* since (see the guard in `runSolvePipeline`), dropping it otherwise
|
|
@@ -50,6 +52,12 @@ interface CachedClient {
|
|
|
50
52
|
seq: number;
|
|
51
53
|
};
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Debug verbosity: `false` is silent, `true` gives concise cache/timing logs
|
|
57
|
+
* (`onDebugLog`), `'verbose'` also enables full lib-level request/response
|
|
58
|
+
* dumps (incl. geometry) — a superset of `true`, not an independent setting.
|
|
59
|
+
*/
|
|
60
|
+
type ClientCacheDebug = boolean | 'verbose';
|
|
53
61
|
/** Config injected by the consuming app, so this module stays env-agnostic and testable. */
|
|
54
62
|
interface ClientCacheConfig {
|
|
55
63
|
/** Per-solve timeout forwarded to the scheduler (`ComputeLimits.maxSolveDurationMs`). */
|
|
@@ -60,15 +68,10 @@ interface ClientCacheConfig {
|
|
|
60
68
|
cacheerroredsolves: boolean;
|
|
61
69
|
/** Reference large definitions by server cache key (pointer) instead of re-uploading. */
|
|
62
70
|
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
71
|
/**
|
|
70
72
|
* Backpressure — max solves that may WAIT in the FIFO queue (excludes the
|
|
71
|
-
* in-flight
|
|
73
|
+
* in-flight solves, capped at the scheduler's `maxConcurrent`, itself driven
|
|
74
|
+
* by the compute server's probed child count). `0` = unbounded (`ComputeLimits.
|
|
72
75
|
* computeMaxQueueDepth`); a full queue sheds new solves with `QUEUE_FULL`.
|
|
73
76
|
*/
|
|
74
77
|
maxQueueDepth: number;
|
|
@@ -81,16 +84,14 @@ interface ClientCacheConfig {
|
|
|
81
84
|
/**
|
|
82
85
|
* Byte budget for this client's in-process solve cache, evicted LRU alongside
|
|
83
86
|
* its entry-count cap. Applies per warm client — total worst-case heap is this
|
|
84
|
-
* × `
|
|
87
|
+
* × `maxWarmComputeServers`. `0` disables the cache entirely
|
|
85
88
|
* (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).
|
|
86
89
|
*/
|
|
87
90
|
responseCacheMaxBytes: number;
|
|
88
|
-
/**
|
|
89
|
-
debug:
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
/** Max distinct warm clients before the LRU evicts the oldest. Default 16. */
|
|
93
|
-
maxCachedClients?: number;
|
|
91
|
+
/** Debug verbosity. `onDebugLog` is only ever invoked when this is not `false`. */
|
|
92
|
+
debug: ClientCacheDebug;
|
|
93
|
+
/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */
|
|
94
|
+
maxWarmComputeServers?: number;
|
|
94
95
|
/** Sink for the concise debug lines (the app wires `console.log`). */
|
|
95
96
|
onDebugLog?: (message: string) => void;
|
|
96
97
|
}
|
|
@@ -112,6 +113,14 @@ interface ClientCache {
|
|
|
112
113
|
* with the client that owns them, so totals can fall over time.
|
|
113
114
|
*/
|
|
114
115
|
solveCacheStats(): SolveCacheStats;
|
|
116
|
+
/**
|
|
117
|
+
* Drop every retained solve result, keeping the warm clients (and their
|
|
118
|
+
* connections and server-side definition pointers) intact. Nothing expires
|
|
119
|
+
* these on its own — the byte budget is the only other pressure — so this is
|
|
120
|
+
* the operator's release valve when a definition's inputs no longer describe
|
|
121
|
+
* its output, e.g. it reads an external source that has since changed.
|
|
122
|
+
*/
|
|
123
|
+
clearSolveCaches(): void;
|
|
115
124
|
/** Dispose every warm client. Test seam / shutdown hook. */
|
|
116
125
|
disposeAll(): void;
|
|
117
126
|
}
|
|
@@ -122,7 +131,7 @@ interface SolveCacheStats {
|
|
|
122
131
|
bytes: number;
|
|
123
132
|
hits: number;
|
|
124
133
|
misses: number;
|
|
125
|
-
/** Entries dropped under size/byte pressure (not
|
|
134
|
+
/** Entries dropped under size/byte pressure (not replacement or manual clears). */
|
|
126
135
|
evictions: number;
|
|
127
136
|
}
|
|
128
137
|
declare function createClientCache(config: ClientCacheConfig): ClientCache;
|
|
@@ -404,4 +413,121 @@ interface SolveCacheSingleFlightOptions {
|
|
|
404
413
|
}
|
|
405
414
|
declare function createSolveCacheSingleFlight(options?: SolveCacheSingleFlightOptions): SolveCacheSingleFlight;
|
|
406
415
|
|
|
407
|
-
|
|
416
|
+
/**
|
|
417
|
+
* `SolveEngine` — the facade that owns everything a consumer needs to run
|
|
418
|
+
* interactive Grasshopper solves: the per-server warm-client cache, the
|
|
419
|
+
* definition-byte cache, single-flight coalescing, the pipeline call, and the
|
|
420
|
+
* outcome→HTTP mapping. Composes `createClientCache` + `createDefinitionByteCache`
|
|
421
|
+
* + `createSolveCacheSingleFlight` + `runSolvePipeline` — the same primitives a
|
|
422
|
+
* hand-assembled app route already wires, minus the wiring.
|
|
423
|
+
*
|
|
424
|
+
* Deliberately does NOT read env itself (no `resolveComputeLimits` call here):
|
|
425
|
+
* `@selvajs/server`, which owns that function, depends on `@selvajs/solve`, so
|
|
426
|
+
* the reverse import would be circular. A consumer resolves its own limits
|
|
427
|
+
* (`resolveComputeLimits` from `@selvajs/server/compute`, or any equivalent) and
|
|
428
|
+
* passes the handful of fields this engine actually needs.
|
|
429
|
+
*
|
|
430
|
+
* What stays OUT, on purpose (matches `server/index.ts`'s existing boundary):
|
|
431
|
+
* auth, DB reads, share tokens, rate limiting, metric sinks, and which compute
|
|
432
|
+
* server to use (`resolveServerForOrg`-equivalent) — all app policy, supplied
|
|
433
|
+
* per-call via `SolveEngineSolveArgs.server`.
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs,
|
|
438
|
+
* passed straight through to `createClientCache` — pass the resolved object
|
|
439
|
+
* through, its extra fields are ignored.
|
|
440
|
+
*/
|
|
441
|
+
interface SolveEngineLimits {
|
|
442
|
+
maxSolveDurationMs: number;
|
|
443
|
+
computeResponseMaxBytes: number;
|
|
444
|
+
computeReuseDefinitionCache: boolean;
|
|
445
|
+
computeServerCachesolve: boolean;
|
|
446
|
+
computeCacheErroredSolves: boolean;
|
|
447
|
+
computeMaxQueueDepth: number;
|
|
448
|
+
computeQueueWaitMs: number;
|
|
449
|
+
computeDefinitionCacheBytes: number;
|
|
450
|
+
computeSolveCacheBytes: number;
|
|
451
|
+
}
|
|
452
|
+
interface SolveEngineOptions {
|
|
453
|
+
/** Required — the engine does not read env itself (see module doc). */
|
|
454
|
+
limits: SolveEngineLimits;
|
|
455
|
+
logger?: ILogger;
|
|
456
|
+
/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16 (see `createClientCache`). */
|
|
457
|
+
maxWarmComputeServers?: number;
|
|
458
|
+
/** Concise cache/timing logs when truthy; `'verbose'` also enables full lib-level request/response dumps. Default off. */
|
|
459
|
+
debug?: ClientCacheDebug;
|
|
460
|
+
onDebugLog?: (message: string) => void;
|
|
461
|
+
/** Fired when a caller joins an already-in-flight solve instead of running its own (`createSolveCacheSingleFlight`'s `onJoin`). */
|
|
462
|
+
onSolveCoalesced?: (key: string) => void;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* `definitionSource` accepts every form `runSolvePipeline` does, plus the
|
|
466
|
+
* `{versionId, load}` sugar that builds (and caches) a `ByteCacheRef` internally.
|
|
467
|
+
* A `ByteCacheRef` obtained from `engine.definitionRef()` ahead of time (e.g. to
|
|
468
|
+
* read its bytes for schema extraction before solving) is accepted directly and
|
|
469
|
+
* NOT re-wrapped — recognized by its `outcome` field, which a plain external
|
|
470
|
+
* `DefinitionRef` never has.
|
|
471
|
+
*/
|
|
472
|
+
type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef | {
|
|
473
|
+
versionId: string;
|
|
474
|
+
load: () => Promise<Uint8Array>;
|
|
475
|
+
};
|
|
476
|
+
interface SolveEngineSolveArgs {
|
|
477
|
+
server: ResolvedServer;
|
|
478
|
+
definitionSource: SolveEngineDefinitionSource;
|
|
479
|
+
/**
|
|
480
|
+
* Coalesce-key identity for a raw `Uint8Array`/`string` `definitionSource`,
|
|
481
|
+
* which has no natural identity the way a `DefinitionRef`'s `.key` does.
|
|
482
|
+
* Required in that case — `solve()` throws without it, rather than silently
|
|
483
|
+
* hashing bytes (a different identity convention than the rest of the
|
|
484
|
+
* package, which keys on immutable ids). Ignored for the other source forms.
|
|
485
|
+
*/
|
|
486
|
+
definitionKey?: string;
|
|
487
|
+
inputs: PipelineInput[];
|
|
488
|
+
values: Record<string, unknown>;
|
|
489
|
+
signal: AbortSignal;
|
|
490
|
+
acceptEncoding?: string;
|
|
491
|
+
/** Stamped as `X-Selva-Definition` on this solve's client — see `ClientCache.getClient`. */
|
|
492
|
+
definitionGuid?: string;
|
|
493
|
+
loadStartMs?: number;
|
|
494
|
+
defLoadMs?: number;
|
|
495
|
+
prepMarks?: [string, number][];
|
|
496
|
+
}
|
|
497
|
+
interface FrameworkAgnosticResponse {
|
|
498
|
+
status: number;
|
|
499
|
+
headers: Record<string, string>;
|
|
500
|
+
body: string | Uint8Array;
|
|
501
|
+
}
|
|
502
|
+
interface SolveEngineStats {
|
|
503
|
+
client: SolveCacheStats;
|
|
504
|
+
definitionBytes: ByteCacheStats;
|
|
505
|
+
coalescing: {
|
|
506
|
+
inFlight: number;
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
declare class SolveEngine {
|
|
510
|
+
private readonly limits;
|
|
511
|
+
private readonly clientCache;
|
|
512
|
+
private readonly byteCache;
|
|
513
|
+
private readonly singleFlight;
|
|
514
|
+
constructor(options: SolveEngineOptions);
|
|
515
|
+
getClient(server: ResolvedServer, opts?: {
|
|
516
|
+
definitionGuid?: string;
|
|
517
|
+
}): Promise<CachedClient>;
|
|
518
|
+
definitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;
|
|
519
|
+
evictServer(id: string): void;
|
|
520
|
+
clearSolveCaches(): void;
|
|
521
|
+
stats(): SolveEngineStats;
|
|
522
|
+
solve(args: SolveEngineSolveArgs): Promise<SolveOutcome>;
|
|
523
|
+
toResponse(outcome: SolveOutcome, opts?: {
|
|
524
|
+
onError?: (status: number, body: {
|
|
525
|
+
message: string;
|
|
526
|
+
retryAfter?: number;
|
|
527
|
+
}) => never;
|
|
528
|
+
}): FrameworkAgnosticResponse;
|
|
529
|
+
toWebResponse(outcome: SolveOutcome): Response;
|
|
530
|
+
private resolveDefinitionSource;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
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.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
|
/**
|
|
@@ -23,7 +25,7 @@ interface CachedClient {
|
|
|
23
25
|
scheduler: SolveScheduler;
|
|
24
26
|
/**
|
|
25
27
|
* Last Server-Timing decode/solve/encode, written by onServerTiming.
|
|
26
|
-
* The scheduler runs up to `
|
|
28
|
+
* The scheduler runs up to `maxConcurrent` solves at once, so `last` alone
|
|
27
29
|
* can't be trusted per-request: callers snapshot `seq` before their solve
|
|
28
30
|
* and only attribute `last` to themselves if exactly one write happened
|
|
29
31
|
* since (see the guard in `runSolvePipeline`), dropping it otherwise
|
|
@@ -50,6 +52,12 @@ interface CachedClient {
|
|
|
50
52
|
seq: number;
|
|
51
53
|
};
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Debug verbosity: `false` is silent, `true` gives concise cache/timing logs
|
|
57
|
+
* (`onDebugLog`), `'verbose'` also enables full lib-level request/response
|
|
58
|
+
* dumps (incl. geometry) — a superset of `true`, not an independent setting.
|
|
59
|
+
*/
|
|
60
|
+
type ClientCacheDebug = boolean | 'verbose';
|
|
53
61
|
/** Config injected by the consuming app, so this module stays env-agnostic and testable. */
|
|
54
62
|
interface ClientCacheConfig {
|
|
55
63
|
/** Per-solve timeout forwarded to the scheduler (`ComputeLimits.maxSolveDurationMs`). */
|
|
@@ -60,15 +68,10 @@ interface ClientCacheConfig {
|
|
|
60
68
|
cacheerroredsolves: boolean;
|
|
61
69
|
/** Reference large definitions by server cache key (pointer) instead of re-uploading. */
|
|
62
70
|
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
71
|
/**
|
|
70
72
|
* Backpressure — max solves that may WAIT in the FIFO queue (excludes the
|
|
71
|
-
* in-flight
|
|
73
|
+
* in-flight solves, capped at the scheduler's `maxConcurrent`, itself driven
|
|
74
|
+
* by the compute server's probed child count). `0` = unbounded (`ComputeLimits.
|
|
72
75
|
* computeMaxQueueDepth`); a full queue sheds new solves with `QUEUE_FULL`.
|
|
73
76
|
*/
|
|
74
77
|
maxQueueDepth: number;
|
|
@@ -81,16 +84,14 @@ interface ClientCacheConfig {
|
|
|
81
84
|
/**
|
|
82
85
|
* Byte budget for this client's in-process solve cache, evicted LRU alongside
|
|
83
86
|
* its entry-count cap. Applies per warm client — total worst-case heap is this
|
|
84
|
-
* × `
|
|
87
|
+
* × `maxWarmComputeServers`. `0` disables the cache entirely
|
|
85
88
|
* (`ComputeLimits.computeSolveCacheBytes`, env `COMPUTE_SOLVE_CACHE_MB`).
|
|
86
89
|
*/
|
|
87
90
|
responseCacheMaxBytes: number;
|
|
88
|
-
/**
|
|
89
|
-
debug:
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
/** Max distinct warm clients before the LRU evicts the oldest. Default 16. */
|
|
93
|
-
maxCachedClients?: number;
|
|
91
|
+
/** Debug verbosity. `onDebugLog` is only ever invoked when this is not `false`. */
|
|
92
|
+
debug: ClientCacheDebug;
|
|
93
|
+
/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16. */
|
|
94
|
+
maxWarmComputeServers?: number;
|
|
94
95
|
/** Sink for the concise debug lines (the app wires `console.log`). */
|
|
95
96
|
onDebugLog?: (message: string) => void;
|
|
96
97
|
}
|
|
@@ -112,6 +113,14 @@ interface ClientCache {
|
|
|
112
113
|
* with the client that owns them, so totals can fall over time.
|
|
113
114
|
*/
|
|
114
115
|
solveCacheStats(): SolveCacheStats;
|
|
116
|
+
/**
|
|
117
|
+
* Drop every retained solve result, keeping the warm clients (and their
|
|
118
|
+
* connections and server-side definition pointers) intact. Nothing expires
|
|
119
|
+
* these on its own — the byte budget is the only other pressure — so this is
|
|
120
|
+
* the operator's release valve when a definition's inputs no longer describe
|
|
121
|
+
* its output, e.g. it reads an external source that has since changed.
|
|
122
|
+
*/
|
|
123
|
+
clearSolveCaches(): void;
|
|
115
124
|
/** Dispose every warm client. Test seam / shutdown hook. */
|
|
116
125
|
disposeAll(): void;
|
|
117
126
|
}
|
|
@@ -122,7 +131,7 @@ interface SolveCacheStats {
|
|
|
122
131
|
bytes: number;
|
|
123
132
|
hits: number;
|
|
124
133
|
misses: number;
|
|
125
|
-
/** Entries dropped under size/byte pressure (not
|
|
134
|
+
/** Entries dropped under size/byte pressure (not replacement or manual clears). */
|
|
126
135
|
evictions: number;
|
|
127
136
|
}
|
|
128
137
|
declare function createClientCache(config: ClientCacheConfig): ClientCache;
|
|
@@ -404,4 +413,121 @@ interface SolveCacheSingleFlightOptions {
|
|
|
404
413
|
}
|
|
405
414
|
declare function createSolveCacheSingleFlight(options?: SolveCacheSingleFlightOptions): SolveCacheSingleFlight;
|
|
406
415
|
|
|
407
|
-
|
|
416
|
+
/**
|
|
417
|
+
* `SolveEngine` — the facade that owns everything a consumer needs to run
|
|
418
|
+
* interactive Grasshopper solves: the per-server warm-client cache, the
|
|
419
|
+
* definition-byte cache, single-flight coalescing, the pipeline call, and the
|
|
420
|
+
* outcome→HTTP mapping. Composes `createClientCache` + `createDefinitionByteCache`
|
|
421
|
+
* + `createSolveCacheSingleFlight` + `runSolvePipeline` — the same primitives a
|
|
422
|
+
* hand-assembled app route already wires, minus the wiring.
|
|
423
|
+
*
|
|
424
|
+
* Deliberately does NOT read env itself (no `resolveComputeLimits` call here):
|
|
425
|
+
* `@selvajs/server`, which owns that function, depends on `@selvajs/solve`, so
|
|
426
|
+
* the reverse import would be circular. A consumer resolves its own limits
|
|
427
|
+
* (`resolveComputeLimits` from `@selvajs/server/compute`, or any equivalent) and
|
|
428
|
+
* passes the handful of fields this engine actually needs.
|
|
429
|
+
*
|
|
430
|
+
* What stays OUT, on purpose (matches `server/index.ts`'s existing boundary):
|
|
431
|
+
* auth, DB reads, share tokens, rate limiting, metric sinks, and which compute
|
|
432
|
+
* server to use (`resolveServerForOrg`-equivalent) — all app policy, supplied
|
|
433
|
+
* per-call via `SolveEngineSolveArgs.server`.
|
|
434
|
+
*/
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The subset of `ComputeLimits` (`@selvajs/server/compute`) the engine needs,
|
|
438
|
+
* passed straight through to `createClientCache` — pass the resolved object
|
|
439
|
+
* through, its extra fields are ignored.
|
|
440
|
+
*/
|
|
441
|
+
interface SolveEngineLimits {
|
|
442
|
+
maxSolveDurationMs: number;
|
|
443
|
+
computeResponseMaxBytes: number;
|
|
444
|
+
computeReuseDefinitionCache: boolean;
|
|
445
|
+
computeServerCachesolve: boolean;
|
|
446
|
+
computeCacheErroredSolves: boolean;
|
|
447
|
+
computeMaxQueueDepth: number;
|
|
448
|
+
computeQueueWaitMs: number;
|
|
449
|
+
computeDefinitionCacheBytes: number;
|
|
450
|
+
computeSolveCacheBytes: number;
|
|
451
|
+
}
|
|
452
|
+
interface SolveEngineOptions {
|
|
453
|
+
/** Required — the engine does not read env itself (see module doc). */
|
|
454
|
+
limits: SolveEngineLimits;
|
|
455
|
+
logger?: ILogger;
|
|
456
|
+
/** Max distinct warm compute servers before the LRU evicts the oldest. Default 16 (see `createClientCache`). */
|
|
457
|
+
maxWarmComputeServers?: number;
|
|
458
|
+
/** Concise cache/timing logs when truthy; `'verbose'` also enables full lib-level request/response dumps. Default off. */
|
|
459
|
+
debug?: ClientCacheDebug;
|
|
460
|
+
onDebugLog?: (message: string) => void;
|
|
461
|
+
/** Fired when a caller joins an already-in-flight solve instead of running its own (`createSolveCacheSingleFlight`'s `onJoin`). */
|
|
462
|
+
onSolveCoalesced?: (key: string) => void;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* `definitionSource` accepts every form `runSolvePipeline` does, plus the
|
|
466
|
+
* `{versionId, load}` sugar that builds (and caches) a `ByteCacheRef` internally.
|
|
467
|
+
* A `ByteCacheRef` obtained from `engine.definitionRef()` ahead of time (e.g. to
|
|
468
|
+
* read its bytes for schema extraction before solving) is accepted directly and
|
|
469
|
+
* NOT re-wrapped — recognized by its `outcome` field, which a plain external
|
|
470
|
+
* `DefinitionRef` never has.
|
|
471
|
+
*/
|
|
472
|
+
type SolveEngineDefinitionSource = Uint8Array | string | DefinitionRef | ByteCacheRef | {
|
|
473
|
+
versionId: string;
|
|
474
|
+
load: () => Promise<Uint8Array>;
|
|
475
|
+
};
|
|
476
|
+
interface SolveEngineSolveArgs {
|
|
477
|
+
server: ResolvedServer;
|
|
478
|
+
definitionSource: SolveEngineDefinitionSource;
|
|
479
|
+
/**
|
|
480
|
+
* Coalesce-key identity for a raw `Uint8Array`/`string` `definitionSource`,
|
|
481
|
+
* which has no natural identity the way a `DefinitionRef`'s `.key` does.
|
|
482
|
+
* Required in that case — `solve()` throws without it, rather than silently
|
|
483
|
+
* hashing bytes (a different identity convention than the rest of the
|
|
484
|
+
* package, which keys on immutable ids). Ignored for the other source forms.
|
|
485
|
+
*/
|
|
486
|
+
definitionKey?: string;
|
|
487
|
+
inputs: PipelineInput[];
|
|
488
|
+
values: Record<string, unknown>;
|
|
489
|
+
signal: AbortSignal;
|
|
490
|
+
acceptEncoding?: string;
|
|
491
|
+
/** Stamped as `X-Selva-Definition` on this solve's client — see `ClientCache.getClient`. */
|
|
492
|
+
definitionGuid?: string;
|
|
493
|
+
loadStartMs?: number;
|
|
494
|
+
defLoadMs?: number;
|
|
495
|
+
prepMarks?: [string, number][];
|
|
496
|
+
}
|
|
497
|
+
interface FrameworkAgnosticResponse {
|
|
498
|
+
status: number;
|
|
499
|
+
headers: Record<string, string>;
|
|
500
|
+
body: string | Uint8Array;
|
|
501
|
+
}
|
|
502
|
+
interface SolveEngineStats {
|
|
503
|
+
client: SolveCacheStats;
|
|
504
|
+
definitionBytes: ByteCacheStats;
|
|
505
|
+
coalescing: {
|
|
506
|
+
inFlight: number;
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
declare class SolveEngine {
|
|
510
|
+
private readonly limits;
|
|
511
|
+
private readonly clientCache;
|
|
512
|
+
private readonly byteCache;
|
|
513
|
+
private readonly singleFlight;
|
|
514
|
+
constructor(options: SolveEngineOptions);
|
|
515
|
+
getClient(server: ResolvedServer, opts?: {
|
|
516
|
+
definitionGuid?: string;
|
|
517
|
+
}): Promise<CachedClient>;
|
|
518
|
+
definitionRef(versionId: string, load: () => Promise<Uint8Array>): ByteCacheRef;
|
|
519
|
+
evictServer(id: string): void;
|
|
520
|
+
clearSolveCaches(): void;
|
|
521
|
+
stats(): SolveEngineStats;
|
|
522
|
+
solve(args: SolveEngineSolveArgs): Promise<SolveOutcome>;
|
|
523
|
+
toResponse(outcome: SolveOutcome, opts?: {
|
|
524
|
+
onError?: (status: number, body: {
|
|
525
|
+
message: string;
|
|
526
|
+
retryAfter?: number;
|
|
527
|
+
}) => never;
|
|
528
|
+
}): FrameworkAgnosticResponse;
|
|
529
|
+
toWebResponse(outcome: SolveOutcome): Response;
|
|
530
|
+
private resolveDefinitionSource;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
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 w,RhinoComputeError as I,TreeBuilder as O}from"@selvajs/compute";import{gzip as _,gunzipSync as A}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(_),E=1,P="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===w.QUEUE_FULL)return{kind:"shed",durationMs:b,reason:"queue_full",retryAfterSeconds:1,message:"Compute server is at capacity. Retry shortly."};if(v.code===w.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??[]}),[P]:String(E),Vary:"Accept-Encoding"},R={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:R}):(C["Content-Length"]=String(Buffer.byteLength(d)),x={body:d,headers:C,result:n,metrics:R}),{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=A(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?{maxEntries:20,ttlMs:5*6e4,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},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{E as COMPUTE_CONTRACT_VERSION,P 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),a=performance.now()-r,s=n.solveMeta.seq,m=n.rhinoTiming.seq,i,c=performance.now();try{i=await n.scheduler.solve(e.definitionSource,o,{signal:t})}catch(v){let x=performance.now()-c;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 u=performance.now()-c,p=performance.now(),d;try{d=JSON.stringify(i)}catch(v){if(v instanceof RangeError)return{kind:"too_large"};throw v}if(d.length>e.responseMaxBytes)return{kind:"too_large"};let f=performance.now()-p,M=/\bgzip\b/i.test(e.acceptEncoding)&&d.length>1024,C=null,S=0;if(M){let v=performance.now();C=await j(Buffer.from(d)),S=performance.now()-v}let g=performance.now()-e.loadStartMs,l=n.solveMeta.seq===s+1?n.solveMeta.last:null,h=l&&!l.fromCache&&n.rhinoTiming.seq===m+1?n.rhinoTiming.last:null,y={"Content-Type":"application/json","Server-Timing":V({defLoadMs:e.defLoadMs,treeBuildMs:a,solveMs:u,serializeMs:f,gzipMs:S,serverTotalMs:g,rhino:h,settle:l,byteRefOutcome:e.byteRefOutcome,prepMarks:e.prepMarks??[]}),[_]:String(F),Vary:"Accept-Encoding"},R={treeBuildMs:a,solveMs:u,serializeMs:f,gzipMs:S,serverTotalMs:g,serializedBytes:d.length,compressedBytes:C?.byteLength??null},D;return C?(y["Content-Encoding"]="gzip",y["Content-Length"]=String(C.byteLength),D={body:new Uint8Array(C),encoding:"gzip",headers:y,result:i,metrics:R}):(y["Content-Length"]=String(Buffer.byteLength(d)),D={body:d,headers:y,result:i,metrics:R}),{kind:"ok",envelope:D,solveMs:u,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 a=t.decode+t.solve+t.encode,s=Math.max(0,e.solveMs-a);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[a,s]of e.prepMarks)n+=`, p_${a};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 a=o.loaded?o.fromCache?"hit":"miss":"skipped";n+=`, def_bytes;desc=${a}`}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 a(s,m){let i={last:null,seq:0},c={last:null,seq:0},u=e.debug==="verbose",p={serverUrl:s.serverUrl,apiKey:s.apiKey,debug:u,cachesolve:e.cachesolve,cacheerroredsolves:e.cacheerroredsolves,onServerTiming:g=>{let l=g.decode??0,h=g.solve??0,b=g.encode??0;if(i.seq+=1,i.last={decode:l,solve:h,encode:b},!e.debug)return;let y=l<=1?"HIT (definition reused, no re-upload/parse)":"miss (full parse)",R=e.cachesolve&&h<=1?"HIT (cached result, solve skipped)":e.cachesolve?"miss (solved fresh)":"off";o(`[Compute/rhino-cache] decode=${l}ms solve=${h}ms encode=${b}ms | definition-cache: ${y} | solve-cache (cachesolve): ${R}`)}};m&&(p.headers={"X-Selva-Definition":m});let d=await K.create(p),f=z,w=0,M=!1;async function C(g){if(!M){M=!0;try{let l=await Promise.resolve(d.serverStats?.getActiveChildren({initialize:!1})).catch(()=>null);w=Date.now();let h=!l||l<=0?z:l;if(h===f)return;o(`[Compute/client-cache] concurrency ${f} \u2192 ${h} (server child count)`),f=h,g?.setMaxConcurrent(h)}finally{M=!1}}}await C();let S=d.createScheduler({mode:"queue",maxConcurrent:f,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:(g,l)=>{if(c.seq+=1,l.status==="success"&&(l.fromCache!==!0&&Date.now()-w>=X&&C(S),c.last={fromCache:l.fromCache===!0,definitionReuploaded:l.definitionReuploaded},!!e.debug&&(o(`[Compute/selva-cache] ${l.fromCache?"HIT \u2014 served from cache (no compute call)":"miss \u2014 went to Rhino.Compute"} (${Math.round(l.durationMs)}ms)`),l.fromCache||o(l.definitionReuploaded===!0?"[Compute/def-cache] miss \u2014 definition RE-UPLOADED (pointer was cold/stale)":l.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"),!l.fromCache))){let h=l.response?.errors,b=Array.isArray(h)?h.length:0;if(b>0){let y=e.cacheerroredsolves?"cached (cacheerroredsolves on)":"not cached (cacheerroredsolves off) \u2014 re-run until fixed";o(`[Compute/rhino-cache] ${b} GH error(s); ${y}. First: ${JSON.stringify(h[0])}`)}}}});return{client:d,scheduler:S,rhinoTiming:i,solveMeta:c}}return{async getClient(s,m){let i=s.id,c=t.get(i);if(c)return t.delete(i),t.set(i,c),c;let u=r.get(i);if(u)return u;o(`[Compute/client-cache] miss \u2014 building warm client for server ${i}`);let p=(async()=>{let d=await a(s,m?.definitionGuid);if(t.size>=n){let f=t.keys().next().value;f!==void 0&&(o(`[Compute/client-cache] LRU evicted warm client for server ${f} (cap ${n})`),t.get(f)?.scheduler.dispose(),t.delete(f))}return t.set(i,d),d})().finally(()=>r.delete(i));return r.set(i,p),p},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 a(i,c){t.delete(i),t.set(i,c)}function s(){for(;r>n&&t.size>0;){let i=t.keys().next().value,c=t.get(i);t.delete(i),r-=c.bytes.byteLength,o.evictions+=1}}function m(i,c){if(n===0||c.byteLength>n)return;let u=t.get(i);u&&(r-=u.bytes.byteLength),t.set(i,{bytes:c}),r+=c.byteLength,s()}return{getOrLoad(i,c){let u={loaded:!1,fromCache:!1};return{key:i,outcome:u,load:async()=>{let p=t.get(i);if(p)return a(i,p),o.hits+=1,u.loaded=!0,u.fromCache=!0,p.bytes;o.misses+=1,u.loaded=!0,u.fromCache=!1;let d=await c();return m(i,d),d}}},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 a=n.get(t);if(a)return e.onJoin?.(t),a.notifyOwner?.(),a.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),a=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,c=!1;n.signal.addEventListener("abort",()=>{c||i.abort()},{once:!0});let u=n.acceptEncoding??"",p=await this.singleFlight.run(m,()=>B({definitionSource:t,byteRefOutcome:r,inputs:n.inputs,values:n.values,inputTree:s,client:a,responseMaxBytes:this.limits.computeResponseMaxBytes,maxSolveDurationMs:this.limits.maxSolveDurationMs,acceptEncoding:u,signal:i.signal,loadStartMs:n.loadStartMs??performance.now(),defLoadMs:n.defLoadMs??0,prepMarks:n.prepMarks}),()=>{c=!0});if(p.kind!=="ok")return p;let d=T(p.envelope,u);return{...p,envelope:{...p.envelope,body:d.body,headers:d.headers}}}toResponse(n,t){let r=(o,a)=>{t?.onError&&t.onError(o,a);let s={"Content-Type":"application/json"};return a.retryAfter!==void 0&&(s["Retry-After"]=String(a.retryAfter)),{status:o,headers:s,body:JSON.stringify(a)}};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
|