@selvajs/server 0.2.0-beta.2 → 0.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -0
  3. package/dist/compute/client-cache.d.ts +12 -0
  4. package/dist/compute/client-cache.d.ts.map +1 -1
  5. package/dist/compute/client-cache.js +18 -3
  6. package/dist/compute/client-cache.js.map +1 -1
  7. package/dist/compute/definition-byte-cache.d.ts +85 -0
  8. package/dist/compute/definition-byte-cache.d.ts.map +1 -0
  9. package/dist/compute/definition-byte-cache.js +110 -0
  10. package/dist/compute/definition-byte-cache.js.map +1 -0
  11. package/dist/compute/index.d.ts +7 -2
  12. package/dist/compute/index.d.ts.map +1 -1
  13. package/dist/compute/index.js +6 -1
  14. package/dist/compute/index.js.map +1 -1
  15. package/dist/compute/limits.d.ts +52 -0
  16. package/dist/compute/limits.d.ts.map +1 -1
  17. package/dist/compute/limits.js +33 -1
  18. package/dist/compute/limits.js.map +1 -1
  19. package/dist/compute/memory-solve-cache.d.ts +58 -0
  20. package/dist/compute/memory-solve-cache.d.ts.map +1 -0
  21. package/dist/compute/memory-solve-cache.js +0 -0
  22. package/dist/compute/memory-solve-cache.js.map +1 -0
  23. package/dist/compute/safe-url.d.ts.map +1 -1
  24. package/dist/compute/safe-url.js +16 -0
  25. package/dist/compute/safe-url.js.map +1 -1
  26. package/dist/compute/solve-cache-envelope.d.ts +53 -0
  27. package/dist/compute/solve-cache-envelope.d.ts.map +1 -0
  28. package/dist/compute/solve-cache-envelope.js +66 -0
  29. package/dist/compute/solve-cache-envelope.js.map +1 -0
  30. package/dist/compute/solve-cache-key.d.ts +65 -0
  31. package/dist/compute/solve-cache-key.d.ts.map +1 -0
  32. package/dist/compute/solve-cache-key.js +50 -0
  33. package/dist/compute/solve-cache-key.js.map +1 -0
  34. package/dist/compute/solve-cache-single-flight.d.ts +42 -0
  35. package/dist/compute/solve-cache-single-flight.d.ts.map +1 -0
  36. package/dist/compute/solve-cache-single-flight.js +50 -0
  37. package/dist/compute/solve-cache-single-flight.js.map +1 -0
  38. package/dist/compute/solve-pipeline.d.ts +71 -8
  39. package/dist/compute/solve-pipeline.d.ts.map +1 -1
  40. package/dist/compute/solve-pipeline.js +180 -8
  41. package/dist/compute/solve-pipeline.js.map +1 -1
  42. package/dist/definitions/definition-service.d.ts +1 -1
  43. package/dist/definitions/definition-service.d.ts.map +1 -1
  44. package/dist/definitions/definition-service.js +8 -4
  45. package/dist/definitions/definition-service.js.map +1 -1
  46. package/package.json +14 -18
  47. package/dist/access/__tests__/project-access-input.test.d.ts +0 -2
  48. package/dist/access/__tests__/project-access-input.test.d.ts.map +0 -1
  49. package/dist/access/__tests__/project-access-input.test.js +0 -146
  50. package/dist/access/__tests__/project-access-input.test.js.map +0 -1
  51. package/dist/compute/__tests__/client-cache.test.d.ts +0 -11
  52. package/dist/compute/__tests__/client-cache.test.d.ts.map +0 -1
  53. package/dist/compute/__tests__/client-cache.test.js +0 -200
  54. package/dist/compute/__tests__/client-cache.test.js.map +0 -1
  55. package/dist/compute/__tests__/rate-limit.test.d.ts +0 -2
  56. package/dist/compute/__tests__/rate-limit.test.d.ts.map +0 -1
  57. package/dist/compute/__tests__/rate-limit.test.js +0 -98
  58. package/dist/compute/__tests__/rate-limit.test.js.map +0 -1
  59. package/dist/compute/__tests__/remote-definition.test.d.ts +0 -11
  60. package/dist/compute/__tests__/remote-definition.test.d.ts.map +0 -1
  61. package/dist/compute/__tests__/remote-definition.test.js +0 -114
  62. package/dist/compute/__tests__/remote-definition.test.js.map +0 -1
  63. package/dist/compute/__tests__/safe-url.test.d.ts +0 -2
  64. package/dist/compute/__tests__/safe-url.test.d.ts.map +0 -1
  65. package/dist/compute/__tests__/safe-url.test.js +0 -71
  66. package/dist/compute/__tests__/safe-url.test.js.map +0 -1
  67. package/dist/compute/__tests__/solve-pipeline.test.d.ts +0 -11
  68. package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +0 -1
  69. package/dist/compute/__tests__/solve-pipeline.test.js +0 -292
  70. package/dist/compute/__tests__/solve-pipeline.test.js.map +0 -1
  71. package/dist/compute/__tests__/transform-input.test.d.ts +0 -14
  72. package/dist/compute/__tests__/transform-input.test.d.ts.map +0 -1
  73. package/dist/compute/__tests__/transform-input.test.js +0 -120
  74. package/dist/compute/__tests__/transform-input.test.js.map +0 -1
  75. package/dist/definitions/__tests__/load-for-render.test.d.ts +0 -8
  76. package/dist/definitions/__tests__/load-for-render.test.d.ts.map +0 -1
  77. package/dist/definitions/__tests__/load-for-render.test.js +0 -196
  78. package/dist/definitions/__tests__/load-for-render.test.js.map +0 -1
  79. package/dist/definitions/__tests__/schema-extraction.test.d.ts +0 -11
  80. package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +0 -1
  81. package/dist/definitions/__tests__/schema-extraction.test.js +0 -115
  82. package/dist/definitions/__tests__/schema-extraction.test.js.map +0 -1
  83. package/dist/http/__tests__/body-size.test.d.ts +0 -2
  84. package/dist/http/__tests__/body-size.test.d.ts.map +0 -1
  85. package/dist/http/__tests__/body-size.test.js +0 -24
  86. package/dist/http/__tests__/body-size.test.js.map +0 -1
  87. package/dist/http/__tests__/route-classifier.test.d.ts +0 -2
  88. package/dist/http/__tests__/route-classifier.test.d.ts.map +0 -1
  89. package/dist/http/__tests__/route-classifier.test.js +0 -54
  90. package/dist/http/__tests__/route-classifier.test.js.map +0 -1
  91. package/dist/http/__tests__/safe-redirect.test.d.ts +0 -2
  92. package/dist/http/__tests__/safe-redirect.test.d.ts.map +0 -1
  93. package/dist/http/__tests__/safe-redirect.test.js +0 -44
  94. package/dist/http/__tests__/safe-redirect.test.js.map +0 -1
  95. package/dist/http/__tests__/security-headers.test.d.ts +0 -2
  96. package/dist/http/__tests__/security-headers.test.d.ts.map +0 -1
  97. package/dist/http/__tests__/security-headers.test.js +0 -24
  98. package/dist/http/__tests__/security-headers.test.js.map +0 -1
  99. package/dist/ops/__tests__/semver.test.d.ts +0 -2
  100. package/dist/ops/__tests__/semver.test.d.ts.map +0 -1
  101. package/dist/ops/__tests__/semver.test.js +0 -38
  102. package/dist/ops/__tests__/semver.test.js.map +0 -1
  103. package/dist/providers/__tests__/create-selva-providers.test.d.ts +0 -7
  104. package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +0 -1
  105. package/dist/providers/__tests__/create-selva-providers.test.js +0 -135
  106. package/dist/providers/__tests__/create-selva-providers.test.js.map +0 -1
  107. package/dist/tokens/__tests__/token-codec.test.d.ts +0 -2
  108. package/dist/tokens/__tests__/token-codec.test.d.ts.map +0 -1
  109. package/dist/tokens/__tests__/token-codec.test.js +0 -50
  110. package/dist/tokens/__tests__/token-codec.test.js.map +0 -1
  111. package/src/access/__tests__/project-access-input.test.ts +0 -175
  112. package/src/access/index.ts +0 -9
  113. package/src/access/project-access-input.ts +0 -145
  114. package/src/compute/__tests__/client-cache.test.ts +0 -224
  115. package/src/compute/__tests__/rate-limit.test.ts +0 -106
  116. package/src/compute/__tests__/remote-definition.test.ts +0 -140
  117. package/src/compute/__tests__/safe-url.test.ts +0 -72
  118. package/src/compute/__tests__/solve-pipeline.test.ts +0 -341
  119. package/src/compute/__tests__/transform-input.test.ts +0 -162
  120. package/src/compute/client-cache.ts +0 -300
  121. package/src/compute/index.ts +0 -48
  122. package/src/compute/limits.ts +0 -159
  123. package/src/compute/rate-limit.ts +0 -110
  124. package/src/compute/remote-definition.ts +0 -149
  125. package/src/compute/safe-url.ts +0 -186
  126. package/src/compute/solve-pipeline.ts +0 -339
  127. package/src/compute/transform-input.ts +0 -66
  128. package/src/definitions/__tests__/load-for-render.test.ts +0 -244
  129. package/src/definitions/__tests__/schema-extraction.test.ts +0 -137
  130. package/src/definitions/definition-service.ts +0 -243
  131. package/src/definitions/index.ts +0 -22
  132. package/src/definitions/load-for-render.ts +0 -272
  133. package/src/definitions/schema-extraction.ts +0 -117
  134. package/src/errors/SentryErrorReporter.ts +0 -88
  135. package/src/errors/index.ts +0 -3
  136. package/src/http/__tests__/body-size.test.ts +0 -27
  137. package/src/http/__tests__/route-classifier.test.ts +0 -60
  138. package/src/http/__tests__/safe-redirect.test.ts +0 -47
  139. package/src/http/__tests__/security-headers.test.ts +0 -29
  140. package/src/http/body-size.ts +0 -24
  141. package/src/http/index.ts +0 -10
  142. package/src/http/redirect.ts +0 -16
  143. package/src/http/route-classifier.ts +0 -74
  144. package/src/http/security-headers.ts +0 -42
  145. package/src/index.ts +0 -27
  146. package/src/ops/__tests__/semver.test.ts +0 -42
  147. package/src/ops/index.ts +0 -3
  148. package/src/ops/semver.ts +0 -60
  149. package/src/providers/__tests__/create-selva-providers.test.ts +0 -172
  150. package/src/providers/__tests__/fixtures/override-config.mjs +0 -8
  151. package/src/providers/create-selva-providers.ts +0 -241
  152. package/src/providers/index.ts +0 -12
  153. package/src/tokens/__tests__/token-codec.test.ts +0 -63
  154. package/src/tokens/index.ts +0 -8
  155. package/src/tokens/token-codec.ts +0 -86
