@malloy-publisher/server 0.0.197 → 0.0.198-dev1

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 (54) hide show
  1. package/README.docker.md +47 -0
  2. package/build.ts +26 -1
  3. package/dist/app/api-doc.yaml +54 -20
  4. package/dist/app/assets/{EnvironmentPage-BVkQH_xQ.js → EnvironmentPage-Dpee_Kn6.js} +1 -1
  5. package/dist/app/assets/{HomePage-BgH9UkjK.js → HomePage-DLRWTNoL.js} +1 -1
  6. package/dist/app/assets/{MainPage-DiBxABem.js → MainPage-DsVt5QGM.js} +1 -1
  7. package/dist/app/assets/{ModelPage-oS70fj83.js → ModelPage-AwAugZ37.js} +1 -1
  8. package/dist/app/assets/{PackagePage-F_qLDAdv.js → PackagePage-XQ-EWGTC.js} +1 -1
  9. package/dist/app/assets/{RouteError-WqpffppN.js → RouteError-3Mv8JQw7.js} +1 -1
  10. package/dist/app/assets/{WorkbookPage-_YmC-ebR.js → WorkbookPage-DHYYpcYc.js} +1 -1
  11. package/dist/app/assets/{core-B8L9xCYT.es-BcRLJTnC.js → core-DfcpQGVP.es-DQggNOdX.js} +1 -1
  12. package/dist/app/assets/{index-C3XPaTaS.js → index-BUp81Qdm.js} +1 -1
  13. package/dist/app/assets/{index-rg8Ok8nl.js → index-D1pdwrUW.js} +1 -1
  14. package/dist/app/assets/{index-BMViiwtJ.js → index-Dv5bF4Ii.js} +4 -4
  15. package/dist/app/assets/{index.umd-CCAfKkxY.js → index.umd-CQH4LZU8.js} +1 -1
  16. package/dist/app/index.html +1 -1
  17. package/dist/compile_worker.mjs +628 -0
  18. package/dist/instrumentation.mjs +36 -36
  19. package/dist/server.mjs +1781 -809
  20. package/package.json +1 -1
  21. package/src/compile/compile_pool.spec.ts +227 -0
  22. package/src/compile/compile_pool.ts +729 -0
  23. package/src/compile/compile_worker.ts +683 -0
  24. package/src/compile/protocol.ts +251 -0
  25. package/src/config.spec.ts +81 -0
  26. package/src/config.ts +126 -0
  27. package/src/controller/compile.controller.ts +3 -1
  28. package/src/controller/model.controller.ts +8 -1
  29. package/src/controller/package.controller.ts +70 -29
  30. package/src/controller/query.controller.ts +3 -0
  31. package/src/errors.ts +13 -0
  32. package/src/health.spec.ts +90 -0
  33. package/src/health.ts +86 -71
  34. package/src/mcp/tools/discovery_tools.ts +6 -2
  35. package/src/mcp/tools/execute_query_tool.ts +12 -0
  36. package/src/path_safety.spec.ts +158 -0
  37. package/src/path_safety.ts +140 -0
  38. package/src/server.ts +29 -0
  39. package/src/service/environment.ts +616 -199
  40. package/src/service/environment_admission.spec.ts +180 -0
  41. package/src/service/environment_store.spec.ts +0 -19
  42. package/src/service/environment_store.ts +24 -21
  43. package/src/service/filter_integration.spec.ts +110 -0
  44. package/src/service/givens_integration.spec.ts +192 -0
  45. package/src/service/manifest_service.spec.ts +7 -2
  46. package/src/service/manifest_service.ts +8 -2
  47. package/src/service/materialization_service.ts +14 -3
  48. package/src/service/model.spec.ts +105 -0
  49. package/src/service/model.ts +317 -10
  50. package/src/service/model_worker_path.spec.ts +125 -0
  51. package/src/service/package_memory_governor.spec.ts +173 -0
  52. package/src/service/package_memory_governor.ts +233 -0
  53. package/src/service/package_race.spec.ts +208 -0
  54. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +280 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Wire protocol between the main thread (CompileWorkerPool) and the
