@selvajs/server 0.2.0-beta.2 → 0.2.0-beta.3
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/compute/__tests__/client-cache.test.js +18 -0
- package/dist/compute/__tests__/client-cache.test.js.map +1 -1
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts +2 -0
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts.map +1 -0
- package/dist/compute/__tests__/definition-byte-cache.test.js +107 -0
- package/dist/compute/__tests__/definition-byte-cache.test.js.map +1 -0
- package/dist/compute/__tests__/solve-cache.test.d.ts +13 -0
- package/dist/compute/__tests__/solve-cache.test.d.ts.map +1 -0
- package/dist/compute/__tests__/solve-cache.test.js +234 -0
- package/dist/compute/__tests__/solve-cache.test.js.map +1 -0
- package/dist/compute/__tests__/solve-pipeline.test.js +267 -2
- package/dist/compute/__tests__/solve-pipeline.test.js.map +1 -1
- package/dist/compute/client-cache.d.ts +12 -0
- package/dist/compute/client-cache.d.ts.map +1 -1
- package/dist/compute/client-cache.js +12 -3
- package/dist/compute/client-cache.js.map +1 -1
- package/dist/compute/definition-byte-cache.d.ts +85 -0
- package/dist/compute/definition-byte-cache.d.ts.map +1 -0
- package/dist/compute/definition-byte-cache.js +110 -0
- package/dist/compute/definition-byte-cache.js.map +1 -0
- package/dist/compute/index.d.ts +7 -2
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +6 -1
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/limits.d.ts +52 -0
- package/dist/compute/limits.d.ts.map +1 -1
- package/dist/compute/limits.js +33 -1
- package/dist/compute/limits.js.map +1 -1
- package/dist/compute/memory-solve-cache.d.ts +58 -0
- package/dist/compute/memory-solve-cache.d.ts.map +1 -0
- package/dist/compute/memory-solve-cache.js +0 -0
- package/dist/compute/memory-solve-cache.js.map +1 -0
- package/dist/compute/solve-cache-envelope.d.ts +53 -0
- package/dist/compute/solve-cache-envelope.d.ts.map +1 -0
- package/dist/compute/solve-cache-envelope.js +66 -0
- package/dist/compute/solve-cache-envelope.js.map +1 -0
- package/dist/compute/solve-cache-key.d.ts +65 -0
- package/dist/compute/solve-cache-key.d.ts.map +1 -0
- package/dist/compute/solve-cache-key.js +50 -0
- package/dist/compute/solve-cache-key.js.map +1 -0
- package/dist/compute/solve-cache-single-flight.d.ts +35 -0
- package/dist/compute/solve-cache-single-flight.d.ts.map +1 -0
- package/dist/compute/solve-cache-single-flight.js +48 -0
- package/dist/compute/solve-cache-single-flight.js.map +1 -0
- package/dist/compute/solve-pipeline.d.ts +71 -8
- package/dist/compute/solve-pipeline.d.ts.map +1 -1
- package/dist/compute/solve-pipeline.js +178 -8
- package/dist/compute/solve-pipeline.js.map +1 -1
- package/dist/definitions/__tests__/definition-service.test.d.ts +14 -0
- package/dist/definitions/__tests__/definition-service.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/definition-service.test.js +95 -0
- package/dist/definitions/__tests__/definition-service.test.js.map +1 -0
- package/dist/definitions/definition-service.d.ts +1 -1
- package/dist/definitions/definition-service.d.ts.map +1 -1
- package/dist/definitions/definition-service.js +8 -4
- package/dist/definitions/definition-service.js.map +1 -1
- package/package.json +2 -2
- package/src/compute/__tests__/client-cache.test.ts +20 -0
- package/src/compute/__tests__/definition-byte-cache.test.ts +129 -0
- package/src/compute/__tests__/solve-cache.test.ts +268 -0
- package/src/compute/__tests__/solve-pipeline.test.ts +320 -1
- package/src/compute/client-cache.ts +24 -3
- package/src/compute/definition-byte-cache.ts +168 -0
- package/src/compute/index.ts +34 -0
- package/src/compute/limits.ts +79 -1
- package/src/compute/memory-solve-cache.ts +0 -0
- package/src/compute/solve-cache-envelope.ts +98 -0
- package/src/compute/solve-cache-key.ts +85 -0
- package/src/compute/solve-cache-single-flight.ts +59 -0
- package/src/compute/solve-pipeline.ts +279 -15
- package/src/definitions/__tests__/definition-service.test.ts +131 -0
- package/src/definitions/definition-service.ts +8 -4
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process cache of `.gh` definition bytes, keyed by immutable **version id**.
|
|
3
|
+
*
|
|
4
|
+
* The solve path used to eagerly `storage.get(fileKey)` on every request — moving
|
|
5
|
+
* multi-MB definition bytes off disk/S3 into memory before the scheduler could
|
|
6
|
+
* decide it didn't need them (the compute server already holds the definition via
|
|
7
|
+
* the pointer/`reuseServerDefinitionCache` fast path). This cache turns that eager
|
|
8
|
+
* read into a lazy `DefinitionRef.load()`: the scheduler calls `load()` ONLY when
|
|
9
|
+
* an upload is unavoidable, and even then a warm entry serves the bytes without
|
|
10
|
+
* touching storage. A pointer-known re-solve therefore moves ZERO definition bytes.
|
|
11
|
+
*
|
|
12
|
+
* Keying discipline (the whole reason this module exists):
|
|
13
|
+
* - The key is the **version id**, an immutable UUID — never a `fileKey`, which
|
|
14
|
+
* a delete-latest-then-reupload can REUSE for different content (see the
|
|
15
|
+
* monotonic-version-number fix). A reused key would serve one version's bytes
|
|
16
|
+
* for another: silent cache poisoning with no diagnostic. Callers pass
|
|
17
|
+
* `version.id`, and the returned `DefinitionRef.key` is that same id, so the
|
|
18
|
+
* scheduler's own result/pointer cache keys on the same immutable identity.
|
|
19
|
+
*
|
|
20
|
+
* Eviction:
|
|
21
|
+
* - LRU by **total byte budget**, not entry count — definitions range from tens
|
|
22
|
+
* of KB to hundreds of MB, so an entry-count cap would either pin gigabytes or
|
|
23
|
+
* evict uselessly. `COMPUTE_DEFINITION_BYTE_CACHE_MB` sets the budget; `0`
|
|
24
|
+
* disables the cache (every `getOrLoad` calls `load` and caches nothing).
|
|
25
|
+
* - No TTL: version ids are immutable, so a cached entry can never go stale.
|
|
26
|
+
* - An entry larger than the whole budget is served but never retained (caching
|
|
27
|
+
* it would immediately evict everything including itself).
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Build a definition-byte cache with a total-byte LRU budget.
|
|
31
|
+
*
|
|
32
|
+
* @param maxBytes total retained-byte budget; `0` (or negative) disables caching
|
|
33
|
+
* entirely — every `load()` calls the loader and nothing is retained.
|
|
34
|
+
*/
|
|
35
|
+
export function createDefinitionByteCache(maxBytes) {
|
|
36
|
+
const budget = Math.max(0, Math.floor(maxBytes));
|
|
37
|
+
// Map preserves insertion order → iteration order IS LRU order; re-inserting
|
|
38
|
+
// on access moves an entry to the most-recently-used (last) position.
|
|
39
|
+
const cache = new Map();
|
|
40
|
+
let retainedBytes = 0;
|
|
41
|
+
const stats = { hits: 0, misses: 0, evictions: 0 };
|
|
42
|
+
function touch(key, entry) {
|
|
43
|
+
cache.delete(key);
|
|
44
|
+
cache.set(key, entry);
|
|
45
|
+
}
|
|
46
|
+
function evictToFit() {
|
|
47
|
+
// Evict least-recently-used (first in insertion order) until within budget.
|
|
48
|
+
while (retainedBytes > budget && cache.size > 0) {
|
|
49
|
+
const oldestKey = cache.keys().next().value;
|
|
50
|
+
const oldest = cache.get(oldestKey);
|
|
51
|
+
cache.delete(oldestKey);
|
|
52
|
+
retainedBytes -= oldest.bytes.byteLength;
|
|
53
|
+
stats.evictions += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function store(key, bytes) {
|
|
57
|
+
if (budget === 0)
|
|
58
|
+
return;
|
|
59
|
+
// An entry bigger than the whole budget is never retained — caching it would
|
|
60
|
+
// evict everything (including itself) on the next `evictToFit`. Serve it
|
|
61
|
+
// through, cache nothing.
|
|
62
|
+
if (bytes.byteLength > budget)
|
|
63
|
+
return;
|
|
64
|
+
const existing = cache.get(key);
|
|
65
|
+
if (existing)
|
|
66
|
+
retainedBytes -= existing.bytes.byteLength;
|
|
67
|
+
cache.set(key, { bytes });
|
|
68
|
+
retainedBytes += bytes.byteLength;
|
|
69
|
+
evictToFit();
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
getOrLoad(versionId, load) {
|
|
73
|
+
const outcome = { loaded: false, fromCache: false };
|
|
74
|
+
return {
|
|
75
|
+
key: versionId,
|
|
76
|
+
outcome,
|
|
77
|
+
load: async () => {
|
|
78
|
+
const hit = cache.get(versionId);
|
|
79
|
+
if (hit) {
|
|
80
|
+
touch(versionId, hit);
|
|
81
|
+
stats.hits += 1;
|
|
82
|
+
outcome.loaded = true;
|
|
83
|
+
outcome.fromCache = true;
|
|
84
|
+
return hit.bytes;
|
|
85
|
+
}
|
|
86
|
+
stats.misses += 1;
|
|
87
|
+
outcome.loaded = true;
|
|
88
|
+
outcome.fromCache = false;
|
|
89
|
+
const bytes = await load();
|
|
90
|
+
store(versionId, bytes);
|
|
91
|
+
return bytes;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
stats() {
|
|
96
|
+
return {
|
|
97
|
+
hits: stats.hits,
|
|
98
|
+
misses: stats.misses,
|
|
99
|
+
evictions: stats.evictions,
|
|
100
|
+
entries: cache.size,
|
|
101
|
+
bytes: retainedBytes
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
clear() {
|
|
105
|
+
cache.clear();
|
|
106
|
+
retainedBytes = 0;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=definition-byte-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definition-byte-cache.js","sourceRoot":"","sources":["../../src/compute/definition-byte-cache.ts"],"names":[],"mappings":"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"}
|
package/dist/compute/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
export { resolveComputeLimits, readPositiveInt, readBool, type ComputeLimits, type EnvRecord } from './limits.js';
|
|
1
|
+
export { resolveComputeLimits, readPositiveInt, readNonNegativeInt, readBool, type ComputeLimits, type EnvRecord } from './limits.js';
|
|
2
|
+
export { createDefinitionByteCache, type DefinitionByteCache, type ByteCacheRef, type ByteCacheStats } from './definition-byte-cache.js';
|
|
3
|
+
export { createMemorySolveResultCache, type MemorySolveResultCache, type SolveCacheStats } from './memory-solve-cache.js';
|
|
4
|
+
export { deriveSolveCacheInputKey, type SolveCacheConfigSubset, type SolveCacheInputKey } from './solve-cache-key.js';
|
|
5
|
+
export { encodeSolveCacheEntry, decodeSolveCacheEntry, gunzipEntryBody, type EnvelopeHeader, type DecodedSolveCacheEntry } from './solve-cache-envelope.js';
|
|
6
|
+
export { createSolveCacheSingleFlight, type SolveCacheSingleFlight } from './solve-cache-single-flight.js';
|
|
2
7
|
export { createComputeRateLimiter, type ComputeRateLimiter, type ComputeRateLimiterConfig, type RateLimitResult } from './rate-limit.js';
|
|
3
8
|
export { isSafeRemoteDefinitionUrl, assertSafeRemoteDefinitionUrl } from './safe-url.js';
|
|
4
9
|
export { transformInputParameter } from './transform-input.js';
|
|
5
10
|
export { createClientCache, serverIdentity, type ClientCache, type ClientCacheConfig, type CachedClient, type ResolvedServer, type ServerIdentity } from './client-cache.js';
|
|
6
11
|
export { createRemoteDefinitionFetcher, readBodyWithCap, type RemoteDefinitionFetcher, type RemoteDefinitionConfig } from './remote-definition.js';
|
|
7
|
-
export { runSolvePipeline, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type SolvePipelineArgs, type SolveOutcome, type SolveEnvelope, type SolvePhaseMetrics, type PipelineInput } from './solve-pipeline.js';
|
|
12
|
+
export { runSolvePipeline, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER, type SolvePipelineArgs, type SolvePipelineCacheHook, type SolveOutcome, type SolveEnvelope, type SolvePhaseMetrics, type PipelineInput } from './solve-pipeline.js';
|
|
8
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compute/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACR,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACN,6BAA6B,EAC7B,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACN,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compute/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,oBAAoB,EACpB,eAAe,EACf,kBAAkB,EAClB,QAAQ,EACR,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,4BAA4B,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACN,wBAAwB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACN,qBAAqB,EACrB,qBAAqB,EACrB,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACN,4BAA4B,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EACN,iBAAiB,EACjB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACN,6BAA6B,EAC7B,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACN,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,MAAM,qBAAqB,CAAC"}
|
package/dist/compute/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Compute/solve server building blocks — transport-agnostic, env-injected.
|
|
2
|
-
export { resolveComputeLimits, readPositiveInt, readBool } from './limits.js';
|
|
2
|
+
export { resolveComputeLimits, readPositiveInt, readNonNegativeInt, readBool } from './limits.js';
|
|
3
|
+
export { createDefinitionByteCache } from './definition-byte-cache.js';
|
|
4
|
+
export { createMemorySolveResultCache } from './memory-solve-cache.js';
|
|
5
|
+
export { deriveSolveCacheInputKey } from './solve-cache-key.js';
|
|
6
|
+
export { encodeSolveCacheEntry, decodeSolveCacheEntry, gunzipEntryBody } from './solve-cache-envelope.js';
|
|
7
|
+
export { createSolveCacheSingleFlight } from './solve-cache-single-flight.js';
|
|
3
8
|
export { createComputeRateLimiter } from './rate-limit.js';
|
|
4
9
|
export { isSafeRemoteDefinitionUrl, assertSafeRemoteDefinitionUrl } from './safe-url.js';
|
|
5
10
|
export { transformInputParameter } from './transform-input.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/compute/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAE3E,OAAO,EACN,oBAAoB,EACpB,eAAe,EACf,QAAQ,EAGR,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,wBAAwB,EAIxB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EACN,iBAAiB,EACjB,cAAc,EAMd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACN,6BAA6B,EAC7B,eAAe,EAGf,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACN,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/compute/index.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAE3E,OAAO,EACN,oBAAoB,EACpB,eAAe,EACf,kBAAkB,EAClB,QAAQ,EAGR,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,yBAAyB,EAIzB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACN,4BAA4B,EAG5B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACN,wBAAwB,EAGxB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACN,qBAAqB,EACrB,qBAAqB,EACrB,eAAe,EAGf,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACN,4BAA4B,EAE5B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACN,wBAAwB,EAIxB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EACN,iBAAiB,EACjB,cAAc,EAMd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACN,6BAA6B,EAC7B,eAAe,EAGf,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACN,gBAAgB,EAChB,wBAAwB,EACxB,sBAAsB,EAOtB,MAAM,qBAAqB,CAAC"}
|
package/dist/compute/limits.d.ts
CHANGED
|
@@ -24,6 +24,13 @@ export type EnvRecord = Record<string, string | undefined>;
|
|
|
24
24
|
* + falls back when present but not a finite positive number.
|
|
25
25
|
*/
|
|
26
26
|
export declare function readPositiveInt(env: EnvRecord, name: string, fallback: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Parse a non-negative integer env value — like {@link readPositiveInt} but `0`
|
|
29
|
+
* is a valid value (used by knobs where `0` means "disable", e.g. a cache
|
|
30
|
+
* budget). Returns `fallback` when absent, and warns + falls back when present
|
|
31
|
+
* but not a finite non-negative number.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readNonNegativeInt(env: EnvRecord, name: string, fallback: number): number;
|
|
27
34
|
/**
|
|
28
35
|
* Parse a boolean env flag. Accepts `true/1/yes/on` (case-insensitive) as true
|
|
29
36
|
* and `false/0/no/off` as false; any other / absent value falls back.
|
|
@@ -102,6 +109,51 @@ export interface ComputeLimits {
|
|
|
102
109
|
* app instances each apply their own cap.
|
|
103
110
|
*/
|
|
104
111
|
computeMaxConcurrentSolves: number;
|
|
112
|
+
/**
|
|
113
|
+
* Backpressure — max solves allowed to WAIT in the per-server FIFO queue (i.e.
|
|
114
|
+
* excluding the `computeMaxConcurrentSolves` already in flight). A solve that
|
|
115
|
+
* arrives to a full queue is shed immediately (scheduler `QUEUE_FULL`, mapped
|
|
116
|
+
* to HTTP 503 + `Retry-After`) instead of piling up unbounded. `0` = unbounded
|
|
117
|
+
* (the pre-shedding default): the queue grows without limit, matching prior
|
|
118
|
+
* behavior. Size it to roughly 2–3× `computeMaxConcurrentSolves` once the
|
|
119
|
+
* Rhino pool's real capacity is known — shedding fast beats a hung request.
|
|
120
|
+
*/
|
|
121
|
+
computeMaxQueueDepth: number;
|
|
122
|
+
/**
|
|
123
|
+
* Backpressure — longest (ms) a solve may sit QUEUED before it starts
|
|
124
|
+
* executing; still-waiting past this it's shed (scheduler `QUEUE_TIMEOUT` →
|
|
125
|
+
* HTTP 503 + `Retry-After`) rather than burning compute on a stale request.
|
|
126
|
+
* Bounds tail latency: the scheduler's `timeoutMs` clock starts at execution,
|
|
127
|
+
* so without this a solve's total wait is unbounded and invisible. `0` = no
|
|
128
|
+
* queue deadline (the default). A sensible tuned value is ≈ `maxSolveDurationMs`.
|
|
129
|
+
*/
|
|
130
|
+
computeQueueWaitMs: number;
|
|
131
|
+
/**
|
|
132
|
+
* Total-byte budget (bytes) for the in-process definition-byte cache keyed on
|
|
133
|
+
* immutable version id. A warm entry lets a solve skip `storage.get` entirely,
|
|
134
|
+
* and a pointer-known solve never loads bytes at all. `0` disables the cache
|
|
135
|
+
* (every solve re-reads storage). Sized as a total-byte LRU because definitions
|
|
136
|
+
* span KB→hundreds of MB; env value is MB, converted here.
|
|
137
|
+
*/
|
|
138
|
+
computeDefinitionByteCacheBytes: number;
|
|
139
|
+
/**
|
|
140
|
+
* Default per-definition entry quota for the durable L2 solve cache (H1), used
|
|
141
|
+
* when a definition's `solveCacheLimit` is absent (inherit). A definition may
|
|
142
|
+
* override this; `0` on the definition turns caching off for it. This global
|
|
143
|
+
* default `0` means the L2 is effectively off until an operator sets a quota —
|
|
144
|
+
* safe because the memory backend also ships only under
|
|
145
|
+
* `SOLVE_CACHE_PROVIDER=memory`. Counts, not bytes: authors think in "how many
|
|
146
|
+
* solves to remember"; the byte backstop below is the operator's memory guard.
|
|
147
|
+
*/
|
|
148
|
+
solveCacheDefaultMaxEntries: number;
|
|
149
|
+
/**
|
|
150
|
+
* Global byte backstop (bytes) across ALL definitions' L2 entries, evicted
|
|
151
|
+
* global-LRU regardless of per-definition counts. Entries range KB→100s of MB,
|
|
152
|
+
* so a count quota alone can't bound memory. `0` disables the backstop (rely on
|
|
153
|
+
* per-definition counts only — not recommended in the memory backend). Env value
|
|
154
|
+
* is MB, converted here. On Redis this moves to `maxmemory allkeys-lru`.
|
|
155
|
+
*/
|
|
156
|
+
solveCacheMaxTotalBytes: number;
|
|
105
157
|
}
|
|
106
158
|
/**
|
|
107
159
|
* Resolve every server-side compute limit from an env map. Pure: the same env
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/compute/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAStF;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAQjF;AAED,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC7B;;;;;;;;;OASG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,CAAC;IAChC,yEAAyE;IACzE,wBAAwB,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,8BAA8B,EAAE,MAAM,CAAC;IACvC,yDAAyD;IACzD,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,2BAA2B,EAAE,OAAO,CAAC;IACrC;;;;OAIG;IACH,uBAAuB,EAAE,OAAO,CAAC;IACjC;;;;;OAKG;IACH,yBAAyB,EAAE,OAAO,CAAC;IACnC;;;;;;;;OAQG;IACH,0BAA0B,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/compute/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAStF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASzF;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAQjF;AAED,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC7B;;;;;;;;;OASG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,CAAC;IAChC,yEAAyE;IACzE,wBAAwB,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,8BAA8B,EAAE,MAAM,CAAC;IACvC,yDAAyD;IACzD,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,2BAA2B,EAAE,OAAO,CAAC;IACrC;;;;OAIG;IACH,uBAAuB,EAAE,OAAO,CAAC;IACjC;;;;;OAKG;IACH,yBAAyB,EAAE,OAAO,CAAC;IACnC;;;;;;;;OAQG;IACH,0BAA0B,EAAE,MAAM,CAAC;IACnC;;;;;;;;OAQG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;;OAOG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,+BAA+B,EAAE,MAAM,CAAC;IACxC;;;;;;;;OAQG;IACH,2BAA2B,EAAE,MAAM,CAAC;IACpC;;;;;;OAMG;IACH,uBAAuB,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,SAAS,GAAG,aAAa,CAwClE"}
|
package/dist/compute/limits.js
CHANGED
|
@@ -33,6 +33,23 @@ export function readPositiveInt(env, name, fallback) {
|
|
|
33
33
|
}
|
|
34
34
|
return Math.floor(parsed);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse a non-negative integer env value — like {@link readPositiveInt} but `0`
|
|
38
|
+
* is a valid value (used by knobs where `0` means "disable", e.g. a cache
|
|
39
|
+
* budget). Returns `fallback` when absent, and warns + falls back when present
|
|
40
|
+
* but not a finite non-negative number.
|
|
41
|
+
*/
|
|
42
|
+
export function readNonNegativeInt(env, name, fallback) {
|
|
43
|
+
const raw = env[name];
|
|
44
|
+
if (!raw)
|
|
45
|
+
return fallback;
|
|
46
|
+
const parsed = Number(raw);
|
|
47
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
48
|
+
console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
return Math.floor(parsed);
|
|
52
|
+
}
|
|
36
53
|
/**
|
|
37
54
|
* Parse a boolean env flag. Accepts `true/1/yes/on` (case-insensitive) as true
|
|
38
55
|
* and `false/0/no/off` as false; any other / absent value falls back.
|
|
@@ -72,7 +89,22 @@ export function resolveComputeLimits(env) {
|
|
|
72
89
|
computeServerCachesolve: readBool(env, 'COMPUTE_SERVER_CACHESOLVE', true),
|
|
73
90
|
computeCacheErroredSolves: readBool(env, 'COMPUTE_CACHE_ERRORED_SOLVES', false),
|
|
74
91
|
// 4 matches rhino.compute's default --childcount; tune to the actual VM.
|
|
75
|
-
computeMaxConcurrentSolves: readPositiveInt(env, 'COMPUTE_MAX_CONCURRENT', 4)
|
|
92
|
+
computeMaxConcurrentSolves: readPositiveInt(env, 'COMPUTE_MAX_CONCURRENT', 4),
|
|
93
|
+
// Both 0 (unbounded / no deadline) by default — nothing sheds until an
|
|
94
|
+
// operator who's measured their pool opts in. readNonNegativeInt so `0` is a
|
|
95
|
+
// valid "disabled" value, not treated as invalid.
|
|
96
|
+
computeMaxQueueDepth: readNonNegativeInt(env, 'COMPUTE_MAX_QUEUE_DEPTH', 0),
|
|
97
|
+
computeQueueWaitMs: readNonNegativeInt(env, 'COMPUTE_QUEUE_WAIT_MS', 0),
|
|
98
|
+
// Env is MB; 0 disables. Default 256 MB holds a handful of typical
|
|
99
|
+
// definitions warm without pinning gigabytes. readNonNegativeInt so `0`
|
|
100
|
+
// (disable) is honored rather than treated as invalid.
|
|
101
|
+
computeDefinitionByteCacheBytes: readNonNegativeInt(env, 'COMPUTE_DEFINITION_BYTE_CACHE_MB', 256) * MB,
|
|
102
|
+
// L2 durable solve cache (H1). Default per-definition quota 0 = inherit-to-off
|
|
103
|
+
// until an operator opts in; the memory backend also only mounts under
|
|
104
|
+
// SOLVE_CACHE_PROVIDER=memory, so nothing caches by accident. Byte backstop
|
|
105
|
+
// 512 MB caps total heap use across all definitions (env is MB).
|
|
106
|
+
solveCacheDefaultMaxEntries: readNonNegativeInt(env, 'SOLVE_CACHE_DEFAULT_MAX_ENTRIES', 0),
|
|
107
|
+
solveCacheMaxTotalBytes: readNonNegativeInt(env, 'SOLVE_CACHE_MAX_TOTAL_MB', 512) * MB
|
|
76
108
|
};
|
|
77
109
|
}
|
|
78
110
|
//# sourceMappingURL=limits.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/compute/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAKvB;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAc,EAAE,IAAY,EAAE,QAAgB;IAC7E,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,mBAAmB,IAAI,IAAI,GAAG,qBAAqB,QAAQ,EAAE,CAAC,CAAC;QAC5E,OAAO,QAAQ,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAc,EAAE,IAAY,EAAE,QAAiB;IACvE,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IAC/C,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACnC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACxE,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAC1E,OAAO,CAAC,IAAI,CAAC,mBAAmB,IAAI,IAAI,GAAG,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAC5E,OAAO,QAAQ,CAAC;AACjB,CAAC;
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/compute/limits.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `ISolveResultCache` — the first durable L2 backend (H1).
|
|
3
|
+
*
|
|
4
|
+
* Two eviction dimensions, both required (see CACHING.md H1):
|
|
5
|
+
*
|
|
6
|
+
* 1. **Per-definition count quota, LRU-within-definition.** Each definition
|
|
7
|
+
* gets its own budget of remembered solves (`maxEntriesForDefinition`,
|
|
8
|
+
* passed at write time). A slider-heavy definition churns ONLY its own
|
|
9
|
+
* entries — it can never evict another definition's. This is what makes a
|
|
10
|
+
* wide-input-space definition harmless structurally instead of via an
|
|
11
|
+
* off-switch.
|
|
12
|
+
* 2. **Global byte backstop, global-LRU.** Entries range KB→100s of MB, so a
|
|
13
|
+
* count quota can't bound heap. `maxTotalBytes` evicts the globally
|
|
14
|
+
* least-recently-used entry across every definition until total retained
|
|
15
|
+
* bytes fit. Authors think in counts; operators think in bytes; both hold.
|
|
16
|
+
*
|
|
17
|
+
* The key is `(orgId, definitionId, versionId, inputKey)`. `definitionId` scopes
|
|
18
|
+
* the count quota; the full tuple addresses the entry. Version keying means a
|
|
19
|
+
* publish is a fresh keyspace and there is nothing to invalidate — entries just
|
|
20
|
+
* age out under the two budgets.
|
|
21
|
+
*
|
|
22
|
+
* Best-effort contract: this never throws. A `get` miss and a `set` drop are both
|
|
23
|
+
* normal. Correctness never depends on a hit.
|
|
24
|
+
*/
|
|
25
|
+
import type { ISolveResultCache } from '@selvajs/platform';
|
|
26
|
+
/** Observability counters for the admin/debug surface (parallels ByteCacheStats). */
|
|
27
|
+
export interface SolveCacheStats {
|
|
28
|
+
/** `get` served a live entry. */
|
|
29
|
+
hits: number;
|
|
30
|
+
/** `get` found nothing. */
|
|
31
|
+
misses: number;
|
|
32
|
+
/** `set` stored a new (or replaced) entry. */
|
|
33
|
+
writes: number;
|
|
34
|
+
/** Entries dropped by the per-definition count quota. */
|
|
35
|
+
quotaEvictions: number;
|
|
36
|
+
/** Entries dropped by the global byte backstop. */
|
|
37
|
+
byteEvictions: number;
|
|
38
|
+
/** Current retained entry count across all definitions. */
|
|
39
|
+
entries: number;
|
|
40
|
+
/** Current retained bytes across all definitions. */
|
|
41
|
+
bytes: number;
|
|
42
|
+
}
|
|
43
|
+
/** A memory backend with its own stats, for wiring + observability. */
|
|
44
|
+
export interface MemorySolveResultCache extends ISolveResultCache {
|
|
45
|
+
stats(): SolveCacheStats;
|
|
46
|
+
/** Drop everything (test seam). */
|
|
47
|
+
clear(): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build an in-memory L2 solve cache.
|
|
51
|
+
*
|
|
52
|
+
* @param maxTotalBytes global retained-byte backstop; `0` (or negative) disables
|
|
53
|
+
* the backstop (per-definition counts are then the only bound — not recommended
|
|
54
|
+
* in-process). An entry larger than the whole backstop is stored-then-immediately
|
|
55
|
+
* evicted (served through, never retained) — same rule as the byte cache.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createMemorySolveResultCache(maxTotalBytes: number): MemorySolveResultCache;
|
|
58
|
+
//# sourceMappingURL=memory-solve-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-solve-cache.d.ts","sourceRoot":"","sources":["../../src/compute/memory-solve-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACX,iBAAiB,EAIjB,MAAM,mBAAmB,CAAC;AAE3B,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC/B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;CACd;AAED,uEAAuE;AACvE,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAChE,KAAK,IAAI,eAAe,CAAC;IACzB,mCAAmC;IACnC,KAAK,IAAI,IAAI,CAAC;CACd;AAOD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,sBAAsB,CAoI1F"}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-solve-cache.js","sourceRoot":"","sources":["../../src/compute/memory-solve-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAuCH;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAAC,aAAqB;IACjE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC;IAC1D,6EAA6E;IAC7E,6EAA6E;IAC7E,kEAAkE;IAClE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiB,CAAC;IACvC,8EAA8E;IAC9E,iFAAiF;IACjF,MAAM,YAAY,GAAG,IAAI,GAAG,EAAoB,CAAC;IACjD,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;IAErF,SAAS,UAAU,CAAC,GAAkB;QACrC,4EAA4E;QAC5E,wEAAwE;QACxE,OAAO,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC5E,CAAC;IAED,SAAS,OAAO,CAAC,YAAoB;QACpC,IAAI,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,EAAE,CAAC;YACX,IAAI,GAAG,EAAE,CAAC;YACV,YAAY,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC;IAED,SAAS,iBAAiB,CAAC,YAAoB,EAAE,CAAS;QACzD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,YAAY,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1D,CAAC;IAED,qEAAqE;IACrE,SAAS,IAAI,CAAC,CAAS;QACtB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChB,aAAa,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC;QACxC,iBAAiB,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,wFAAwF;IACxF,SAAS,KAAK,CAAC,CAAS,EAAE,KAAY;QACrC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChB,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACpB,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACzC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACd,CAAC;IAED,SAAS,UAAU,CAAC,YAAoB,EAAE,UAAkB;QAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;QACnC,0EAA0E;QAC1E,OAAO,IAAI,CAAC,MAAM,GAAG,UAAU,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YACvB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,gDAAgD;YAC9D,KAAK,CAAC,cAAc,IAAI,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IAED,SAAS,iBAAiB;QACzB,IAAI,UAAU,KAAK,CAAC;YAAE,OAAO;QAC7B,OAAO,aAAa,GAAG,UAAU,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACrD,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAe,CAAC;YACtD,IAAI,CAAC,SAAS,CAAC,CAAC;YAChB,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC;QAC1B,CAAC;IACF,CAAC;IAED,OAAO;QACN,KAAK,CAAC,GAAG,CAAC,IAAoB,EAAE,GAAkB;YACjD,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;YAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACZ,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;gBAClB,OAAO,IAAI,CAAC;YACb,CAAC;YACD,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YAChB,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;YAChB,OAAO,KAAK,CAAC,KAAK,CAAC;QACpB,CAAC;QAED,KAAK,CAAC,GAAG,CACR,IAAoB,EACpB,GAAkB,EAClB,KAAiB,EACjB,IAA0B;YAE1B,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;YACzE,2EAA2E;YAC3E,wEAAwE;YACxE,IAAI,UAAU,KAAK,CAAC;gBAAE,OAAO;YAE7B,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;YAC1B,uEAAuE;YACvE,IAAI,CAAC,CAAC,CAAC,CAAC;YAER,MAAM,KAAK,GAAU,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC;YAC/D,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClC,aAAa,IAAI,KAAK,CAAC,UAAU,CAAC;YAClC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;YAElB,wEAAwE;YACxE,yEAAyE;YACzE,0EAA0E;YAC1E,UAAU,CAAC,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YACzC,iBAAiB,EAAE,CAAC;QACrB,CAAC;QAED,KAAK;YACJ,OAAO;gBACN,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,aAAa,EAAE,KAAK,CAAC,aAAa;gBAClC,OAAO,EAAE,KAAK,CAAC,IAAI;gBACnB,KAAK,EAAE,aAAa;aACpB,CAAC;QACH,CAAC;QAED,KAAK;YACJ,KAAK,CAAC,KAAK,EAAE,CAAC;YACd,YAAY,CAAC,KAAK,EAAE,CAAC;YACrB,aAAa,GAAG,CAAC,CAAC;QACnB,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 cache entry format (R6). An entry is the gzipped response body plus a tiny
|
|
3
|
+
* metadata header, so a hit is near-CPU-free: no `JSON.stringify`, no re-solve —
|
|
4
|
+
* gzip clients get the stored bytes verbatim, and the header carries just enough
|
|
5
|
+
* to rebuild the `ok` outcome (error/warning counts for the metric, uncompressed
|
|
6
|
+
* length for the `Content-Length` on a gunzip fallback).
|
|
7
|
+
*
|
|
8
|
+
* Wire layout (opaque to the `ISolveResultCache` backend, which sees only bytes):
|
|
9
|
+
*
|
|
10
|
+
* [ 4 bytes: header length N, big-endian uint32 ]
|
|
11
|
+
* [ N bytes: UTF-8 JSON EnvelopeHeader ]
|
|
12
|
+
* [ rest: gzipped JSON response body ]
|
|
13
|
+
*
|
|
14
|
+
* The `algo` echo (M3) is already stripped at the source in the package's
|
|
15
|
+
* `runSolve`, so the body never carries the base64 definition — entries stay
|
|
16
|
+
* small without a strip step here.
|
|
17
|
+
*/
|
|
18
|
+
/** Metadata stored beside the gzipped body. Small; parsed on every hit. */
|
|
19
|
+
export interface EnvelopeHeader {
|
|
20
|
+
/** Grasshopper runtime error count, for the solve metric. */
|
|
21
|
+
errorCount: number;
|
|
22
|
+
/** Grasshopper runtime warning count, for the solve metric. */
|
|
23
|
+
warningCount: number;
|
|
24
|
+
/** Uncompressed JSON body length in bytes (Content-Length for a gunzip fallback). */
|
|
25
|
+
serializedBytes: number;
|
|
26
|
+
/**
|
|
27
|
+
* SHA-256 hex of the canonical input preimage (H2 defense-in-depth). A hit
|
|
28
|
+
* verifies this equals the requesting key's hash before serving — a wide-hash
|
|
29
|
+
* collision that produced the same storage key still can't serve the wrong
|
|
30
|
+
* geometry.
|
|
31
|
+
*/
|
|
32
|
+
inputHash: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Encode a cache entry from the gzipped body + header. The body MUST already be
|
|
36
|
+
* gzipped (the pipeline gzips before storing so every hit serves compressed
|
|
37
|
+
* bytes; a rare non-gzip client gunzips on read).
|
|
38
|
+
*/
|
|
39
|
+
export declare function encodeSolveCacheEntry(header: EnvelopeHeader, gzippedBody: Uint8Array): Uint8Array;
|
|
40
|
+
/** A decoded entry: the parsed header + the still-gzipped body slice. */
|
|
41
|
+
export interface DecodedSolveCacheEntry {
|
|
42
|
+
header: EnvelopeHeader;
|
|
43
|
+
gzippedBody: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decode a cache entry. Returns `null` on any malformed input (truncated, bad
|
|
47
|
+
* length prefix, non-JSON header) — a corrupt entry is treated as a miss, never
|
|
48
|
+
* an error, matching the best-effort cache contract.
|
|
49
|
+
*/
|
|
50
|
+
export declare function decodeSolveCacheEntry(bytes: Uint8Array): DecodedSolveCacheEntry | null;
|
|
51
|
+
/** Gunzip a stored body back to the JSON string, for a non-gzip client (rare). */
|
|
52
|
+
export declare function gunzipEntryBody(gzippedBody: Uint8Array): string;
|
|
53
|
+
//# sourceMappingURL=solve-cache-envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solve-cache-envelope.d.ts","sourceRoot":"","sources":["../../src/compute/solve-cache-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC9B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,GAAG,UAAU,CAUjG;AAED,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACtC,MAAM,EAAE,cAAc,CAAC;IACvB,WAAW,EAAE,UAAU,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,sBAAsB,GAAG,IAAI,CA2BtF;AAED,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,WAAW,EAAE,UAAU,GAAG,MAAM,CAE/D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 cache entry format (R6). An entry is the gzipped response body plus a tiny
|
|
3
|
+
* metadata header, so a hit is near-CPU-free: no `JSON.stringify`, no re-solve —
|
|
4
|
+
* gzip clients get the stored bytes verbatim, and the header carries just enough
|
|
5
|
+
* to rebuild the `ok` outcome (error/warning counts for the metric, uncompressed
|
|
6
|
+
* length for the `Content-Length` on a gunzip fallback).
|
|
7
|
+
*
|
|
8
|
+
* Wire layout (opaque to the `ISolveResultCache` backend, which sees only bytes):
|
|
9
|
+
*
|
|
10
|
+
* [ 4 bytes: header length N, big-endian uint32 ]
|
|
11
|
+
* [ N bytes: UTF-8 JSON EnvelopeHeader ]
|
|
12
|
+
* [ rest: gzipped JSON response body ]
|
|
13
|
+
*
|
|
14
|
+
* The `algo` echo (M3) is already stripped at the source in the package's
|
|
15
|
+
* `runSolve`, so the body never carries the base64 definition — entries stay
|
|
16
|
+
* small without a strip step here.
|
|
17
|
+
*/
|
|
18
|
+
import { gunzipSync } from 'node:zlib';
|
|
19
|
+
/**
|
|
20
|
+
* Encode a cache entry from the gzipped body + header. The body MUST already be
|
|
21
|
+
* gzipped (the pipeline gzips before storing so every hit serves compressed
|
|
22
|
+
* bytes; a rare non-gzip client gunzips on read).
|
|
23
|
+
*/
|
|
24
|
+
export function encodeSolveCacheEntry(header, gzippedBody) {
|
|
25
|
+
const headerJson = Buffer.from(JSON.stringify(header), 'utf8');
|
|
26
|
+
const out = Buffer.allocUnsafe(4 + headerJson.byteLength + gzippedBody.byteLength);
|
|
27
|
+
out.writeUInt32BE(headerJson.byteLength, 0);
|
|
28
|
+
headerJson.copy(out, 4);
|
|
29
|
+
Buffer.from(gzippedBody.buffer, gzippedBody.byteOffset, gzippedBody.byteLength).copy(out, 4 + headerJson.byteLength);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Decode a cache entry. Returns `null` on any malformed input (truncated, bad
|
|
34
|
+
* length prefix, non-JSON header) — a corrupt entry is treated as a miss, never
|
|
35
|
+
* an error, matching the best-effort cache contract.
|
|
36
|
+
*/
|
|
37
|
+
export function decodeSolveCacheEntry(bytes) {
|
|
38
|
+
if (bytes.byteLength < 4)
|
|
39
|
+
return null;
|
|
40
|
+
const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
41
|
+
const headerLen = buf.readUInt32BE(0);
|
|
42
|
+
const headerEnd = 4 + headerLen;
|
|
43
|
+
if (headerEnd > buf.byteLength)
|
|
44
|
+
return null;
|
|
45
|
+
let header;
|
|
46
|
+
try {
|
|
47
|
+
header = JSON.parse(buf.toString('utf8', 4, headerEnd));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (typeof header?.errorCount !== 'number' ||
|
|
53
|
+
typeof header?.warningCount !== 'number' ||
|
|
54
|
+
typeof header?.serializedBytes !== 'number' ||
|
|
55
|
+
typeof header?.inputHash !== 'string') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Slice (view, no copy) of the gzipped remainder.
|
|
59
|
+
const gzippedBody = new Uint8Array(bytes.buffer, bytes.byteOffset + headerEnd, bytes.byteLength - headerEnd);
|
|
60
|
+
return { header, gzippedBody };
|
|
61
|
+
}
|
|
62
|
+
/** Gunzip a stored body back to the JSON string, for a non-gzip client (rare). */
|
|
63
|
+
export function gunzipEntryBody(gzippedBody) {
|
|
64
|
+
return gunzipSync(gzippedBody).toString('utf8');
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=solve-cache-envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solve-cache-envelope.js","sourceRoot":"","sources":["../../src/compute/solve-cache-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAmBvC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAsB,EAAE,WAAuB;IACpF,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/D,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,UAAU,CAAC,UAAU,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACnF,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;IAC5C,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACxB,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,IAAI,CACnF,GAAG,EACH,CAAC,GAAG,UAAU,CAAC,UAAU,CACzB,CAAC;IACF,OAAO,GAAG,CAAC;AACZ,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAiB;IACtD,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,CAAC,GAAG,SAAS,CAAC;IAChC,IAAI,SAAS,GAAG,GAAG,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,MAAsB,CAAC;IAC3B,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAmB,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;IACD,IACC,OAAO,MAAM,EAAE,UAAU,KAAK,QAAQ;QACtC,OAAO,MAAM,EAAE,YAAY,KAAK,QAAQ;QACxC,OAAO,MAAM,EAAE,eAAe,KAAK,QAAQ;QAC3C,OAAO,MAAM,EAAE,SAAS,KAAK,QAAQ,EACpC,CAAC;QACF,OAAO,IAAI,CAAC;IACb,CAAC;IACD,kDAAkD;IAClD,MAAM,WAAW,GAAG,IAAI,UAAU,CACjC,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,UAAU,GAAG,SAAS,EAC5B,KAAK,CAAC,UAAU,GAAG,SAAS,CAC5B,CAAC;IACF,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AAChC,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,eAAe,CAAC,WAAuB;IACtD,OAAO,UAAU,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable L2 cache-key derivation (H2 / R8 / R13).
|
|
3
|
+
*
|
|
4
|
+
* The in-process L1 cache keys on a 32-bit FNV hash — fine for a 20-entry Map,
|
|
5
|
+
* unsafe for a durable, cross-user cache where a collision serves user A's
|
|
6
|
+
* geometry to user B (CONTEXT.md L92–97 records exactly this bug shipping once).
|
|
7
|
+
* So the L2 key is a wide SHA-256 hash, and the entry stores the canonical
|
|
8
|
+
* preimage so a hit can compare it byte-for-byte (defense-in-depth).
|
|
9
|
+
*
|
|
10
|
+
* What goes into the key:
|
|
11
|
+
* - The **transformed** input tree (R13) — the exact tree handed to the
|
|
12
|
+
* scheduler, canonicalized with the package's exported `stableStringify` so
|
|
13
|
+
* the L2 key parity-matches the L1 canonicalization path.
|
|
14
|
+
* - A **config subset** (R8): everything OTHER than definition+inputs that
|
|
15
|
+
* changes a solve's result — model units, tolerances, `dataversion`, the
|
|
16
|
+
* compute contract version, and (when known) the Rhino server identity. A
|
|
17
|
+
* durable key of `(versionId, inputHash)` alone would collide across
|
|
18
|
+
* differing server configs or Rhino versions.
|
|
19
|
+
*
|
|
20
|
+
* `versionId`, `definitionId`, and `orgId` are NOT hashed in here — they're
|
|
21
|
+
* separate parts of the {@link SolveCacheKey} tuple so the backend can scope its
|
|
22
|
+
* keyspace (per-definition quota, per-org isolation) without unpacking the hash.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Solve-affecting configuration folded into the key alongside the input tree.
|
|
26
|
+
* Every field here is something that, if it changed, would legitimately change
|
|
27
|
+
* the solve output for the SAME definition + inputs. Assemble it from the app's
|
|
28
|
+
* resolved solve config; unset fields are simply absent from the canonical form.
|
|
29
|
+
*/
|
|
30
|
+
export interface SolveCacheConfigSubset {
|
|
31
|
+
/** Rhino model unit system, if the app pins one (else the definition/server default). */
|
|
32
|
+
modelUnits?: string;
|
|
33
|
+
/** Absolute tolerance, if pinned. */
|
|
34
|
+
absoluteTolerance?: number;
|
|
35
|
+
/** Angle tolerance, if pinned. */
|
|
36
|
+
angleTolerance?: number;
|
|
37
|
+
/** Grasshopper `dataversion` / data-tree schema version, if pinned. */
|
|
38
|
+
dataVersion?: number | string;
|
|
39
|
+
/**
|
|
40
|
+
* Compute-server identity (id or URL). Folds in because two servers can run
|
|
41
|
+
* different Rhino/plugin versions that yield different geometry for the same
|
|
42
|
+
* inputs. Optional — omit for a single-pool deployment where it's constant.
|
|
43
|
+
*/
|
|
44
|
+
computeServerId?: string;
|
|
45
|
+
}
|
|
46
|
+
/** The canonical preimage + its SHA-256 digest. Store both in the L2 entry header. */
|
|
47
|
+
export interface SolveCacheInputKey {
|
|
48
|
+
/** The canonical string that was hashed (for hit-time byte comparison). */
|
|
49
|
+
canonical: string;
|
|
50
|
+
/** SHA-256 hex of {@link canonical} — the `inputKey` part of the storage key. */
|
|
51
|
+
hash: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Derive the durable `inputKey` from the transformed input tree and the solve
|
|
55
|
+
* config subset. Deterministic and stable across processes/instances: the same
|
|
56
|
+
* tree + config always produce the same hash, which is what lets a shared cache
|
|
57
|
+
* hit across app instances.
|
|
58
|
+
*
|
|
59
|
+
* @param transformedTree the tree AFTER `transformInputParameter`/`TreeBuilder`
|
|
60
|
+
* (R13) — the exact object handed to `scheduler.solve`.
|
|
61
|
+
* @param config solve-affecting config (R8); `COMPUTE_CONTRACT_VERSION` is folded
|
|
62
|
+
* in automatically so a wire-contract bump invalidates old entries for free.
|
|
63
|
+
*/
|
|
64
|
+
export declare function deriveSolveCacheInputKey(transformedTree: unknown, config: SolveCacheConfigSubset): SolveCacheInputKey;
|
|
65
|
+
//# sourceMappingURL=solve-cache-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solve-cache-key.d.ts","sourceRoot":"","sources":["../../src/compute/solve-cache-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACtC,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IAClC,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACvC,eAAe,EAAE,OAAO,EACxB,MAAM,EAAE,sBAAsB,GAC5B,kBAAkB,CAWpB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable L2 cache-key derivation (H2 / R8 / R13).
|
|
3
|
+
*
|
|
4
|
+
* The in-process L1 cache keys on a 32-bit FNV hash — fine for a 20-entry Map,
|
|
5
|
+
* unsafe for a durable, cross-user cache where a collision serves user A's
|
|
6
|
+
* geometry to user B (CONTEXT.md L92–97 records exactly this bug shipping once).
|
|
7
|
+
* So the L2 key is a wide SHA-256 hash, and the entry stores the canonical
|
|
8
|
+
* preimage so a hit can compare it byte-for-byte (defense-in-depth).
|
|
9
|
+
*
|
|
10
|
+
* What goes into the key:
|
|
11
|
+
* - The **transformed** input tree (R13) — the exact tree handed to the
|
|
12
|
+
* scheduler, canonicalized with the package's exported `stableStringify` so
|
|
13
|
+
* the L2 key parity-matches the L1 canonicalization path.
|
|
14
|
+
* - A **config subset** (R8): everything OTHER than definition+inputs that
|
|
15
|
+
* changes a solve's result — model units, tolerances, `dataversion`, the
|
|
16
|
+
* compute contract version, and (when known) the Rhino server identity. A
|
|
17
|
+
* durable key of `(versionId, inputHash)` alone would collide across
|
|
18
|
+
* differing server configs or Rhino versions.
|
|
19
|
+
*
|
|
20
|
+
* `versionId`, `definitionId`, and `orgId` are NOT hashed in here — they're
|
|
21
|
+
* separate parts of the {@link SolveCacheKey} tuple so the backend can scope its
|
|
22
|
+
* keyspace (per-definition quota, per-org isolation) without unpacking the hash.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
import { stableStringify } from '@selvajs/compute';
|
|
26
|
+
import { COMPUTE_CONTRACT_VERSION } from './solve-pipeline.js';
|
|
27
|
+
/**
|
|
28
|
+
* Derive the durable `inputKey` from the transformed input tree and the solve
|
|
29
|
+
* config subset. Deterministic and stable across processes/instances: the same
|
|
30
|
+
* tree + config always produce the same hash, which is what lets a shared cache
|
|
31
|
+
* hit across app instances.
|
|
32
|
+
*
|
|
33
|
+
* @param transformedTree the tree AFTER `transformInputParameter`/`TreeBuilder`
|
|
34
|
+
* (R13) — the exact object handed to `scheduler.solve`.
|
|
35
|
+
* @param config solve-affecting config (R8); `COMPUTE_CONTRACT_VERSION` is folded
|
|
36
|
+
* in automatically so a wire-contract bump invalidates old entries for free.
|
|
37
|
+
*/
|
|
38
|
+
export function deriveSolveCacheInputKey(transformedTree, config) {
|
|
39
|
+
// stableStringify sorts keys deterministically — same content, same string,
|
|
40
|
+
// regardless of property insertion order. Tag each part so a value can't shift
|
|
41
|
+
// across the tree/config boundary and collide (e.g. tree "]" meeting config "[").
|
|
42
|
+
const canonical = stableStringify({
|
|
43
|
+
v: COMPUTE_CONTRACT_VERSION,
|
|
44
|
+
tree: transformedTree,
|
|
45
|
+
config
|
|
46
|
+
});
|
|
47
|
+
const hash = createHash('sha256').update(canonical).digest('hex');
|
|
48
|
+
return { canonical, hash };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=solve-cache-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solve-cache-key.js","sourceRoot":"","sources":["../../src/compute/solve-cache-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAiC/D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CACvC,eAAwB,EACxB,MAA8B;IAE9B,4EAA4E;IAC5E,+EAA+E;IAC/E,kFAAkF;IAClF,MAAM,SAAS,GAAG,eAAe,CAAC;QACjC,CAAC,EAAE,wBAAwB;QAC3B,IAAI,EAAE,eAAe;QACrB,MAAM;KACN,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAClE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC"}
|