@@ -1,300 +0,0 @@
1
- import {
2
- GrasshopperClient,
3
- enableDebugLogging,
4
- type GrasshopperComputeConfig,
5
- type SolveScheduler
6
- } from '@selvajs/compute';
7
-
8
- // ============================================================================
9
- // Shared compute client cache
10
- // ============================================================================
11
- //
12
- // One warm `GrasshopperClient` (+ its `SolveScheduler`) per distinct compute
13
- // server, keyed by the server's **`id`** (ADR 0004 — a server's identity is its
14
- // `id`, never its URL). Definitions can pin different servers (the app's
15
- // `resolveServerForOrg` honors a per-definition pin → org default → global
16
- // default), so this is deliberately a per-server LRU, NOT a single shared
17
- // client — two definitions on two servers keep two warm clients; a churn of
18
- // one-off servers is bounded by `maxCachedClients`.
19
- //
20
- // Both hot paths share this cache:
21
- // - the solve endpoint uses the entry's `scheduler`;
22
- // - the definition-viewer render path uses the entry's `client` for `getIO`.
23
- // Sharing means a definition that was just solved renders from the same warm
24
- // client (and vice-versa) instead of re-handshaking Rhino.Compute per page load.
25
- //
26
- // Staleness: keyed on `id`, a rotated URL/apiKey keeps the SAME key with stale
27
- // connection details — so the config-write path MUST call `evict(id)` when a
28
- // server's config changes (ADR 0004 Consequences). That replaces the old
29
- // implicit "new key → LRU age-out" staleness the URL-keyed cache relied on.
30
-
31
- /**
32
- * Opaque identity of a compute server. Callers never construct the branded
33
- * string directly — they pass a resolved server config to `serverIdentity()`.
34
- * Keeping this opaque from day one (ADR 0004 D1) means "a server is now a pool
35
- * of URLs behind one id" stays an additive, non-breaking change.
36
- */
37
- export type ServerIdentity = string & { readonly __brand: 'ServerIdentity' };
38
-
39
- /** Minimal resolved-server shape the cache needs (a subset of the app's `ComputeServerConfig`). */
40
- export interface ResolvedServer {
41
- /** Stable identity — the cache key. */
42
- id: string;
43
- /** Base URL of the Rhino.Compute instance (a resolution detail of `id`). */
44
- serverUrl: string;
45
- /** Sent as `RhinoComputeKey`. */
46
- apiKey?: string;
47
- }
48
-
49
- /** Derive the opaque cache identity from a resolved server. Identity is the `id`. */
50
- export function serverIdentity(server: Pick<ResolvedServer, 'id'>): ServerIdentity {
51
- return server.id as ServerIdentity;
52
- }
53
-
54
- /**
55
- * Per-request telemetry holders read by the caller right after a solve resolves
56
- * to build a Server-Timing header. Populated by the client/scheduler callbacks.
57
- */
58
- export interface CachedClient {
59
- client: GrasshopperClient;
60
- scheduler: SolveScheduler;
61
- /**
62
- * Last decode/solve/encode reported by the compute server (Server-Timing),
63
- * written by onServerTiming. `seq` increments on every write. The scheduler
64
- * runs up to `maxConcurrentSolves` solves at once, so `last` alone can't be
65
- * trusted per-request: callers snapshot `seq` before their solve and
66
- * attribute `last` only when exactly one write happened since (necessarily
67
- * theirs — see the guard in `runSolvePipeline`), dropping the segment
68
- * otherwise instead of misattributing another request's timing.
69
- */
70
- rhinoTiming: { last: { decode: number; solve: number; encode: number } | null; seq: number };
71
- /**
72
- * Last solve's cache verdicts from the scheduler's onSettle: whether the
73
- * in-process response cache served it (no compute call) and whether the
74
- * definition had to be re-uploaded. `seq` increments on EVERY settle
75
- * (success or error); `last` is only written on success. Same
76
- * snapshot-and-attribute pattern as rhinoTiming — onSettle always fires
77
- * before the corresponding `scheduler.solve()` promise resolves, so at the
78
- * caller's read point its own settle is included in `seq`.
79
- */
80
- solveMeta: {
81
- last: { fromCache: boolean; definitionReuploaded?: boolean } | null;
82
- seq: number;
83
- };
84
- }
85
-
86
- /**
87
- * Config injected by the consuming app. Everything that used to be read from
88
- * `$env` / `computeLimits` in the app module is passed in, so this module stays
89
- * env-agnostic and testable.
90
- */
91
- export interface ClientCacheConfig {
92
- /** Per-solve timeout forwarded to the scheduler (`ComputeLimits.maxSolveDurationMs`). */
93
- maxSolveDurationMs: number;
94
- /** Ask Rhino.Compute to cache solve results and return them on identical repeats. */
95
- cachesolve: boolean;
96
- /** Also cache solves that reported GH errors (only meaningful with `cachesolve`). */
97
- cacheerroredsolves: boolean;
98
- /** Reference large definitions by server cache key (pointer) instead of re-uploading. */
99
- reuseServerDefinitionCache: boolean;
100
- /**
101
- * Max in-flight solves per compute server (scheduler `maxConcurrent`; excess
102
- * FIFO-queues). Size to the server's `compute.geometry` child count
103
- * (`ComputeLimits.computeMaxConcurrentSolves`).
104
- */
105
- maxConcurrentSolves: number;
106
- /** Concise cache/timing logs. When false, `onDebugLog` is never invoked. */
107
- debug: boolean;
108
- /** VERBOSE lib-level logging (full solve request/response incl. geometry). */
109
- debugVerbose: boolean;
110
- /** Max distinct warm clients before the LRU evicts the oldest. Default 16. */
111
- maxCachedClients?: number;
112
- /** Sink for the concise debug lines (the app wires `console.log`). */
113
- onDebugLog?: (message: string) => void;
114
- }
115
-
116
- export interface ClientCache {
117
- /**
118
- * Get (or create) the warm client + scheduler for a resolved compute server,
119
- * keyed by its `id`. `definitionGuid`, when present, is stamped as the
120
- * `X-Selva-Definition` header on this client's outbound solve/IO requests —
121
- * inert routing/telemetry metadata until a pool router exists (ADR 0004 D2).
122
- */
123
- getClient(server: ResolvedServer, opts?: { definitionGuid?: string }): Promise<CachedClient>;
124
- /**
125
- * Dispose and drop the warm client for `id` (or a `ServerIdentity`). Called by
126
- * the config-write path when a server's URL/key rotates so the next request
127
- * rebuilds against fresh connection details (ADR 0004 Consequences).
128
- */
129
- evict(id: string | ServerIdentity): void;
130
- /** Dispose every warm client. Test seam / shutdown hook. */
131
- disposeAll(): void;
132
- }
133
-
134
- const DEFAULT_MAX_CACHED_CLIENTS = 16;
135
-
136
- /**
137
- * Build a per-server warm-client cache. Each cache owns its own LRU `Map` keyed
138
- * on server `id`.
139
- */
140
- export function createClientCache(config: ClientCacheConfig): ClientCache {
141
- const maxCachedClients = config.maxCachedClients ?? DEFAULT_MAX_CACHED_CLIENTS;
142
- const cache = new Map<string, CachedClient>();
143
-
144
- if (config.debugVerbose) enableDebugLogging();
145
-
146
- const debugLog = (message: string) => {
147
- if (config.debug) config.onDebugLog?.(message);
148
- };
149
-
150
- async function build(server: ResolvedServer, definitionGuid?: string): Promise<CachedClient> {
151
- // Holders for per-request results from the client/scheduler callbacks; the
152
- // caller reads them right after each solve resolves to build Server-Timing,
153
- // using the seq counters to detect (and drop) ambiguous concurrent writes.
154
- const rhinoTiming: CachedClient['rhinoTiming'] = { last: null, seq: 0 };
155
- const solveMeta: CachedClient['solveMeta'] = { last: null, seq: 0 };
156
-
157
- const clientConfig: GrasshopperComputeConfig = {
158
- serverUrl: server.serverUrl,
159
- apiKey: server.apiKey,
160
- // Lib-level debug is the verbose one (full payload logs); keep it on the
161
- // verbose flag so the concise cache logs can run without the noise.
162
- debug: config.debugVerbose,
163
- // Ask Rhino.Compute to cache solve results and return them on identical
164
- // repeats (same definition + inputs), skipping the solve.
165
- cachesolve: config.cachesolve,
166
- // Opt-in: also cache solves that reported GH errors.
167
- cacheerroredsolves: config.cacheerroredsolves,
168
- // DEBUG: the server's per-request timing breakdown, which exposes the two
169
- // SERVER-side caches at once — definition cache (pointer reuse) via
170
- // `decode`, solve cache (cachesolve) via `solve`. Flagged below so the
171
- // contrast is obvious in the logs.
172
- onServerTiming: (t) => {
173
- const decode = t.decode ?? 0;
174
- const solve = t.solve ?? 0;
175
- const encode = t.encode ?? 0;
176
- // Always recorded (cheap): the caller reads this after scheduler.solve.
177
- rhinoTiming.seq += 1;
178
- rhinoTiming.last = { decode, solve, encode };
179
- if (!config.debug) return;
180
- // Heuristic thresholds: a genuine parse/solve is many ms; a cache hit
181
- // is sub-millisecond. Only meaningful relative to the first (cold) solve.
182
- const defCache =
183
- decode <= 1 ? 'HIT (definition reused, no re-upload/parse)' : 'miss (full parse)';
184
- const solveCache =
185
- config.cachesolve && solve <= 1
186
- ? 'HIT (cached result, solve skipped)'
187
- : config.cachesolve
188
- ? 'miss (solved fresh)'
189
- : 'off';
190
- debugLog(
191
- `[Compute/rhino-cache] decode=${decode}ms solve=${solve}ms encode=${encode}ms ` +
192
- `| definition-cache: ${defCache} | solve-cache (cachesolve): ${solveCache}`
193
- );
194
- }
195
- };
196
-
197
- // `X-Selva-Definition` on the wire (ADR 0004 D2). Baked at client-create
198
- // time because the scheduler has no per-request header hook — inert for a
199
- // single-member "pool", useful in compute-side access logs immediately.
200
- if (definitionGuid) {
201
- clientConfig.headers = { 'X-Selva-Definition': definitionGuid };
202
- }
203
-
204
- const client = await GrasshopperClient.create(clientConfig);
205
- const scheduler = client.createScheduler({
206
- mode: 'queue',
207
- // Queue mode defaults maxConcurrent to 1, which would serialize every
208
- // solve on this server through a single slot while the compute VM's
209
- // other compute.geometry children idle (audit B6) — always pass it.
210
- maxConcurrent: config.maxConcurrentSolves,
211
- timeoutMs: config.maxSolveDurationMs,
212
- cache: { maxEntries: 20, ttlMs: 5 * 60_000 },
213
- // Solve large definitions by server cache-key (pointer) instead of
214
- // re-uploading the full binary every solve. On a stale-pointer miss the
215
- // client transparently re-uploads.
216
- reuseServerDefinitionCache: config.reuseServerDefinitionCache,
217
- // DEBUG: observe the in-process response cache. `fromCache === true` means
218
- // this solve was served from the app's own cache — Rhino.Compute was never
219
- // called. `false` means it went to the compute server.
220
- onSettle: (_ctx, result) => {
221
- // Count EVERY settle (success or error) so the caller's attribution
222
- // guard sees all concurrent activity, not just successful writes.
223
- solveMeta.seq += 1;
224
- if (result.status !== 'success') return;
225
- // Always recorded (cheap): the caller surfaces these on Server-Timing.
226
- solveMeta.last = {
227
- fromCache: result.fromCache === true,
228
- definitionReuploaded: result.definitionReuploaded
229
- };
230
- if (!config.debug) return;
231
- debugLog(
232
- `[Compute/selva-cache] ${result.fromCache ? 'HIT — served from cache (no compute call)' : 'miss — went to Rhino.Compute'} (${Math.round(result.durationMs)}ms)`
233
- );
234
- // Definition-cache verdict for a real compute call (not a cache HIT).
235
- if (!result.fromCache) {
236
- debugLog(
237
- result.definitionReuploaded === true
238
- ? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'
239
- : result.definitionReuploaded === false
240
- ? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'
241
- : '[Compute/def-cache] n/a — reuse disabled or non-reusable definition'
242
- );
243
- }
244
- // Surface real GH errors when present. An errored solve is NEVER cached
245
- // server-side (cachesolve only stores error-free solves).
246
- if (!result.fromCache) {
247
- const errs = (result.response as { errors?: unknown[] })?.errors;
248
- const errCount = Array.isArray(errs) ? errs.length : 0;
249
- if (errCount > 0) {
250
- debugLog(
251
- `[Compute/rhino-cache] ${errCount} GH error(s); not cacheable until fixed. First: ${JSON.stringify((errs as unknown[])[0])}`
252
- );
253
- }
254
- }
255
- }
256
- });
257
- return { client, scheduler, rhinoTiming, solveMeta };
258
- }
259
-
260
- return {
261
- async getClient(server, opts): Promise<CachedClient> {
262
- const key = server.id;
263
- const existing = cache.get(key);
264
- if (existing) {
265
- // Refresh LRU position.
266
- cache.delete(key);
267
- cache.set(key, existing);
268
- return existing;
269
- }
270
-
271
- const entry = await build(server, opts?.definitionGuid);
272
-
273
- // Evict the least-recently-used entry before inserting when at capacity.
274
- if (cache.size >= maxCachedClients) {
275
- const oldestKey = cache.keys().next().value;
276
- if (oldestKey !== undefined) {
277
- cache.get(oldestKey)?.scheduler.dispose();
278
- cache.delete(oldestKey);
279
- }
280
- }
281
-
282
- cache.set(key, entry);
283
- return entry;
284
- },
285
-
286
- evict(id): void {
287
- const key = id as string;
288
- const entry = cache.get(key);
289
- if (entry) {
290
- entry.scheduler.dispose();
291
- cache.delete(key);
292
- }
293
- },
294
-
295
- disposeAll(): void {
296
- for (const entry of cache.values()) entry.scheduler.dispose();
297
- cache.clear();
298
- }
299
- };
300
- }
@@ -1,48 +0,0 @@
1
- // Compute/solve server building blocks — transport-agnostic, env-injected.
2
-
3
- export {
4
- resolveComputeLimits,
5
- readPositiveInt,
6
- readBool,
7
- type ComputeLimits,
8
- type EnvRecord
9
- } from './limits.js';
10
-
11
- export {
12
- createComputeRateLimiter,
13
- type ComputeRateLimiter,
14
- type ComputeRateLimiterConfig,
15
- type RateLimitResult
16
- } from './rate-limit.js';
17
-
18
- export { isSafeRemoteDefinitionUrl, assertSafeRemoteDefinitionUrl } from './safe-url.js';
19
-
20
- export { transformInputParameter } from './transform-input.js';
21
-
22
- export {
23
- createClientCache,
24
- serverIdentity,
25
- type ClientCache,
26
- type ClientCacheConfig,
27
- type CachedClient,
28
- type ResolvedServer,
29
- type ServerIdentity
30
- } from './client-cache.js';
31
-
32
- export {
33
- createRemoteDefinitionFetcher,
34
- readBodyWithCap,
35
- type RemoteDefinitionFetcher,
36
- type RemoteDefinitionConfig
37
- } from './remote-definition.js';
38
-
39
- export {
40
- runSolvePipeline,
41
- COMPUTE_CONTRACT_VERSION,
42
- COMPUTE_VERSION_HEADER,
43
- type SolvePipelineArgs,
44
- type SolveOutcome,
45
- type SolveEnvelope,
46
- type SolvePhaseMetrics,
47
- type PipelineInput
48
- } from './solve-pipeline.js';
@@ -1,159 +0,0 @@
1
- /**
2
- * Compute/solve server limits, resolved from an injected env map.
3
- *
4
- * This is the transport- and framework-agnostic core: it takes a plain
5
- * `Record<string, string | undefined>` (the caller passes SvelteKit's
6
- * `$env/dynamic/private`, `process.env`, or any config source) and returns a
7
- * fully-resolved `ComputeLimits` object. No ambient `$env` / `process.env`
8
- * read happens here, so the module is importable from any runtime and testable
9
- * without env plumbing.
10
- *
11
- * Every knob is env-overridable so deployments can tune without code changes;
12
- * the defaults are sized for typical interactive use (sliders + small uploads).
13
- * Centralizing them surfaces the tradeoffs (raise the rate cap → also consider
14
- * the body cap) at one glance.
15
- *
16
- * Client-side counterparts (the in-flight compute throttle and slider debounce)
17
- * live in `@selvajs/ui` and are bundled into the client, so they can't read
18
- * env — they carry their own defaults.
19
- */
20
-
21
- const MB = 1024 * 1024;
22
-
23
- /** Env source: any string→string map. `$env/dynamic/private` satisfies this. */
24
- export type EnvRecord = Record<string, string | undefined>;
25
-
26
- /**
27
- * Parse a positive integer env value. Returns `fallback` when absent, and warns
28
- * + falls back when present but not a finite positive number.
29
- */
30
- export function readPositiveInt(env: EnvRecord, name: string, fallback: number): number {
31
- const raw = env[name];
32
- if (!raw) return fallback;
33
- const parsed = Number(raw);
34
- if (!Number.isFinite(parsed) || parsed <= 0) {
35
- console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
36
- return fallback;
37
- }
38
- return Math.floor(parsed);
39
- }
40
-
41
- /**
42
- * Parse a boolean env flag. Accepts `true/1/yes/on` (case-insensitive) as true
43
- * and `false/0/no/off` as false; any other / absent value falls back.
44
- */
45
- export function readBool(env: EnvRecord, name: string, fallback: boolean): boolean {
46
- const raw = env[name];
47
- if (raw == null || raw === '') return fallback;
48
- const v = raw.trim().toLowerCase();
49
- if (v === 'true' || v === '1' || v === 'yes' || v === 'on') return true;
50
- if (v === 'false' || v === '0' || v === 'no' || v === 'off') return false;
51
- console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
52
- return fallback;
53
- }
54
-
55
- /** Fully-resolved server-side compute limits. */
56
- export interface ComputeLimits {
57
- /**
58
- * Longest the app waits for one solve. Enforced by the SolveScheduler
59
- * (`@selvajs/compute`), which propagates AbortSignal into the upstream
60
- * Compute call — a timeout actually cancels the work. Forwarded to the
61
- * client to drive the in-browser AbortController.
62
- *
63
- * Caveat: bounds only the parts of the stack we own. A reverse proxy or
64
- * serverless platform cap may shoot the request sooner; bumping past those
65
- * caps produces 502s, not longer solves.
66
- */
67
- maxSolveDurationMs: number;
68
- /** Fixed-window cap on /api/compute: window length + max requests per window. */
69
- rateLimitWindowMs: number;
70
- rateLimitMaxRequests: number;
71
- /**
72
- * Largest .gh definition accepted on upload AND the largest remote definition
73
- * fetched for compute — kept in lockstep so a remote URL can't smuggle a file
74
- * past the upload cap.
75
- */
76
- maxGhFileSize: number;
77
- maxImageFileSize: number;
78
- /**
79
- * /api/compute JSON *request* body cap (inputs + values, not the .gh). A
80
- * `file`-widget input embeds geometry as base64 in `values`; must stay <=
81
- * the adapter-node global BODY_SIZE_LIMIT or the global backstop rejects first.
82
- */
83
- computeRequestMaxBytes: number;
84
- /**
85
- * /api/compute JSON *response* cap. A `file`-typed output is base64-embedded
86
- * in the response; guards V8's ~512 MB single-string wall (a `JSON.stringify`
87
- * `RangeError`) and browser-tab OOM. Defensive backstop → clear 413 instead of
88
- * an opaque crash (real fix is out-of-band streaming, ADR 0003).
89
- */
90
- computeResponseMaxBytes: number;
91
- /** Hard cap on fetching a remote definition — tracks `maxGhFileSize`. */
92
- remoteDefinitionMaxBytes: number;
93
- /** Deadline on fetching a remote definition (slow-loris protection). */
94
- remoteDefinitionFetchTimeoutMs: number;
95
- /** In-process cache TTL for remote-fetched .gh bytes. */
96
- definitionCacheTtlMs: number;
97
- /**
98
- * Reuse the server's cached definition via a pointer instead of re-uploading
99
- * the binary each solve. SAFE only on a compute server that signals a
100
- * stale-pointer miss (VektorNode fork / a fork that throws) — a server that
101
- * returns an empty 200 would silently yield EMPTY geometry. Off for unknown
102
- * / standard rhino.compute.
103
- */
104
- computeReuseDefinitionCache: boolean;
105
- /**
106
- * Ask Rhino.Compute to cache solve RESULTS keyed on the full request and
107
- * return a cached result on an identical repeat. Server-wide (survives Selva
108
- * restarts, spans instances). Only helps IDENTICAL re-solves.
109
- */
110
- computeServerCachesolve: boolean;
111
- /**
112
- * Also cache solves that reported Grasshopper errors — correct for
113
- * error-by-design definitions (guarded Python, pruned branches) that still
114
- * produce valid geometry. Requires `computeServerCachesolve` and a server
115
- * honoring the flag. Conservative default off.
116
- */
117
- computeCacheErroredSolves: boolean;
118
- /**
119
- * Max in-flight solves this app instance sends to ONE compute server
120
- * (forwarded as the SolveScheduler's `maxConcurrent`; excess solves FIFO-queue).
121
- * Size it to the server's `compute.geometry` child count — Rhino.Compute
122
- * spawns N children per VM precisely to run N solves concurrently, and a
123
- * lower value idles that capacity while users queue (audit B6: the scheduler's
124
- * queue-mode default is 1, which serialized ALL solves per server). Multiple
125
- * app instances each apply their own cap.
126
- */
127
- computeMaxConcurrentSolves: number;
128
- }
129
-
130
- /**
131
- * Resolve every server-side compute limit from an env map. Pure: the same env
132
- * always yields the same limits, and nothing is read ambiently. The app calls
133
- * this once at its composition root with `$env/dynamic/private`.
134
- */
135
- export function resolveComputeLimits(env: EnvRecord): ComputeLimits {
136
- const maxGhFileSize = readPositiveInt(env, 'MAX_GH_FILE_SIZE_BYTES', 300 * MB);
137
- return {
138
- maxSolveDurationMs: readPositiveInt(env, 'MAX_SOLVE_DURATION_MS', 100_000),
139
- rateLimitWindowMs: readPositiveInt(env, 'COMPUTE_RATE_LIMIT_WINDOW_MS', 100_000),
140
- rateLimitMaxRequests: readPositiveInt(env, 'COMPUTE_RATE_LIMIT_MAX', 120),
141
- maxGhFileSize,
142
- maxImageFileSize: readPositiveInt(env, 'MAX_IMAGE_FILE_SIZE_BYTES', 10 * MB),
143
- computeRequestMaxBytes: readPositiveInt(env, 'COMPUTE_REQUEST_MAX_BYTES', 300 * MB),
144
- computeResponseMaxBytes: readPositiveInt(env, 'COMPUTE_RESPONSE_MAX_BYTES', 300 * MB),
145
- // Tracks the upload cap so a remote URL can't smuggle a larger file.
146
- remoteDefinitionMaxBytes: maxGhFileSize,
147
- remoteDefinitionFetchTimeoutMs: readPositiveInt(
148
- env,
149
- 'REMOTE_DEFINITION_FETCH_TIMEOUT_MS',
150
- 30_000
151
- ),
152
- definitionCacheTtlMs: readPositiveInt(env, 'DEFINITION_CACHE_TTL_MS', 5 * 60 * 1000),
153
- computeReuseDefinitionCache: readBool(env, 'COMPUTE_REUSE_DEFINITION_CACHE', true),
154
- computeServerCachesolve: readBool(env, 'COMPUTE_SERVER_CACHESOLVE', true),
155
- computeCacheErroredSolves: readBool(env, 'COMPUTE_CACHE_ERRORED_SOLVES', false),
156
- // 4 matches rhino.compute's default --childcount; tune to the actual VM.
157
- computeMaxConcurrentSolves: readPositiveInt(env, 'COMPUTE_MAX_CONCURRENT', 4)
158
- };
159
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * Per-key fixed-window rate limiter for the compute-solve endpoint. Compute
3
- * solves are the one expensive endpoint in a Selva-engine app — without a cap a
4
- * single authenticated user (or share-link consumer) can burn through compute
5
- * budget far faster than any other surface allows.
6
- *
7
- * Design:
8
- * - Process-local `Map`. Multi-instance deployments see N× the per-key rate —
9
- * acceptable as a first line; a distributed limiter is a follow-up. The state
10
- * lives behind this factory so a shared-store implementation can slot in
11
- * later without changing the call sites.
12
- * - Fixed window (vs sliding) — simpler, no per-request bookkeeping; the
13
- * "burst at window edges" mode is fine here (share-link `maxSolves` and
14
- * per-user auth already prevent unbounded abuse — this flattens short spikes).
15
- * - Keyed by caller — the app passes `user:{userId}` for authenticated solves,
16
- * `share:{linkId}` for share-token solves, so anonymous consumers of one link
17
- * don't share a bucket with the link's owner.
18
- *
19
- * The window + cap are injected (they live in `ComputeLimits`), so this module
20
- * reads no env of its own.
21
- *
22
- * Not compute-only despite the name: `peek` + `clear` make it usable for
23
- * failure-counting flows too (login limiting keys by IP, records only failed
24
- * attempts via `check`, and forgives on success via `clear`).
25
- */
26
-
27
- export interface RateLimitResult {
28
- allowed: boolean;
29
- /** Seconds until the bucket resets, when not allowed. */
30
- retryAfter?: number;
31
- }
32
-
33
- export interface ComputeRateLimiterConfig {
34
- /** Fixed-window length in ms. */
35
- windowMs: number;
36
- /** Max requests admitted per key per window. */
37
- maxPerWindow: number;
38
- }
39
-
40
- export interface ComputeRateLimiter {
41
- /**
42
- * Check + record one request against `key`. Increments the counter on allow.
43
- * Returns `{ allowed: false, retryAfter }` once the bucket is full.
44
- */
45
- check(key: string): RateLimitResult;
46
- /**
47
- * Check WITHOUT recording. For flows that only count selected outcomes —
48
- * e.g. login limiting, where `peek` gates the attempt and `check` records
49
- * only failures (a success calls `clear` instead).
50
- */
51
- peek(key: string): RateLimitResult;
52
- /** Drop `key`'s bucket — e.g. a successful login forgives prior failures. */
53
- clear(key: string): void;
54
- /** Test seam — wipes in-memory state. Production code never calls this. */
55
- reset(): void;
56
- /** The resolved config, exposed for time-based test assertions. */
57
- readonly config: Readonly<ComputeRateLimiterConfig>;
58
- }
59
-
60
- interface BucketEntry {
61
- count: number;
62
- resetAt: number;
63
- }
64
-
65
- /**
66
- * Build a process-local fixed-window rate limiter. Each limiter owns its own
67
- * bucket `Map`, so distinct endpoints/keys-spaces don't collide.
68
- */
69
- export function createComputeRateLimiter(config: ComputeRateLimiterConfig): ComputeRateLimiter {
70
- const { windowMs, maxPerWindow } = config;
71
- const buckets = new Map<string, BucketEntry>();
72
-
73
- return {
74
- config,
75
- check(key: string): RateLimitResult {
76
- const now = Date.now();
77
- const entry = buckets.get(key);
78
-
79
- if (!entry || now > entry.resetAt) {
80
- buckets.set(key, { count: 1, resetAt: now + windowMs });
81
- return { allowed: true };
82
- }
83
-
84
- if (entry.count >= maxPerWindow) {
85
- return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
86
- }
87
-
88
- entry.count += 1;
89
- return { allowed: true };
90
- },
91
- peek(key: string): RateLimitResult {
92
- const now = Date.now();
93
- const entry = buckets.get(key);
94
-
95
- if (!entry || now > entry.resetAt) {
96
- return { allowed: true };
97
- }
98
- if (entry.count >= maxPerWindow) {
99
- return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
100
- }
101
- return { allowed: true };
102
- },
103
- clear(key: string): void {
104
- buckets.delete(key);
105
- },
106
- reset(): void {
107
- buckets.clear();
108
- }
109
- };
110
- }