3
+ * compile worker threads. Messages flow in both directions over the
4
+ * worker_threads MessagePort:
5
+ *
6
+ * main ──▶ worker: CompileJobRequest (start a compile)
7
+ * worker ──▶ main: CompileJobResult (success)
8
+ * worker ──▶ main: CompileJobError (failure)
9
+ *
10
+ * worker ──▶ main: SchemaForTablesRequest (proxy schema fetch)
11
+ * worker ──▶ main: SchemaForSqlRequest (proxy SQL block schema)
12
+ * main ──▶ worker: SchemaForTablesResponse / SchemaForSqlResponse
13
+ *
14
+ * main ──▶ worker: ShutdownRequest (graceful drain & exit)
15
+ *
16
+ * The protocol intentionally uses plain structured-clonable POJOs so
17
+ * `parentPort.postMessage` and `worker.postMessage` can transfer them
18
+ * via V8's structured clone — much cheaper than JSON.stringify for
19
+ * the multi-MB `modelDef` payloads that come back from compile.
20
+ *
21
+ * All requests are correlated by an opaque `requestId` string so the
22
+ * receiver can match responses without relying on FIFO ordering.
23
+ */
24
+
25
+ import type {
26
+ Annotation,
27
+ SQLSourceDef,
28
+ TableSourceDef,
29
+ } from "@malloydata/malloy";
30
+
31
+ // ──────────────────────────────────────────────────────────────────────
32
+ // Direction: main ──▶ worker (compile job)
33
+ // ──────────────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Connection metadata the worker needs to construct a stub
37
+ * `InfoConnection`. Resolved lazily — the worker asks the main thread
38
+ * for these on the first `lookupConnection(name)` call (see
39
+ * {@link ConnectionMetadataRequest}). We don't ship the full list
40
+ * upfront because the caller layer doesn't always know it; Malloy
41
+ * sees connection names only as `connection.table('...')`
42
+ * references inside the model.
43
+ */
44
+ export interface ConnectionMetadata {
45
+ name: string;
46
+ dialectName: string;
47
+ digest: string;
48
+ }
49
+
50
+ export interface CompileJobRequest {
51
+ type: "compile";
52
+ requestId: string;
53
+ /** Absolute path to the package directory on disk. */
54
+ packagePath: string;
55
+ /** Path of the model file relative to `packagePath`. */
56
+ modelPath: string;
57
+ /** Name of the default connection (e.g. "duckdb"), or null. */
58
+ defaultConnectionName: string | null;
59
+ /** Optional row-build manifest passed through to the Runtime. */
60
+ buildManifest?: unknown;
61
+ }
62
+
63
+ // ──────────────────────────────────────────────────────────────────────
64
+ // Direction: worker ──▶ main (compile result)
65
+ // ──────────────────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Wire shape of a successful compile. Mirrors the fields the
69
+ * server's `Model` constructor needs to fully describe a `.malloy`
70
+ * file without holding a `ModelMaterializer` reference.
71
+ *
72
+ * The materializer itself is intentionally NOT shipped back — it
73
+ * binds to a Runtime that holds live native connection handles and
74
+ * cannot cross a worker_threads boundary. The main thread builds
75
+ * its own materializer lazily on the first query (see
76
+ * `Model.ensureMaterializer`).
77
+ */
78
+ export interface CompileJobResult {
79
+ type: "compile-result";
80
+ requestId: string;
81
+ /** Whatever `await modelMaterializer.getModel()`._modelDef returned. */
82
+ modelDef: unknown;
83
+ /** Source-info entries (from imports + local sources). */
84
+ sourceInfos: unknown[];
85
+ /** Pre-extracted API source descriptors. */
86
+ sources: unknown[];
87
+ /** Pre-extracted API query descriptors. */
88
+ queries: unknown[];
89
+ /** Parsed `#(filter)` map, keyed by source name. */
90
+ filterMap: Array<[string, unknown[]]>;
91
+ /** Givens declared on the model, already in API shape so the main
92
+ * thread can stash them on the `Model` without further conversion. */
93
+ givens?: unknown[];
94
+ /** Accumulated dataStyles (from HackyDataStylesAccumulator). */
95
+ dataStyles: unknown;
96
+ /** Wall-clock ms inside the worker for the actual compile. */
97
+ compileDurationMs: number;
98
+ }
99
+
100
+ export interface CompileJobError {
101
+ type: "compile-error";
102
+ requestId: string;
103
+ /** Serialized error — the main thread reconstructs an Error. */
104
+ error: SerializedError;
105
+ }
106
+
107
+ /**
108
+ * Error wire-shape. We cannot transfer Error instances directly
109
+ * across postMessage cleanly (Bun/Node behaviour diverges on stack
110
+ * propagation), so we ship a structured payload and reconstitute on
111
+ * the main thread.
112
+ */
113
+ export interface SerializedError {
114
+ name: string;
115
+ message: string;
116
+ stack?: string;
117
+ /** Set when the error originated as a Malloy `MalloyError`. */
118
+ malloyProblems?: unknown[];
119
+ /** Set when the error originated as `ModelCompilationError`. */
120
+ isCompilationError?: boolean;
121
+ }
122
+
123
+ // ──────────────────────────────────────────────────────────────────────
124
+ // Direction: worker ──▶ main (proxy connection metadata)
125
+ // ──────────────────────────────────────────────────────────────────────
126
+
127
+ export interface ConnectionMetadataRequest {
128
+ type: "connection-metadata";
129
+ requestId: string;
130
+ jobId: string;
131
+ connectionName: string;
132
+ }
133
+
134
+ export interface ConnectionMetadataResponse {
135
+ type: "connection-metadata-response";
136
+ requestId: string;
137
+ ok: true;
138
+ metadata: ConnectionMetadata;
139
+ }
140
+
141
+ // ──────────────────────────────────────────────────────────────────────
142
+ // Direction: worker ──▶ main (proxy schema fetches)
143
+ // ──────────────────────────────────────────────────────────────────────
144
+
145
+ export interface SchemaForTablesRequest {
146
+ type: "schema-for-tables";
147
+ requestId: string;
148
+ /** Job this RPC belongs to (so main routes to the right config). */
149
+ jobId: string;
150
+ connectionName: string;
151
+ tables: Record<string, string>;
152
+ options: {
153
+ refreshTimestamp?: number;
154
+ modelAnnotation?: Annotation;
155
+ };
156
+ }
157
+
158
+ export interface SchemaForTablesResponse {
159
+ type: "schema-for-tables-response";
160
+ requestId: string;
161
+ ok: true;
162
+ schemas: Record<string, TableSourceDef>;
163
+ errors: Record<string, string>;
164
+ }
165
+
166
+ export interface SchemaForSqlRequest {
167
+ type: "schema-for-sql";
168
+ requestId: string;
169
+ jobId: string;
170
+ connectionName: string;
171
+ sentence: unknown;
172
+ options: {
173
+ refreshTimestamp?: number;
174
+ modelAnnotation?: Annotation;
175
+ };
176
+ }
177
+
178
+ export interface SchemaForSqlResponse {
179
+ type: "schema-for-sql-response";
180
+ requestId: string;
181
+ ok: true;
182
+ structDef?: SQLSourceDef;
183
+ error?: string;
184
+ }
185
+
186
+ export interface RpcErrorResponse {
187
+ type: "rpc-error";
188
+ requestId: string;
189
+ ok: false;
190
+ error: SerializedError;
191
+ }
192
+
193
+ // ──────────────────────────────────────────────────────────────────────
194
+ // Direction: worker ──▶ main (file read for imports)
195
+ // ──────────────────────────────────────────────────────────────────────
196
+
197
+ /**
198
+ * Workers read most files directly via fs (they run in the same
199
+ * filesystem namespace). This RPC exists for the rare case where the
200
+ * package URL reader has host-specific behaviour (e.g. virtual files,
201
+ * remote URLs) — we delegate back to the main thread's URL reader so
202
+ * compile semantics stay identical to the in-process path.
203
+ */
204
+ export interface ReadUrlRequest {
205
+ type: "read-url";
206
+ requestId: string;
207
+ jobId: string;
208
+ url: string;
209
+ }
210
+
211
+ export interface ReadUrlResponse {
212
+ type: "read-url-response";
213
+ requestId: string;
214
+ ok: true;
215
+ contents: string;
216
+ invalidationKey?: string | number | null;
217
+ }
218
+
219
+ // ──────────────────────────────────────────────────────────────────────
220
+ // Lifecycle
221
+ // ──────────────────────────────────────────────────────────────────────
222
+
223
+ export interface ShutdownRequest {
224
+ type: "shutdown";
225
+ }
226
+
227
+ export interface ReadyMessage {
228
+ type: "ready";
229
+ }
230
+
231
+ // ──────────────────────────────────────────────────────────────────────
232
+ // Union types for routing
233
+ // ──────────────────────────────────────────────────────────────────────
234
+
235
+ export type MainToWorkerMessage =
236
+ | CompileJobRequest
237
+ | ConnectionMetadataResponse
238
+ | SchemaForTablesResponse
239
+ | SchemaForSqlResponse
240
+ | ReadUrlResponse
241
+ | RpcErrorResponse
242
+ | ShutdownRequest;
243
+
244
+ export type WorkerToMainMessage =
245
+ | CompileJobResult
246
+ | CompileJobError
247
+ | ConnectionMetadataRequest
248
+ | SchemaForTablesRequest
249
+ | SchemaForSqlRequest
250
+ | ReadUrlRequest
251
+ | ReadyMessage;
@@ -1081,3 +1081,84 @@ describe("Config path resolution (--config and bundled default)", () => {
1081
1081
  expect(result.environments).toEqual([]);
1082
1082
  });
