@selvajs/server 0.2.0-beta.1

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/dist/compute/__tests__/client-cache.test.d.ts +11 -0
  3. package/dist/compute/__tests__/client-cache.test.d.ts.map +1 -0
  4. package/dist/compute/__tests__/client-cache.test.js +149 -0
  5. package/dist/compute/__tests__/client-cache.test.js.map +1 -0
  6. package/dist/compute/__tests__/rate-limit.test.d.ts +2 -0
  7. package/dist/compute/__tests__/rate-limit.test.d.ts.map +1 -0
  8. package/dist/compute/__tests__/rate-limit.test.js +62 -0
  9. package/dist/compute/__tests__/rate-limit.test.js.map +1 -0
  10. package/dist/compute/__tests__/remote-definition.test.d.ts +11 -0
  11. package/dist/compute/__tests__/remote-definition.test.d.ts.map +1 -0
  12. package/dist/compute/__tests__/remote-definition.test.js +114 -0
  13. package/dist/compute/__tests__/remote-definition.test.js.map +1 -0
  14. package/dist/compute/__tests__/safe-url.test.d.ts +2 -0
  15. package/dist/compute/__tests__/safe-url.test.d.ts.map +1 -0
  16. package/dist/compute/__tests__/safe-url.test.js +71 -0
  17. package/dist/compute/__tests__/safe-url.test.js.map +1 -0
  18. package/dist/compute/__tests__/solve-pipeline.test.d.ts +11 -0
  19. package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +1 -0
  20. package/dist/compute/__tests__/solve-pipeline.test.js +245 -0
  21. package/dist/compute/__tests__/solve-pipeline.test.js.map +1 -0
  22. package/dist/compute/__tests__/transform-input.test.d.ts +14 -0
  23. package/dist/compute/__tests__/transform-input.test.d.ts.map +1 -0
  24. package/dist/compute/__tests__/transform-input.test.js +114 -0
  25. package/dist/compute/__tests__/transform-input.test.js.map +1 -0
  26. package/dist/compute/client-cache.d.ts +104 -0
  27. package/dist/compute/client-cache.d.ts.map +1 -0
  28. package/dist/compute/client-cache.js +147 -0
  29. package/dist/compute/client-cache.js.map +1 -0
  30. package/dist/compute/index.d.ts +8 -0
  31. package/dist/compute/index.d.ts.map +1 -0
  32. package/dist/compute/index.js +9 -0
  33. package/dist/compute/index.js.map +1 -0
  34. package/dist/compute/limits.d.ts +102 -0
  35. package/dist/compute/limits.d.ts.map +1 -0
  36. package/dist/compute/limits.js +76 -0
  37. package/dist/compute/limits.js.map +1 -0
  38. package/dist/compute/rate-limit.d.ts +49 -0
  39. package/dist/compute/rate-limit.d.ts.map +1 -0
  40. package/dist/compute/rate-limit.js +49 -0
  41. package/dist/compute/rate-limit.js.map +1 -0
  42. package/dist/compute/remote-definition.d.ts +53 -0
  43. package/dist/compute/remote-definition.d.ts.map +1 -0
  44. package/dist/compute/remote-definition.js +110 -0
  45. package/dist/compute/remote-definition.js.map +1 -0
  46. package/dist/compute/safe-url.d.ts +15 -0
  47. package/dist/compute/safe-url.d.ts.map +1 -0
  48. package/dist/compute/safe-url.js +195 -0
  49. package/dist/compute/safe-url.js.map +1 -0
  50. package/dist/compute/solve-pipeline.d.ts +127 -0
  51. package/dist/compute/solve-pipeline.d.ts.map +1 -0
  52. package/dist/compute/solve-pipeline.js +195 -0
  53. package/dist/compute/solve-pipeline.js.map +1 -0
  54. package/dist/compute/transform-input.d.ts +20 -0
  55. package/dist/compute/transform-input.d.ts.map +1 -0
  56. package/dist/compute/transform-input.js +47 -0
  57. package/dist/compute/transform-input.js.map +1 -0
  58. package/dist/definitions/__tests__/load-for-render.test.d.ts +8 -0
  59. package/dist/definitions/__tests__/load-for-render.test.d.ts.map +1 -0
  60. package/dist/definitions/__tests__/load-for-render.test.js +196 -0
  61. package/dist/definitions/__tests__/load-for-render.test.js.map +1 -0
  62. package/dist/definitions/__tests__/schema-extraction.test.d.ts +11 -0
  63. package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +1 -0
  64. package/dist/definitions/__tests__/schema-extraction.test.js +115 -0
  65. package/dist/definitions/__tests__/schema-extraction.test.js.map +1 -0
  66. package/dist/definitions/definition-service.d.ts +77 -0
  67. package/dist/definitions/definition-service.d.ts.map +1 -0
  68. package/dist/definitions/definition-service.js +184 -0
  69. package/dist/definitions/definition-service.js.map +1 -0
  70. package/dist/definitions/index.d.ts +4 -0
  71. package/dist/definitions/index.d.ts.map +1 -0
  72. package/dist/definitions/index.js +8 -0
  73. package/dist/definitions/index.js.map +1 -0
  74. package/dist/definitions/load-for-render.d.ts +61 -0
  75. package/dist/definitions/load-for-render.d.ts.map +1 -0
  76. package/dist/definitions/load-for-render.js +165 -0
  77. package/dist/definitions/load-for-render.js.map +1 -0
  78. package/dist/definitions/schema-extraction.d.ts +22 -0
  79. package/dist/definitions/schema-extraction.d.ts.map +1 -0
  80. package/dist/definitions/schema-extraction.js +89 -0
  81. package/dist/definitions/schema-extraction.js.map +1 -0
  82. package/dist/index.d.ts +18 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +18 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/providers/__tests__/create-selva-providers.test.d.ts +7 -0
  87. package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +1 -0
  88. package/dist/providers/__tests__/create-selva-providers.test.js +135 -0
  89. package/dist/providers/__tests__/create-selva-providers.test.js.map +1 -0
  90. package/dist/providers/create-selva-providers.d.ts +53 -0
  91. package/dist/providers/create-selva-providers.d.ts.map +1 -0
  92. package/dist/providers/create-selva-providers.js +152 -0
  93. package/dist/providers/create-selva-providers.js.map +1 -0
  94. package/dist/providers/index.d.ts +2 -0
  95. package/dist/providers/index.d.ts.map +1 -0
  96. package/dist/providers/index.js +6 -0
  97. package/dist/providers/index.js.map +1 -0
  98. package/package.json +60 -0
  99. package/src/compute/__tests__/client-cache.test.ts +164 -0
  100. package/src/compute/__tests__/rate-limit.test.ts +69 -0
  101. package/src/compute/__tests__/remote-definition.test.ts +140 -0
  102. package/src/compute/__tests__/safe-url.test.ts +72 -0
  103. package/src/compute/__tests__/solve-pipeline.test.ts +294 -0
  104. package/src/compute/__tests__/transform-input.test.ts +155 -0
  105. package/src/compute/client-cache.ts +278 -0
  106. package/src/compute/index.ts +48 -0
  107. package/src/compute/limits.ts +147 -0
  108. package/src/compute/rate-limit.ts +83 -0
  109. package/src/compute/remote-definition.ts +149 -0
  110. package/src/compute/safe-url.ts +186 -0
  111. package/src/compute/solve-pipeline.ts +324 -0
  112. package/src/compute/transform-input.ts +52 -0
  113. package/src/definitions/__tests__/load-for-render.test.ts +244 -0
  114. package/src/definitions/__tests__/schema-extraction.test.ts +137 -0
  115. package/src/definitions/definition-service.ts +243 -0
  116. package/src/definitions/index.ts +22 -0
  117. package/src/definitions/load-for-render.ts +272 -0
  118. package/src/definitions/schema-extraction.ts +117 -0
  119. package/src/index.ts +18 -0
  120. package/src/providers/__tests__/create-selva-providers.test.ts +172 -0
  121. package/src/providers/__tests__/fixtures/override-config.mjs +8 -0
  122. package/src/providers/create-selva-providers.ts +241 -0
  123. package/src/providers/index.ts +12 -0