1083
1083
  });
1084
+
1085
+ describe("getMemoryGovernorConfig", () => {
1086
+ const GOVERNOR_ENV_VARS = [
1087
+ "PUBLISHER_MAX_MEMORY_BYTES",
1088
+ "PUBLISHER_MEMORY_HIGH_WATER_FRACTION",
1089
+ "PUBLISHER_MEMORY_LOW_WATER_FRACTION",
1090
+ "PUBLISHER_MEMORY_CHECK_INTERVAL_MS",
1091
+ "PUBLISHER_MEMORY_BACKPRESSURE",
1092
+ ];
1093
+
1094
+ beforeEach(() => {
1095
+ for (const v of GOVERNOR_ENV_VARS) delete process.env[v];
1096
+ });
1097
+ afterEach(() => {
1098
+ for (const v of GOVERNOR_ENV_VARS) delete process.env[v];
1099
+ });
1100
+
1101
+ it("returns null when PUBLISHER_MAX_MEMORY_BYTES is unset", async () => {
1102
+ const { getMemoryGovernorConfig } = await import("./config");
1103
+ expect(getMemoryGovernorConfig()).toBeNull();
1104
+ });
1105
+
1106
+ it("parses defaults when only PUBLISHER_MAX_MEMORY_BYTES is set", async () => {
1107
+ process.env.PUBLISHER_MAX_MEMORY_BYTES = String(2 * 1024 * 1024 * 1024);
1108
+ const { getMemoryGovernorConfig } = await import("./config");
1109
+ const cfg = getMemoryGovernorConfig();
1110
+ expect(cfg).not.toBeNull();
1111
+ expect(cfg!.maxMemoryBytes).toBe(2 * 1024 * 1024 * 1024);
1112
+ expect(cfg!.backpressureEnabled).toBe(true);
1113
+ expect(cfg!.highWaterFraction).toBeGreaterThan(cfg!.lowWaterFraction);
1114
+ });
1115
+
1116
+ it("honours fraction and interval overrides", async () => {
1117
+ process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1118
+ process.env.PUBLISHER_MEMORY_HIGH_WATER_FRACTION = "0.85";
1119
+ process.env.PUBLISHER_MEMORY_LOW_WATER_FRACTION = "0.7";
1120
+ process.env.PUBLISHER_MEMORY_CHECK_INTERVAL_MS = "10000";
1121
+ process.env.PUBLISHER_MEMORY_BACKPRESSURE = "false";
1122
+ const { getMemoryGovernorConfig } = await import("./config");
1123
+ const cfg = getMemoryGovernorConfig();
1124
+ expect(cfg).not.toBeNull();
1125
+ expect(cfg!.highWaterFraction).toBe(0.85);
1126
+ expect(cfg!.lowWaterFraction).toBe(0.7);
1127
+ expect(cfg!.checkIntervalMs).toBe(10000);
1128
+ expect(cfg!.backpressureEnabled).toBe(false);
1129
+ });
1130
+
1131
+ it("treats PUBLISHER_MAX_MEMORY_BYTES=0 as disabled (returns null)", async () => {
1132
+ process.env.PUBLISHER_MAX_MEMORY_BYTES = "0";
1133
+ const { getMemoryGovernorConfig } = await import("./config");
1134
+ expect(getMemoryGovernorConfig()).toBeNull();
1135
+ });
1136
+
1137
+ it("rejects a negative PUBLISHER_MAX_MEMORY_BYTES", async () => {
1138
+ process.env.PUBLISHER_MAX_MEMORY_BYTES = "-1";
1139
+ const { getMemoryGovernorConfig } = await import("./config");
1140
+ expect(() => getMemoryGovernorConfig()).toThrow();
1141
+ });
1142
+
1143
+ it("rejects low >= high", async () => {
1144
+ process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1145
+ process.env.PUBLISHER_MEMORY_HIGH_WATER_FRACTION = "0.7";
1146
+ process.env.PUBLISHER_MEMORY_LOW_WATER_FRACTION = "0.8";
1147
+ const { getMemoryGovernorConfig } = await import("./config");
1148
+ expect(() => getMemoryGovernorConfig()).toThrow();
1149
+ });
1150
+
1151
+ it("rejects an out-of-range fraction", async () => {
1152
+ process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1153
+ process.env.PUBLISHER_MEMORY_HIGH_WATER_FRACTION = "1.5";
1154
+ const { getMemoryGovernorConfig } = await import("./config");
1155
+ expect(() => getMemoryGovernorConfig()).toThrow();
1156
+ });
1157
+
1158
+ it("rejects a check interval below the safety floor", async () => {
1159
+ process.env.PUBLISHER_MAX_MEMORY_BYTES = "1000000000";
1160
+ process.env.PUBLISHER_MEMORY_CHECK_INTERVAL_MS = "10";
1161
+ const { getMemoryGovernorConfig } = await import("./config");
1162
+ expect(() => getMemoryGovernorConfig()).toThrow();
1163
+ });
1164
+ });
package/src/config.ts CHANGED
@@ -99,6 +99,132 @@ export type ProcessedPublisherConfig = {
99
99
  environments: ProcessedEnvironment[];
100
100
  };
101
101
 
102
+ /**
103
+ * Tunables for {@link PackageMemoryGovernor}. All values are sourced
104
+ * from environment variables at startup; see {@link getMemoryGovernorConfig}
105
+ * for parsing and defaults.
106
+ *
107
+ * The governor is admission control only: it polls process RSS on
108
+ * `checkIntervalMs` and toggles a single `isBackpressured` flag using
109
+ * a low/high-water hysteresis band. It does NOT evict, unload, or
110
+ * interrupt already-loaded packages — recovery is left to the kernel
111
+ * reclaiming pages as in-flight traffic completes.
112
+ */
113
+ export interface MemoryGovernorConfig {
114
+ /** Hard ceiling for process RSS in bytes (the OOM-relevant figure). */
115
+ maxMemoryBytes: number;
116
+ /** Fraction of `maxMemoryBytes` at which the governor activates back-pressure (new package loads start returning HTTP 503). Must be in (0, 1) and strictly greater than `lowWaterFraction`. */
117
+ highWaterFraction: number;
118
+ /** Fraction of `maxMemoryBytes` at which the governor clears back-pressure (new package loads admitted again). Must be in (0, 1) and strictly less than `highWaterFraction`; the gap is the hysteresis band that prevents flap. */
119
+ lowWaterFraction: number;
120
+ /** Polling cadence for the RSS sampler, in milliseconds. */
121
+ checkIntervalMs: number;
122
+ /** When true, RSS crossings flip the back-pressure flag. When false, the governor still samples and emits metrics but never rejects requests — useful for a monitoring-only rollout before enabling the 503 behaviour. */
123
+ backpressureEnabled: boolean;
124
+ }
125
+
126
+ const DEFAULT_HIGH_WATER_FRACTION = 0.8;
127
+ const DEFAULT_LOW_WATER_FRACTION = 0.7;
128
+ const DEFAULT_CHECK_INTERVAL_MS = 5_000;
129
+ const MIN_CHECK_INTERVAL_MS = 100;
130
+
131
+ function parseIntEnv(name: string): number | undefined {
132
+ const raw = process.env[name];
133
+ if (raw === undefined || raw.trim() === "") return undefined;
134
+ const value = Number.parseInt(raw, 10);
135
+ if (!Number.isFinite(value) || String(value) !== raw.trim()) {
136
+ throw new Error(
137
+ `Invalid value for ${name}: expected a base-10 integer, got "${raw}"`,
138
+ );
139
+ }
140
+ return value;
141
+ }
142
+
143
+ function parseFloatEnv(name: string): number | undefined {
144
+ const raw = process.env[name];
145
+ if (raw === undefined || raw.trim() === "") return undefined;
146
+ const value = Number.parseFloat(raw);
147
+ if (!Number.isFinite(value)) {
148
+ throw new Error(
149
+ `Invalid value for ${name}: expected a finite number, got "${raw}"`,
150
+ );
151
+ }
152
+ return value;
153
+ }
154
+
155
+ function parseBoolEnv(name: string): boolean | undefined {
156
+ const raw = process.env[name];
157
+ if (raw === undefined || raw.trim() === "") return undefined;
158
+ const normalised = raw.trim().toLowerCase();
159
+ if (["1", "true", "yes", "on"].includes(normalised)) return true;
160
+ if (["0", "false", "no", "off"].includes(normalised)) return false;
161
+ throw new Error(
162
+ `Invalid value for ${name}: expected a boolean (true/false), got "${raw}"`,
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Parse memory-governor settings from environment variables and return
168
+ * either a fully-validated config or `null` when the feature is
169
+ * disabled. The feature is disabled iff `PUBLISHER_MAX_MEMORY_BYTES`
170
+ * is unset or set to `0`.
171
+ *
172
+ * Throws at startup on malformed input so a typo in a k8s manifest
173
+ * surfaces as a loud failure rather than silently disabling the cap.
174
+ */
175
+ export const getMemoryGovernorConfig = (): MemoryGovernorConfig | null => {
176
+ const maxMemoryBytes = parseIntEnv("PUBLISHER_MAX_MEMORY_BYTES");
177
+ if (maxMemoryBytes === undefined || maxMemoryBytes === 0) {
178
+ return null;
179
+ }
180
+ if (maxMemoryBytes < 0) {
181
+ throw new Error(
182
+ `PUBLISHER_MAX_MEMORY_BYTES must be a positive integer (got ${maxMemoryBytes})`,
183
+ );
184
+ }
185
+
186
+ const highWaterFraction =
187
+ parseFloatEnv("PUBLISHER_MEMORY_HIGH_WATER_FRACTION") ??
188
+ DEFAULT_HIGH_WATER_FRACTION;
189
+ const lowWaterFraction =
190
+ parseFloatEnv("PUBLISHER_MEMORY_LOW_WATER_FRACTION") ??
191
+ DEFAULT_LOW_WATER_FRACTION;
192
+ const checkIntervalMs =
193
+ parseIntEnv("PUBLISHER_MEMORY_CHECK_INTERVAL_MS") ??
194
+ DEFAULT_CHECK_INTERVAL_MS;
195
+ const backpressureEnabled =
196
+ parseBoolEnv("PUBLISHER_MEMORY_BACKPRESSURE") ?? true;
197
+
198
+ if (highWaterFraction <= 0 || highWaterFraction >= 1) {
199
+ throw new Error(
200
+ `PUBLISHER_MEMORY_HIGH_WATER_FRACTION must be in (0, 1) (got ${highWaterFraction})`,
201
+ );
202
+ }
203
+ if (lowWaterFraction <= 0 || lowWaterFraction >= 1) {
204
+ throw new Error(
205
+ `PUBLISHER_MEMORY_LOW_WATER_FRACTION must be in (0, 1) (got ${lowWaterFraction})`,
206
+ );
207
+ }
208
+ if (lowWaterFraction >= highWaterFraction) {
209
+ throw new Error(
210
+ `PUBLISHER_MEMORY_LOW_WATER_FRACTION (${lowWaterFraction}) must be strictly less than PUBLISHER_MEMORY_HIGH_WATER_FRACTION (${highWaterFraction})`,
211
+ );
212
+ }
213
+ if (checkIntervalMs < MIN_CHECK_INTERVAL_MS) {
214
+ throw new Error(
215
+ `PUBLISHER_MEMORY_CHECK_INTERVAL_MS must be >= ${MIN_CHECK_INTERVAL_MS} (got ${checkIntervalMs})`,
216
+ );
217
+ }
218
+
219
+ return {
220
+ maxMemoryBytes,
221
+ highWaterFraction,
222
+ lowWaterFraction,
223
+ checkIntervalMs,
224
+ backpressureEnabled,
225
+ };
226
+ };
227
+
102
228
  function substituteEnvVars(value: string): string {
103
229
  const envVarPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
104
230
 
@@ -1,4 +1,4 @@
1
- import type { LogMessage } from "@malloydata/malloy";
1
+ import type { GivenValue, LogMessage } from "@malloydata/malloy";
2
2
  import { EnvironmentStore } from "../service/environment_store";
3
3
 
4
4
  export class CompileController {
@@ -14,6 +14,7 @@ export class CompileController {
14
14
  modelName: string,
15
15
  source: string,
16
16
  includeSql: boolean = false,
17
+ givens?: Record<string, GivenValue>,
17
18
  ): Promise<{ status: string; problems: LogMessage[]; sql?: string }> {
18
19
  const environment = await this.environmentStore.getEnvironment(
19
20
  environmentName,
@@ -24,6 +25,7 @@ export class CompileController {
24
25
  modelName,
25
26
  source,
26
27
  includeSql,
28
+ givens,
27
29
  );
28
30
 
29
31
  // Determine overall status based on presence of errors
@@ -2,6 +2,7 @@ import { components } from "../api";
2
2
  import { ModelNotFoundError } from "../errors";
3
3
  import { EnvironmentStore } from "../service/environment_store";
4
4
  import type { FilterParams } from "../service/filter";
5
+ import type { GivenValue } from "@malloydata/malloy";
5
6
 
6
7
  type ApiNotebook = components["schemas"]["Notebook"];
7
8
  type ApiModel = components["schemas"]["Model"];
@@ -97,6 +98,7 @@ export class ModelController {
97
98
  cellIndex: number,
98
99
  filterParams?: FilterParams,
99
100
  bypassFilters?: boolean,
101
+ givens?: Record<string, GivenValue>,
100
102
  ): Promise<{
101
103
  type: "code" | "markdown";
102
104
  text: string;
@@ -117,6 +119,11 @@ export class ModelController {
117
119
  throw new ModelNotFoundError(`${notebookPath} is a model`);
118
120
  }
119
121
 
120
- return model.executeNotebookCell(cellIndex, filterParams, bypassFilters);
122
+ return model.executeNotebookCell(
123
+ cellIndex,
124
+ filterParams,
125
+ bypassFilters,
126
+ givens,
127
+ );
121
128
  }
122
129
  }