@@ -0,0 +1,147 @@
1
+ import { GrasshopperClient, enableDebugLogging } from '@selvajs/compute';
2
+ /** Derive the opaque cache identity from a resolved server. Identity is the `id`. */
3
+ export function serverIdentity(server) {
4
+ return server.id;
5
+ }
6
+ const DEFAULT_MAX_CACHED_CLIENTS = 16;
7
+ /**
8
+ * Build a per-server warm-client cache. Each cache owns its own LRU `Map` keyed
9
+ * on server `id`.
10
+ */
11
+ export function createClientCache(config) {
12
+ const maxCachedClients = config.maxCachedClients ?? DEFAULT_MAX_CACHED_CLIENTS;
13
+ const cache = new Map();
14
+ if (config.debugVerbose)
15
+ enableDebugLogging();
16
+ const debugLog = (message) => {
17
+ if (config.debug)
18
+ config.onDebugLog?.(message);
19
+ };
20
+ async function build(server, definitionGuid) {
21
+ // Holders for per-request results from the client/scheduler callbacks; the
22
+ // caller reads them right after each solve resolves to build Server-Timing.
23
+ const rhinoTiming = { last: null };
24
+ const solveMeta = { last: null };
25
+ const clientConfig = {
26
+ serverUrl: server.serverUrl,
27
+ apiKey: server.apiKey,
28
+ // Lib-level debug is the verbose one (full payload logs); keep it on the
29
+ // verbose flag so the concise cache logs can run without the noise.
30
+ debug: config.debugVerbose,
31
+ // Ask Rhino.Compute to cache solve results and return them on identical
32
+ // repeats (same definition + inputs), skipping the solve.
33
+ cachesolve: config.cachesolve,
34
+ // Opt-in: also cache solves that reported GH errors.
35
+ cacheerroredsolves: config.cacheerroredsolves,
36
+ // DEBUG: the server's per-request timing breakdown, which exposes the two
37
+ // SERVER-side caches at once — definition cache (pointer reuse) via
38
+ // `decode`, solve cache (cachesolve) via `solve`. Flagged below so the
39
+ // contrast is obvious in the logs.
40
+ onServerTiming: (t) => {
41
+ const decode = t.decode ?? 0;
42
+ const solve = t.solve ?? 0;
43
+ const encode = t.encode ?? 0;
44
+ // Always recorded (cheap): the caller reads this after scheduler.solve.
45
+ rhinoTiming.last = { decode, solve, encode };
46
+ if (!config.debug)
47
+ return;
48
+ // Heuristic thresholds: a genuine parse/solve is many ms; a cache hit
49
+ // is sub-millisecond. Only meaningful relative to the first (cold) solve.
50
+ const defCache = decode <= 1 ? 'HIT (definition reused, no re-upload/parse)' : 'miss (full parse)';
51
+ const solveCache = config.cachesolve && solve <= 1
52
+ ? 'HIT (cached result, solve skipped)'
53
+ : config.cachesolve
54
+ ? 'miss (solved fresh)'
55
+ : 'off';
56
+ debugLog(`[Compute/rhino-cache] decode=${decode}ms solve=${solve}ms encode=${encode}ms ` +
57
+ `| definition-cache: ${defCache} | solve-cache (cachesolve): ${solveCache}`);
58
+ }
59
+ };
60
+ // `X-Selva-Definition` on the wire (ADR 0004 D2). Baked at client-create
61
+ // time because the scheduler has no per-request header hook — inert for a
62
+ // single-member "pool", useful in compute-side access logs immediately.
63
+ if (definitionGuid) {
64
+ clientConfig.headers = { 'X-Selva-Definition': definitionGuid };
65
+ }
66
+ const client = await GrasshopperClient.create(clientConfig);
67
+ const scheduler = client.createScheduler({
68
+ mode: 'queue',
69
+ timeoutMs: config.maxSolveDurationMs,
70
+ cache: { maxEntries: 20, ttlMs: 5 * 60_000 },
71
+ // Solve large definitions by server cache-key (pointer) instead of
72
+ // re-uploading the full binary every solve. On a stale-pointer miss the
73
+ // client transparently re-uploads.
74
+ reuseServerDefinitionCache: config.reuseServerDefinitionCache,
75
+ // DEBUG: observe the in-process response cache. `fromCache === true` means
76
+ // this solve was served from the app's own cache — Rhino.Compute was never
77
+ // called. `false` means it went to the compute server.
78
+ onSettle: (_ctx, result) => {
79
+ if (result.status !== 'success')
80
+ return;
81
+ // Always recorded (cheap): the caller surfaces these on Server-Timing.
82
+ solveMeta.last = {
83
+ fromCache: result.fromCache === true,
84
+ definitionReuploaded: result.definitionReuploaded
85
+ };
86
+ if (!config.debug)
87
+ return;
88
+ debugLog(`[Compute/selva-cache] ${result.fromCache ? 'HIT — served from cache (no compute call)' : 'miss — went to Rhino.Compute'} (${Math.round(result.durationMs)}ms)`);
89
+ // Definition-cache verdict for a real compute call (not a cache HIT).
90
+ if (!result.fromCache) {
91
+ debugLog(result.definitionReuploaded === true
92
+ ? '[Compute/def-cache] miss — definition RE-UPLOADED (pointer was cold/stale)'
93
+ : result.definitionReuploaded === false
94
+ ? '[Compute/def-cache] HIT — definition reused from server cache (no upload)'
95
+ : '[Compute/def-cache] n/a — reuse disabled or non-reusable definition');
96
+ }
97
+ // Surface real GH errors when present. An errored solve is NEVER cached
98
+ // server-side (cachesolve only stores error-free solves).
99
+ if (!result.fromCache) {
100
+ const errs = result.response?.errors;
101
+ const errCount = Array.isArray(errs) ? errs.length : 0;
102
+ if (errCount > 0) {
103
+ debugLog(`[Compute/rhino-cache] ${errCount} GH error(s); not cacheable until fixed. First: ${JSON.stringify(errs[0])}`);
104
+ }
105
+ }
106
+ }
107
+ });
108
+ return { client, scheduler, rhinoTiming, solveMeta };
109
+ }
110
+ return {
111
+ async getClient(server, opts) {
112
+ const key = server.id;
113
+ const existing = cache.get(key);
114
+ if (existing) {
115
+ // Refresh LRU position.
116
+ cache.delete(key);
117
+ cache.set(key, existing);
118
+ return existing;
119
+ }
120
+ const entry = await build(server, opts?.definitionGuid);
121
+ // Evict the least-recently-used entry before inserting when at capacity.
122
+ if (cache.size >= maxCachedClients) {
123
+ const oldestKey = cache.keys().next().value;
124
+ if (oldestKey !== undefined) {
125
+ cache.get(oldestKey)?.scheduler.dispose();
126
+ cache.delete(oldestKey);
127
+ }
128
+ }
129
+ cache.set(key, entry);
130
+ return entry;
131
+ },
132
+ evict(id) {
133
+ const key = id;
134
+ const entry = cache.get(key);
135
+ if (entry) {
136
+ entry.scheduler.dispose();
137
+ cache.delete(key);
138
+ }
139
+ },
140
+ disposeAll() {
141
+ for (const entry of cache.values())
142
+ entry.scheduler.dispose();
143
+ cache.clear();
144
+ }
145
+ };
146
+ }
147
+ //# sourceMappingURL=client-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-cache.js","sourceRoot":"","sources":["../../src/compute/client-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { resolveComputeLimits, readPositiveInt, readBool, type ComputeLimits, type EnvRecord } from './limits.js';
2
+ export { createComputeRateLimiter, type ComputeRateLimiter, type ComputeRateLimiterConfig, type RateLimitResult } from './rate-limit.js';
3
+ export { isSafeRemoteDefinitionUrl, assertSafeRemoteDefinitionUrl } from './safe-url.js';
4
+ export { transformInputParameter } from './transform-input.js';
5
+ export { createClientCache, serverIdentity, type ClientCache, type ClientCacheConfig, type CachedClient, type ResolvedServer, type ServerIdentity } from './client-cache.js';
6
+ 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';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,9 @@
1
+ // Compute/solve server building blocks — transport-agnostic, env-injected.
2
+ export { resolveComputeLimits, readPositiveInt, readBool } from './limits.js';
3
+ export { createComputeRateLimiter } from './rate-limit.js';
4
+ export { isSafeRemoteDefinitionUrl, assertSafeRemoteDefinitionUrl } from './safe-url.js';
5
+ export { transformInputParameter } from './transform-input.js';
6
+ export { createClientCache, serverIdentity } from './client-cache.js';
7
+ export { createRemoteDefinitionFetcher, readBodyWithCap } from './remote-definition.js';
8
+ export { runSolvePipeline, COMPUTE_CONTRACT_VERSION, COMPUTE_VERSION_HEADER } from './solve-pipeline.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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,EAMtB,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,102 @@
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
+ /** Env source: any string→string map. `$env/dynamic/private` satisfies this. */
21
+ export type EnvRecord = Record<string, string | undefined>;
22
+ /**
23
+ * Parse a positive integer env value. Returns `fallback` when absent, and warns
24
+ * + falls back when present but not a finite positive number.
25
+ */
26
+ export declare function readPositiveInt(env: EnvRecord, name: string, fallback: number): number;
27
+ /**
28
+ * Parse a boolean env flag. Accepts `true/1/yes/on` (case-insensitive) as true
29
+ * and `false/0/no/off` as false; any other / absent value falls back.
30
+ */
31
+ export declare function readBool(env: EnvRecord, name: string, fallback: boolean): boolean;
32
+ /** Fully-resolved server-side compute limits. */
33
+ export interface ComputeLimits {
34
+ /**
35
+ * Longest the app waits for one solve. Enforced by the SolveScheduler
36
+ * (`@selvajs/compute`), which propagates AbortSignal into the upstream
37
+ * Compute call — a timeout actually cancels the work. Forwarded to the
38
+ * client to drive the in-browser AbortController.
39
+ *
40
+ * Caveat: bounds only the parts of the stack we own. A reverse proxy or
41
+ * serverless platform cap may shoot the request sooner; bumping past those
42
+ * caps produces 502s, not longer solves.
43
+ */
44
+ maxSolveDurationMs: number;
45
+ /** Fixed-window cap on /api/compute: window length + max requests per window. */
46
+ rateLimitWindowMs: number;
47
+ rateLimitMaxRequests: number;
48
+ /**
49
+ * Largest .gh definition accepted on upload AND the largest remote definition
50
+ * fetched for compute — kept in lockstep so a remote URL can't smuggle a file
51
+ * past the upload cap.
52
+ */
53
+ maxGhFileSize: number;
54
+ maxImageFileSize: number;
55
+ /**
56
+ * /api/compute JSON *request* body cap (inputs + values, not the .gh). A
57
+ * `file`-widget input embeds geometry as base64 in `values`; must stay <=
58
+ * the adapter-node global BODY_SIZE_LIMIT or the global backstop rejects first.
59
+ */
60
+ computeRequestMaxBytes: number;
61
+ /**
62
+ * /api/compute JSON *response* cap. A `file`-typed output is base64-embedded
63
+ * in the response; guards V8's ~512 MB single-string wall (a `JSON.stringify`
64
+ * `RangeError`) and browser-tab OOM. Defensive backstop → clear 413 instead of
65
+ * an opaque crash (real fix is out-of-band streaming, ADR 0003).
66
+ */
67
+ computeResponseMaxBytes: number;
68
+ /** Hard cap on fetching a remote definition — tracks `maxGhFileSize`. */
69
+ remoteDefinitionMaxBytes: number;
70
+ /** Deadline on fetching a remote definition (slow-loris protection). */
71
+ remoteDefinitionFetchTimeoutMs: number;
72
+ /** In-process cache TTL for remote-fetched .gh bytes. */
73
+ definitionCacheTtlMs: number;
74
+ /**
75
+ * Reuse the server's cached definition via a pointer instead of re-uploading
76
+ * the binary each solve. SAFE only on a compute server that signals a
77
+ * stale-pointer miss (VektorNode fork / a fork that throws) — a server that
78
+ * returns an empty 200 would silently yield EMPTY geometry. Off for unknown
79
+ * / standard rhino.compute.
80
+ */
81
+ computeReuseDefinitionCache: boolean;
82
+ /**
83
+ * Ask Rhino.Compute to cache solve RESULTS keyed on the full request and
84
+ * return a cached result on an identical repeat. Server-wide (survives Selva
85
+ * restarts, spans instances). Only helps IDENTICAL re-solves.
86
+ */
87
+ computeServerCachesolve: boolean;
88
+ /**
89
+ * Also cache solves that reported Grasshopper errors — correct for
90
+ * error-by-design definitions (guarded Python, pruned branches) that still
91
+ * produce valid geometry. Requires `computeServerCachesolve` and a server
92
+ * honoring the flag. Conservative default off.
93
+ */
94
+ computeCacheErroredSolves: boolean;
95
+ }
96
+ /**
97
+ * Resolve every server-side compute limit from an env map. Pure: the same env
98
+ * always yields the same limits, and nothing is read ambiently. The app calls
99
+ * this once at its composition root with `$env/dynamic/private`.
100
+ */
101
+ export declare function resolveComputeLimits(env: EnvRecord): ComputeLimits;
102
+ //# sourceMappingURL=limits.d.ts.map
@@ -0,0 +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;CACnC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,SAAS,GAAG,aAAa,CAsBlE"}
@@ -0,0 +1,76 @@
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
+ const MB = 1024 * 1024;
21
+ /**
22
+ * Parse a positive integer env value. Returns `fallback` when absent, and warns
23
+ * + falls back when present but not a finite positive number.
24
+ */
25
+ export function readPositiveInt(env, name, fallback) {
26
+ const raw = env[name];
27
+ if (!raw)
28
+ return fallback;
29
+ const parsed = Number(raw);
30
+ if (!Number.isFinite(parsed) || parsed <= 0) {
31
+ console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
32
+ return fallback;
33
+ }
34
+ return Math.floor(parsed);
35
+ }
36
+ /**
37
+ * Parse a boolean env flag. Accepts `true/1/yes/on` (case-insensitive) as true
38
+ * and `false/0/no/off` as false; any other / absent value falls back.
39
+ */
40
+ export function readBool(env, name, fallback) {
41
+ const raw = env[name];
42
+ if (raw == null || raw === '')
43
+ return fallback;
44
+ const v = raw.trim().toLowerCase();
45
+ if (v === 'true' || v === '1' || v === 'yes' || v === 'on')
46
+ return true;
47
+ if (v === 'false' || v === '0' || v === 'no' || v === 'off')
48
+ return false;
49
+ console.warn(`[selva] Invalid ${name}=${raw}, falling back to ${fallback}`);
50
+ return fallback;
51
+ }
52
+ /**
53
+ * Resolve every server-side compute limit from an env map. Pure: the same env
54
+ * always yields the same limits, and nothing is read ambiently. The app calls
55
+ * this once at its composition root with `$env/dynamic/private`.
56
+ */
57
+ export function resolveComputeLimits(env) {
58
+ const maxGhFileSize = readPositiveInt(env, 'MAX_GH_FILE_SIZE_BYTES', 300 * MB);
59
+ return {
60
+ maxSolveDurationMs: readPositiveInt(env, 'MAX_SOLVE_DURATION_MS', 100_000),
61
+ rateLimitWindowMs: readPositiveInt(env, 'COMPUTE_RATE_LIMIT_WINDOW_MS', 100_000),
62
+ rateLimitMaxRequests: readPositiveInt(env, 'COMPUTE_RATE_LIMIT_MAX', 120),
63
+ maxGhFileSize,
64
+ maxImageFileSize: readPositiveInt(env, 'MAX_IMAGE_FILE_SIZE_BYTES', 10 * MB),
65
+ computeRequestMaxBytes: readPositiveInt(env, 'COMPUTE_REQUEST_MAX_BYTES', 300 * MB),
66
+ computeResponseMaxBytes: readPositiveInt(env, 'COMPUTE_RESPONSE_MAX_BYTES', 300 * MB),
67
+ // Tracks the upload cap so a remote URL can't smuggle a larger file.
68
+ remoteDefinitionMaxBytes: maxGhFileSize,
69
+ remoteDefinitionFetchTimeoutMs: readPositiveInt(env, 'REMOTE_DEFINITION_FETCH_TIMEOUT_MS', 30_000),
70
+ definitionCacheTtlMs: readPositiveInt(env, 'DEFINITION_CACHE_TTL_MS', 5 * 60 * 1000),
71
+ computeReuseDefinitionCache: readBool(env, 'COMPUTE_REUSE_DEFINITION_CACHE', true),
72
+ computeServerCachesolve: readBool(env, 'COMPUTE_SERVER_CACHESOLVE', true),
73
+ computeCacheErroredSolves: readBool(env, 'COMPUTE_CACHE_ERRORED_SOLVES', false)
74
+ };
75
+ }
76
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +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;AAmED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAc;IAClD,MAAM,aAAa,GAAG,eAAe,CAAC,GAAG,EAAE,wBAAwB,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;IAC/E,OAAO;QACN,kBAAkB,EAAE,eAAe,CAAC,GAAG,EAAE,uBAAuB,EAAE,OAAO,CAAC;QAC1E,iBAAiB,EAAE,eAAe,CAAC,GAAG,EAAE,8BAA8B,EAAE,OAAO,CAAC;QAChF,oBAAoB,EAAE,eAAe,CAAC,GAAG,EAAE,wBAAwB,EAAE,GAAG,CAAC;QACzE,aAAa;QACb,gBAAgB,EAAE,eAAe,CAAC,GAAG,EAAE,2BAA2B,EAAE,EAAE,GAAG,EAAE,CAAC;QAC5E,sBAAsB,EAAE,eAAe,CAAC,GAAG,EAAE,2BAA2B,EAAE,GAAG,GAAG,EAAE,CAAC;QACnF,uBAAuB,EAAE,eAAe,CAAC,GAAG,EAAE,4BAA4B,EAAE,GAAG,GAAG,EAAE,CAAC;QACrF,qEAAqE;QACrE,wBAAwB,EAAE,aAAa;QACvC,8BAA8B,EAAE,eAAe,CAC9C,GAAG,EACH,oCAAoC,EACpC,MAAM,CACN;QACD,oBAAoB,EAAE,eAAe,CAAC,GAAG,EAAE,yBAAyB,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACpF,2BAA2B,EAAE,QAAQ,CAAC,GAAG,EAAE,gCAAgC,EAAE,IAAI,CAAC;QAClF,uBAAuB,EAAE,QAAQ,CAAC,GAAG,EAAE,2BAA2B,EAAE,IAAI,CAAC;QACzE,yBAAyB,EAAE,QAAQ,CAAC,GAAG,EAAE,8BAA8B,EAAE,KAAK,CAAC;KAC/E,CAAC;AACH,CAAC"}
@@ -0,0 +1,49 @@
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
+ export interface RateLimitResult {
23
+ allowed: boolean;
24
+ /** Seconds until the bucket resets, when not allowed. */
25
+ retryAfter?: number;
26
+ }
27
+ export interface ComputeRateLimiterConfig {
28
+ /** Fixed-window length in ms. */
29
+ windowMs: number;
30
+ /** Max requests admitted per key per window. */
31
+ maxPerWindow: number;
32
+ }
33
+ export interface ComputeRateLimiter {
34
+ /**
35
+ * Check + record one request against `key`. Increments the counter on allow.
36
+ * Returns `{ allowed: false, retryAfter }` once the bucket is full.
37
+ */
38
+ check(key: string): RateLimitResult;
39
+ /** Test seam — wipes in-memory state. Production code never calls this. */
40
+ reset(): void;
41
+ /** The resolved config, exposed for time-based test assertions. */
42
+ readonly config: Readonly<ComputeRateLimiterConfig>;
43
+ }
44
+ /**
45
+ * Build a process-local fixed-window rate limiter. Each limiter owns its own
46
+ * bucket `Map`, so distinct endpoints/keys-spaces don't collide.
47
+ */
48
+ export declare function createComputeRateLimiter(config: ComputeRateLimiterConfig): ComputeRateLimiter;
49
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/compute/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACxC,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAClC;;;OAGG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAAC;IACd,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,wBAAwB,CAAC,CAAC;CACpD;AAOD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,wBAAwB,GAAG,kBAAkB,CA0B7F"}
@@ -0,0 +1,49 @@
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
+ /**
23
+ * Build a process-local fixed-window rate limiter. Each limiter owns its own
24
+ * bucket `Map`, so distinct endpoints/keys-spaces don't collide.
25
+ */
26
+ export function createComputeRateLimiter(config) {
27
+ const { windowMs, maxPerWindow } = config;
28
+ const buckets = new Map();
29
+ return {
30
+ config,
31
+ check(key) {
32
+ const now = Date.now();
33
+ const entry = buckets.get(key);
34
+ if (!entry || now > entry.resetAt) {
35
+ buckets.set(key, { count: 1, resetAt: now + windowMs });
36
+ return { allowed: true };
37
+ }
38
+ if (entry.count >= maxPerWindow) {
39
+ return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
40
+ }
41
+ entry.count += 1;
42
+ return { allowed: true };
43
+ },
44
+ reset() {
45
+ buckets.clear();
46
+ }
47
+ };
48
+ }
49
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/compute/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgCH;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAgC;IACxE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE/C,OAAO;QACN,MAAM;QACN,KAAK,CAAC,GAAW;YAChB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAE/B,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,QAAQ,EAAE,CAAC,CAAC;gBACxD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC1B,CAAC;YAED,IAAI,KAAK,CAAC,KAAK,IAAI,YAAY,EAAE,CAAC;gBACjC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;YAChF,CAAC;YAED,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;YACjB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC1B,CAAC;QACD,KAAK;YACJ,OAAO,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;KACD,CAAC;AACH,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Remote-definition fetch: SSRF-guarded, size-capped, timed, and TTL-cached.
3
+ *
4
+ * A solve can reference its `.gh` by remote URL instead of a stored blob. That
5
+ * URL is attacker-controllable (a share-token caller supplies `definitionUrl`),
6
+ * so every fetch runs through {@link assertSafeRemoteDefinitionUrl} (literal +
7
+ * DNS host validation, blocking private/loopback/link-local targets and
8
+ * literal-encoding bypasses), streams the body with an early size cap, and
9
+ * bounds the wall time with a deadline.
10
+ *
11
+ * The TTL cache is a per-fetcher `Map<url, {data, fetchedAt}>` so repeated
12
+ * solves of the same remote definition skip the network. It's created by
13
+ * {@link createRemoteDefinitionFetcher} rather than being module-global, so a
14
+ * consuming app can hold one fetcher per process (the app binding does) or
15
+ * several isolated ones (tests) without shared state leaking across them.
16
+ */
17
+ /** Injected limits — the app derives these from its env-resolved `ComputeLimits`. */
18
+ export interface RemoteDefinitionConfig {
19
+ /** Hard cap on the fetched body; a lying/absent content-length can't exceed it. */
20
+ maxBytes: number;
21
+ /** Deadline on the fetch (slow-loris protection). */
22
+ fetchTimeoutMs: number;
23
+ /** How long a fetched definition stays warm in the in-process cache. */
24
+ cacheTtlMs: number;
25
+ /**
26
+ * Current epoch millis. Injected (not read via `Date.now()` ambiently) so the
27
+ * TTL is testable and the module stays side-effect-free. The app binding
28
+ * passes `() => Date.now()`.
29
+ */
30
+ now: () => number;
31
+ }
32
+ export interface RemoteDefinitionFetcher {
33
+ /**
34
+ * Fetch (or return cached) `.gh` bytes for a remote URL. Throws on an unsafe
35
+ * host, a non-2xx response, an oversized body, or a timeout — the caller maps
36
+ * the failure to a 400.
37
+ */
38
+ load(url: string): Promise<Uint8Array>;
39
+ }
40
+ /**
41
+ * Read a response body into memory, aborting (and throwing) as soon as the
42
+ * running byte total exceeds `maxBytes`. Falls back to `arrayBuffer()` only when
43
+ * the body isn't a readable stream (older fetch impls). Exported for direct use
44
+ * and reuse; the fetcher below wraps it with SSRF + TTL.
45
+ */
46
+ export declare function readBodyWithCap(response: Response, maxBytes: number, controller: AbortController): Promise<Uint8Array>;
47
+ /**
48
+ * Build a remote-definition fetcher owning its own TTL cache. Each fetcher's
49
+ * cache is independent, so one per process shares warm bytes while test fetchers
50
+ * stay isolated.
51
+ */
52
+ export declare function createRemoteDefinitionFetcher(config: RemoteDefinitionConfig): RemoteDefinitionFetcher;
53
+ //# sourceMappingURL=remote-definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-definition.d.ts","sourceRoot":"","sources":["../../src/compute/remote-definition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,qFAAqF;AACrF,MAAM,WAAW,sBAAsB;IACtC,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,GAAG,EAAE,MAAM,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACvC;;;;OAIG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACvC;AAMD;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,eAAe,GACzB,OAAO,CAAC,UAAU,CAAC,CA4BrB;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC5C,MAAM,EAAE,sBAAsB,GAC5B,uBAAuB,CAqDzB"}