@malloy-publisher/server 0.0.234 → 0.0.236

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 (318) hide show
  1. package/dist/app/api-doc.yaml +47 -17
  2. package/dist/app/assets/{EnvironmentPage-DTZQ4Gxc.js → EnvironmentPage-BsAnavYN.js} +1 -1
  3. package/dist/app/assets/{HomePage-C5mlDPXK.js → HomePage-CADE138j.js} +1 -1
  4. package/dist/app/assets/{LightMode-DGNmhG0u.js → LightMode-Cfh7KzN8.js} +1 -1
  5. package/dist/app/assets/{MainPage-CVL_wmP4.js → MainPage-CO3pRlnV.js} +1 -1
  6. package/dist/app/assets/{MaterializationsPage-DmzMBCpy.js → MaterializationsPage-p9YjkRXZ.js} +1 -1
  7. package/dist/app/assets/{ModelPage-Dbvf4QbB.js → ModelPage-C1OSTv-x.js} +1 -1
  8. package/dist/app/assets/{PackagePage-DxdHc2Qs.js → PackagePage-e4kN75YR.js} +1 -1
  9. package/dist/app/assets/{RouteError-OJdT4tCd.js → RouteError-CzbfOkng.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-Bk7s0KXY.js → ThemeEditorPage-CciagFTq.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-j_vCWdN3.js → WorkbookPage-DNWmkCXa.js} +1 -1
  12. package/dist/app/assets/{core-Rj_4rRnA.es-DoIfLxDJ.js → core-Rj_4rRnA.es-BZyvITuO.js} +1 -1
  13. package/dist/app/assets/{index-B_jKMR35.js → index-CH2AcDzc.js} +4 -4
  14. package/dist/app/assets/{index-hw-xn0X7.js → index-DQa463gC.js} +2 -2
  15. package/dist/app/assets/{index-DWIe_hK0.js → index-DQpV7MyA.js} +1 -1
  16. package/dist/app/assets/{index-BabP-V-S.js → index-DzaYbhnD.js} +1 -1
  17. package/dist/app/assets/{index-D-rDyK11.js → index-VBbcc8s6.js} +1 -1
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +8 -1
  20. package/dist/server.mjs +471 -54
  21. package/package.json +16 -13
  22. package/.eslintrc.json +0 -44
  23. package/.prettierignore +0 -1
  24. package/.prettierrc +0 -4
  25. package/build.ts +0 -97
  26. package/eslint.config.mjs +0 -76
  27. package/k6-tests/README.md +0 -144
  28. package/k6-tests/bun.lock +0 -797
  29. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  30. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  31. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  32. package/k6-tests/load-test/load-test-crud.ts +0 -142
  33. package/k6-tests/load-test/load-test.ts +0 -784
  34. package/k6-tests/package.json +0 -27
  35. package/k6-tests/scripts/run-with-otel.sh +0 -50
  36. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  37. package/k6-tests/tsconfig.json +0 -29
  38. package/k6-tests/utils/client_factory.ts +0 -128
  39. package/k6-tests/utils/common.ts +0 -489
  40. package/k6-tests/utils/logger.ts +0 -62
  41. package/k6-tests/utils/types.d.ts +0 -35
  42. package/malloy_mcp.dxt +0 -0
  43. package/publisher.config.example.bigquery.json +0 -33
  44. package/publisher.config.example.duckdb.json +0 -23
  45. package/publisher.config.json +0 -23
  46. package/scripts/bake-duckdb-extensions.js +0 -110
  47. package/src/config.spec.ts +0 -1535
  48. package/src/config.theme.spec.ts +0 -178
  49. package/src/config.ts +0 -1142
  50. package/src/constants.ts +0 -105
  51. package/src/controller/compile.controller.ts +0 -40
  52. package/src/controller/connection.controller.spec.ts +0 -849
  53. package/src/controller/connection.controller.ts +0 -901
  54. package/src/controller/database.controller.ts +0 -24
  55. package/src/controller/materialization.controller.spec.ts +0 -293
  56. package/src/controller/materialization.controller.ts +0 -319
  57. package/src/controller/model.controller.ts +0 -164
  58. package/src/controller/package.controller.spec.ts +0 -273
  59. package/src/controller/package.controller.ts +0 -306
  60. package/src/controller/query.controller.ts +0 -152
  61. package/src/controller/theme.controller.ts +0 -83
  62. package/src/controller/watch-mode.controller.ts +0 -243
  63. package/src/data_styles.ts +0 -71
  64. package/src/default-publisher.config.json +0 -23
  65. package/src/ducklake_version.spec.ts +0 -163
  66. package/src/ducklake_version.ts +0 -153
  67. package/src/errors.spec.ts +0 -119
  68. package/src/errors.ts +0 -237
  69. package/src/filter_deprecation.spec.ts +0 -64
  70. package/src/filter_deprecation.ts +0 -42
  71. package/src/health.spec.ts +0 -123
  72. package/src/health.ts +0 -233
  73. package/src/heap_check.spec.ts +0 -144
  74. package/src/heap_check.ts +0 -144
  75. package/src/instrumentation.ts +0 -214
  76. package/src/json_utils.spec.ts +0 -51
  77. package/src/json_utils.ts +0 -33
  78. package/src/logger.spec.ts +0 -210
  79. package/src/logger.ts +0 -251
  80. package/src/malloy_pin_prereqs.spec.ts +0 -25
  81. package/src/materialization_metrics.spec.ts +0 -169
  82. package/src/materialization_metrics.ts +0 -346
  83. package/src/mcp/error_messages.spec.ts +0 -35
  84. package/src/mcp/error_messages.ts +0 -183
  85. package/src/mcp/handler_utils.spec.ts +0 -108
  86. package/src/mcp/handler_utils.ts +0 -251
  87. package/src/mcp/mcp_constants.ts +0 -6
  88. package/src/mcp/query_envelope.spec.ts +0 -229
  89. package/src/mcp/query_envelope.ts +0 -240
  90. package/src/mcp/server.protocol.spec.ts +0 -250
  91. package/src/mcp/server.ts +0 -87
  92. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  93. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  94. package/src/mcp/skills/skills_bundle.json +0 -1
  95. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  96. package/src/mcp/tool_response.spec.ts +0 -108
  97. package/src/mcp/tool_response.ts +0 -138
  98. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  99. package/src/mcp/tools/compile_tool.ts +0 -208
  100. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  101. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  102. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  103. package/src/mcp/tools/docs_search_tool.ts +0 -128
  104. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  105. package/src/mcp/tools/embedding_index.ts +0 -808
  106. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  107. package/src/mcp/tools/execute_query_tool.ts +0 -316
  108. package/src/mcp/tools/get_context_eval.ts +0 -275
  109. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  110. package/src/mcp/tools/get_context_tool.ts +0 -585
  111. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  112. package/src/mcp/tools/reload_package_tool.ts +0 -132
  113. package/src/mcp_config.spec.ts +0 -919
  114. package/src/mcp_config.ts +0 -425
  115. package/src/oom_guards.integration.spec.ts +0 -269
  116. package/src/package_load/package_load_pool.spec.ts +0 -252
  117. package/src/package_load/package_load_pool.ts +0 -931
  118. package/src/package_load/package_load_worker.ts +0 -1034
  119. package/src/package_load/protocol.ts +0 -364
  120. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  121. package/src/package_load/rpc_wait_accountant.ts +0 -76
  122. package/src/package_load_metrics.spec.ts +0 -114
  123. package/src/package_load_metrics.ts +0 -127
  124. package/src/path_safety.spec.ts +0 -158
  125. package/src/path_safety.ts +0 -146
  126. package/src/pg_helpers.spec.ts +0 -223
  127. package/src/pg_helpers.ts +0 -52
  128. package/src/query_cap_metrics.spec.ts +0 -89
  129. package/src/query_cap_metrics.ts +0 -116
  130. package/src/query_concurrency.spec.ts +0 -247
  131. package/src/query_concurrency.ts +0 -237
  132. package/src/query_metadata_metrics.ts +0 -49
  133. package/src/query_param_utils.ts +0 -29
  134. package/src/query_timeout.spec.ts +0 -224
  135. package/src/query_timeout.ts +0 -179
  136. package/src/runtime/publisher.js +0 -328
  137. package/src/server-old.ts +0 -988
  138. package/src/server.ts +0 -2059
  139. package/src/service/annotations.spec.ts +0 -118
  140. package/src/service/annotations.ts +0 -91
  141. package/src/service/authorize.spec.ts +0 -154
  142. package/src/service/authorize.ts +0 -498
  143. package/src/service/authorize_integration.spec.ts +0 -2041
  144. package/src/service/build_plan.spec.ts +0 -685
  145. package/src/service/build_plan.ts +0 -797
  146. package/src/service/compile_authorize.spec.ts +0 -85
  147. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  148. package/src/service/connection.spec.ts +0 -2360
  149. package/src/service/connection.ts +0 -2187
  150. package/src/service/connection_config.spec.ts +0 -734
  151. package/src/service/connection_config.ts +0 -899
  152. package/src/service/connection_federation.spec.ts +0 -184
  153. package/src/service/connection_fingerprint.spec.ts +0 -102
  154. package/src/service/connection_service.spec.ts +0 -711
  155. package/src/service/connection_service.ts +0 -291
  156. package/src/service/cron_evaluator.spec.ts +0 -79
  157. package/src/service/cron_evaluator.ts +0 -105
  158. package/src/service/db_utils.spec.ts +0 -1085
  159. package/src/service/db_utils.ts +0 -1502
  160. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  161. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  162. package/src/service/embedding_provider.spec.ts +0 -329
  163. package/src/service/embedding_provider.ts +0 -236
  164. package/src/service/environment.ts +0 -2191
  165. package/src/service/environment_admission.spec.ts +0 -344
  166. package/src/service/environment_compile.spec.ts +0 -197
  167. package/src/service/environment_store.spec.ts +0 -2274
  168. package/src/service/environment_store.ts +0 -2434
  169. package/src/service/environment_store_anchoring.spec.ts +0 -107
  170. package/src/service/environment_store_clone.spec.ts +0 -350
  171. package/src/service/explore_visibility.spec.ts +0 -434
  172. package/src/service/exports_probe.spec.ts +0 -107
  173. package/src/service/extension_fetch_policy.spec.ts +0 -256
  174. package/src/service/filter.spec.ts +0 -458
  175. package/src/service/filter.ts +0 -341
  176. package/src/service/filter_bypass.spec.ts +0 -418
  177. package/src/service/filter_integration.spec.ts +0 -1004
  178. package/src/service/freshness.spec.ts +0 -183
  179. package/src/service/freshness.ts +0 -112
  180. package/src/service/gcs_s3_utils.ts +0 -427
  181. package/src/service/given.ts +0 -105
  182. package/src/service/givens_integration.spec.ts +0 -219
  183. package/src/service/manifest_loader.spec.ts +0 -189
  184. package/src/service/manifest_loader.ts +0 -156
  185. package/src/service/materialization_build_session.spec.ts +0 -435
  186. package/src/service/materialization_build_session.ts +0 -681
  187. package/src/service/materialization_config_validation.spec.ts +0 -99
  188. package/src/service/materialization_config_validation.ts +0 -120
  189. package/src/service/materialization_eligibility.spec.ts +0 -158
  190. package/src/service/materialization_eligibility.ts +0 -305
  191. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  192. package/src/service/materialization_scheduler.spec.ts +0 -289
  193. package/src/service/materialization_scheduler.ts +0 -304
  194. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  195. package/src/service/materialization_serve_transform.ts +0 -779
  196. package/src/service/materialization_service.spec.ts +0 -2373
  197. package/src/service/materialization_service.ts +0 -2342
  198. package/src/service/materialization_test_fixtures.ts +0 -249
  199. package/src/service/model.spec.ts +0 -1157
  200. package/src/service/model.ts +0 -3370
  201. package/src/service/model_limits.spec.ts +0 -209
  202. package/src/service/model_limits.ts +0 -131
  203. package/src/service/model_storage_serve.spec.ts +0 -193
  204. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  205. package/src/service/package.spec.ts +0 -612
  206. package/src/service/package.ts +0 -1799
  207. package/src/service/package_manifest.spec.ts +0 -230
  208. package/src/service/package_manifest.ts +0 -241
  209. package/src/service/package_memory_governor.spec.ts +0 -173
  210. package/src/service/package_memory_governor.ts +0 -233
  211. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  212. package/src/service/package_race.spec.ts +0 -208
  213. package/src/service/package_reload_safety.spec.ts +0 -193
  214. package/src/service/package_rollback.spec.ts +0 -190
  215. package/src/service/package_worker_path.spec.ts +0 -645
  216. package/src/service/path_injection.spec.ts +0 -39
  217. package/src/service/persist_annotation_validation.spec.ts +0 -89
  218. package/src/service/persist_annotation_validation.ts +0 -59
  219. package/src/service/persistence_policy.spec.ts +0 -374
  220. package/src/service/proxy.spec.ts +0 -414
  221. package/src/service/proxy.ts +0 -248
  222. package/src/service/query_boundary.spec.ts +0 -470
  223. package/src/service/query_metadata.spec.ts +0 -408
  224. package/src/service/query_metadata.ts +0 -492
  225. package/src/service/query_metadata_identity.spec.ts +0 -149
  226. package/src/service/query_text.spec.ts +0 -79
  227. package/src/service/query_text.ts +0 -43
  228. package/src/service/quoting.spec.ts +0 -135
  229. package/src/service/quoting.ts +0 -78
  230. package/src/service/resolve_environment.ts +0 -15
  231. package/src/service/restricted_mode.spec.ts +0 -299
  232. package/src/service/source_extraction.ts +0 -225
  233. package/src/service/theme_store.ts +0 -199
  234. package/src/storage/DatabaseInterface.ts +0 -198
  235. package/src/storage/StorageManager.ts +0 -113
  236. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  237. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  238. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  239. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  240. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  241. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  242. package/src/storage/duckdb/PackageRepository.ts +0 -135
  243. package/src/storage/duckdb/schema.ts +0 -229
  244. package/src/stream_helpers.spec.ts +0 -280
  245. package/src/stream_helpers.ts +0 -162
  246. package/src/telemetry.ts +0 -20
  247. package/src/test_helpers/metrics_harness.ts +0 -166
  248. package/src/theme_key_parity.spec.ts +0 -57
  249. package/src/utils.ts +0 -29
  250. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  251. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  252. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  253. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  254. package/tests/fixtures/html-pages-test/data.csv +0 -3
  255. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  256. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  257. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  258. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  259. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  260. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  261. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  262. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  263. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  264. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  265. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  266. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  267. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  268. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  269. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  270. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  271. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  272. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  273. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  274. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  275. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  276. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  277. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  278. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  279. package/tests/fixtures/persist-test/publisher.json +0 -5
  280. package/tests/fixtures/publisher.config.json +0 -15
  281. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  282. package/tests/fixtures/query-givens/model.malloy +0 -34
  283. package/tests/fixtures/query-givens/publisher.json +0 -5
  284. package/tests/fixtures/xlsx/database.xlsx +0 -0
  285. package/tests/harness/mcp_test_setup.ts +0 -332
  286. package/tests/harness/rest_e2e.ts +0 -68
  287. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  288. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  289. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  290. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  291. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  292. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  293. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  294. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  295. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  296. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  297. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  298. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  299. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  300. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  301. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  302. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  303. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  304. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  305. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  306. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  307. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  308. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  309. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  310. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  311. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  312. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  313. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  314. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  315. package/tests/unit/duckdb/repositories.test.ts +0 -208
  316. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  317. package/tests/unit/service/theme_store.spec.ts +0 -139
  318. package/tsconfig.json +0 -32
@@ -1,931 +0,0 @@
1
- /**
2
- * Main-thread half of the package-load worker pool.
3
- *
4
- * Why this exists: Malloy compile is pure JavaScript that runs on the
5
- * V8 main event loop. A large package can hold the loop for many
6
- * seconds — long enough to time out the Kubernetes `/health/liveness`
7
- * probe (timeout=5s, see worker/main.tf) and trigger a pod restart in
8
- * the middle of an otherwise routine package load. This pool moves the
9
- * CPU-bound bulk of `Package.create` (manifest read, every
10
- * `.malloy`/`.malloynb` compile) into `worker_threads` workers, leaving
11
- * the main loop free to serve `/health/liveness` and other API traffic.
12
- *
13
- * Public surface
14
- * --------------
15
- * getPackageLoadPool() — lazy singleton; respects PACKAGE_LOAD_WORKERS
16
- * pool.loadPackage({...}) — submit one package for off-thread load
17
- * pool.shutdown() — graceful drain (called from health.ts)
18
- *
19
- * The pool is REQUIRED. There is no in-process compile fallback —
20
- * `PACKAGE_LOAD_WORKERS=0` throws at boot. Running Malloy compile on
21
- * the main event loop is what tripped the K8s liveness probe in the
22
- * first place; we don't want a knob that re-enables that. Pool-
23
- * infrastructure failures (worker crash, job timeout, pool shutting
24
- * down) reject `loadPackage()`; `Package.loadViaWorker` then rewraps
25
- * them as `ServiceUnavailableError` so the HTTP layer responds 503.
26
- *
27
- * Concurrency model
28
- * -----------------
29
- * - Workers are spawned lazily on first use, up to N (configurable
30
- * via PACKAGE_LOAD_WORKERS).
31
- * - Each worker emits {type:'ready'} once its module has initialized;
32
- * the pool waits for that before dispatching to a newly-spawned
33
- * worker.
34
- * - **Per-worker active job cap = 1.** Compile is CPU-bound and
35
- * doesn't multiplex meaningfully inside a single event loop, so
36
- * loading two large packages on the same worker just doubles each
37
- * of their latencies rather than overlapping their work. Excess
38
- * jobs queue at the pool layer and pick a worker the moment one
39
- * becomes idle.
40
- * - Schema/URL/connection-metadata RPCs from a worker back to the
41
- * main thread (over MessagePort) are not counted against the
42
- * active-job cap — they're sub-RPCs of the worker's currently
43
- * active load-package job.
44
- *
45
- * Failure modes
46
- * -------------
47
- * - **Worker spawn fails.** `pw.ready` rejects on `worker.error`,
48
- * `worker.exit`, or after `WORKER_SPAWN_TIMEOUT_MS`. Pending jobs
49
- * waiting on that worker get a clean rejection and are re-queued
50
- * to whichever worker becomes available next.
51
- * - **Worker dies mid-job.** `worker.exit` fails every in-flight job
52
- * on that worker; the pool prunes the dead worker, and the next
53
- * `loadPackage()` lazily respawns up to `maxWorkers`.
54
- * - **Job timeout.** `PACKAGE_LOAD_JOB_TIMEOUT_MS` (default 120s) caps a
55
- * single package load. On timeout we **terminate the worker** and
56
- * reject the caller — we do not silently fall back, and we do not
57
- * leave a zombie compile burning CPU on the worker.
58
- *
59
- * Schema-fetch RPC routing
60
- * ------------------------
61
- * When a worker proxies `fetchSchemaForTables` (or other connection
62
- * metadata calls) back to the main thread, the message includes a
63
- * `jobId`. The pool keeps a `jobId → JobContext` map so it knows
64
- * which live `MalloyConfig` to dispatch the request against. The
65
- * mapping is installed when the job is submitted and removed when
66
- * it resolves.
67
- */
68
- import type {
69
- FetchSchemaOptions,
70
- InfoConnection,
71
- LookupConnection,
72
- MalloyConfig,
73
- ModelDef,
74
- SQLSourceDef,
75
- TableSourceDef,
76
- } from "@malloydata/malloy";
77
- import * as Malloy from "@malloydata/malloy-interfaces";
78
- import { Worker } from "node:worker_threads";
79
- import { dirname, join } from "path";
80
- import { fileURLToPath, pathToFileURL } from "url";
81
-
82
- import { ModelCompilationError } from "../errors";
83
- import { logger } from "../logger";
84
- import type {
85
- PackageMaterializationConfig,
86
- PackageScope,
87
- } from "../service/package_manifest";
88
- import type {
89
- ConnectionMetadataRequest,
90
- ConnectionMetadataResponse,
91
- LoadPackageError,
92
- LoadPackageRequest,
93
- LoadPackageResult,
94
- MainToWorkerMessage,
95
- ReadUrlRequest,
96
- ReadUrlResponse,
97
- RpcErrorResponse,
98
- SchemaForSqlRequest,
99
- SchemaForSqlResponse,
100
- SchemaForTablesRequest,
101
- SchemaForTablesResponse,
102
- SerializedError,
103
- SerializedModel,
104
- WorkerToMainMessage,
105
- } from "./protocol";
106
-
107
- // ──────────────────────────────────────────────────────────────────────
108
- // Configuration
109
- // ──────────────────────────────────────────────────────────────────────
110
-
111
- /**
112
- * Returns the configured worker pool size. The pool is REQUIRED — no
113
- * in-process fallback exists. Defaults to 1 worker when the env var
114
- * is unset; throws when set to 0 or to a non-positive / non-numeric
115
- * value so we fail fast at boot rather than silently degrading.
116
- *
117
- * Operators set `PACKAGE_LOAD_WORKERS=N` (N ≥ 1) to tune the pool
118
- * size for their workload. Setting it to 0 to "disable" the pool is
119
- * unsupported by design — running Malloy compile on the main event
120
- * loop is what tripped the K8s liveness probe in the first place
121
- * and we don't want a config knob that re-enables that footgun.
122
- */
123
- export function getPackageLoadWorkerCount(): number {
124
- const raw = process.env.PACKAGE_LOAD_WORKERS;
125
- if (raw === undefined) return 1;
126
- const parsed = Number.parseInt(raw, 10);
127
- if (!Number.isFinite(parsed) || parsed < 1) {
128
- throw new Error(
129
- `PACKAGE_LOAD_WORKERS must be a positive integer (got ${JSON.stringify(raw)}). ` +
130
- `Refusing to start without a package-load worker pool.`,
131
- );
132
- }
133
- return parsed;
134
- }
135
-
136
- /**
137
- * Wall-clock cap for a single load-package job. If a worker hangs we
138
- * don't want to leak a Promise forever. Twice the K8s liveness
139
- * threshold (5s × 15 fails = 75s) gives comfortable margin against
140
- * false timeouts during cold starts while still failing fast on real
141
- * hangs. Overridable via `PACKAGE_LOAD_JOB_TIMEOUT_MS`.
142
- */
143
- const PACKAGE_LOAD_JOB_TIMEOUT_MS = (() => {
144
- const raw = process.env.PACKAGE_LOAD_JOB_TIMEOUT_MS;
145
- if (raw === undefined) return 120_000;
146
- const parsed = Number.parseInt(raw, 10);
147
- if (!Number.isFinite(parsed) || parsed <= 0) return 120_000;
148
- return parsed;
149
- })();
150
-
151
- /**
152
- * How long a freshly-spawned worker has to send its `ready` handshake.
153
- * If the worker can't load its bundle (missing `package_load_worker.mjs`,
154
- * native module init failure, etc.), this fires and the pool fails
155
- * the pending jobs on that worker without waiting forever. Distinct
156
- * from the per-job timeout so a slow-to-start worker doesn't poison
157
- * the job timeout budget.
158
- */
159
- const WORKER_SPAWN_TIMEOUT_MS = 30_000;
160
-
161
- // ──────────────────────────────────────────────────────────────────────
162
- // Internal types
163
- // ──────────────────────────────────────────────────────────────────────
164
-
165
- interface JobContext {
166
- jobId: string;
167
- pw: PoolWorker;
168
- connections: LookupConnection<InfoConnection>;
169
- urlReader: PackageLoadUrlReader;
170
- resolve: (result: LoadPackageResult) => void;
171
- reject: (err: Error) => void;
172
- timeout: ReturnType<typeof setTimeout>;
173
- }
174
-
175
- interface QueuedJob {
176
- request: LoadPackageJob;
177
- resolve: (result: LoadPackageOutcome) => void;
178
- reject: (err: Error) => void;
179
- }
180
-
181
- interface PoolWorker {
182
- id: number;
183
- worker: Worker;
184
- ready: Promise<void>;
185
- inFlight: Set<string>; // job ids — capped at 1 by per-worker active job rule
186
- exited: boolean;
187
- }
188
-
189
- /**
190
- * Minimal URL-reader shape the pool dispatches to. Intentionally
191
- * narrower than Malloy's `URLReader` (which can also return
192
- * `{contents, invalidationKey}`) — the pool only needs the string
193
- * payload to forward back to the worker.
194
- */
195
- export interface PackageLoadUrlReader {
196
- readURL(
197
- url: URL,
198
- ): Promise<string | { contents: string; invalidationKey?: unknown }>;
199
- }
200
-
201
- export interface LoadPackageJob {
202
- packagePath: string;
203
- packageName: string;
204
- /**
205
- * The live MalloyConfig. We don't ship it across the worker
206
- * boundary; we hold it on the main side and answer the worker's
207
- * proxy RPCs (for non-duckdb connections) against
208
- * `config.connections`. The worker has its own DuckDB connection
209
- * for embedded-database probes.
210
- */
211
- malloyConfig: MalloyConfig;
212
- /** Default connection name (passed verbatim to the worker). */
213
- defaultConnectionName: string | null;
214
- /** Custom URLReader for non-file:// imports; falls back to fs in the worker. */
215
- urlReader?: PackageLoadUrlReader;
216
- /** Optional buildManifest passed through to Malloy Runtime. */
217
- buildManifest?: unknown;
218
- }
219
-
220
- /**
221
- * Adapter result the pool surfaces to callers. Same wire shape as
222
- * `LoadPackageResult` but with the heavy `modelDef` / `sourceInfos`
223
- * fields restored to their Malloy types so the consumer (currently
224
- * `Package.createViaWorker`) doesn't need to repeat the cast at
225
- * every call site. The protocol module uses `unknown` for these so
226
- * it stays decoupled from the full Malloy type surface; this is
227
- * where we re-tighten.
228
- *
229
- * `filterMap` stays as the wire array; `Model.fromSerialized` will
230
- * rebuild a `Map` on demand. Keeping it array-shaped here means we
231
- * don't allocate a Map for models the caller never looks at.
232
- */
233
- export interface LoadPackageOutcome {
234
- packageMetadata: {
235
- name?: string;
236
- description?: string;
237
- explores?: string[];
238
- queryableSources?: "declared" | "all";
239
- manifestLocation?: string | null;
240
- materialization?: PackageMaterializationConfig | null;
241
- scope?: PackageScope;
242
- /** See {@link LoadPackageResult.packageMetadata.manifestWarnings}. */
243
- manifestWarnings?: string[];
244
- };
245
- models: Array<
246
- Omit<SerializedModel, "modelDef" | "sourceInfos"> & {
247
- modelDef?: ModelDef;
248
- sourceInfos?: Malloy.SourceInfo[];
249
- }
250
- >;
251
- loadDurationMs: number;
252
- /** Per-load phase timing breakdown (see {@link LoadPackageResult.timings}). */
253
- timings: LoadPackageResult["timings"];
254
- }
255
-
256
- // ──────────────────────────────────────────────────────────────────────
257
- // Path to the bundled worker script
258
- // ──────────────────────────────────────────────────────────────────────
259
-
260
- /**
261
- * Locate the worker entrypoint. In production builds it's bundled to
262
- * `dist/package_load_worker.mjs` next to `server.mjs`. In dev
263
- * (bun --watch) the source `.ts` next to this file loads directly —
264
- * Bun supports `new Worker(<ts-url>)`. The .ts fallback also keeps
265
- * `bun test` working without a build step.
266
- *
267
- * Uses `pathToFileURL` rather than a `file://${path}` template so the
268
- * URL stays valid on Windows, where absolute paths look like
269
- * `D:\foo\bar` (template form would yield `file://D:\foo\bar`, which
270
- * parses as host `D:` rather than a path).
271
- */
272
- function resolveWorkerScript(): URL {
273
- const thisFile = fileURLToPath(import.meta.url);
274
- const thisDir = dirname(thisFile);
275
- const distCandidate = join(thisDir, "package_load_worker.mjs");
276
- if (thisFile.endsWith(".mjs") || thisFile.endsWith(".js")) {
277
- return pathToFileURL(distCandidate);
278
- }
279
- const tsCandidate = join(thisDir, "package_load_worker.ts");
280
- return pathToFileURL(tsCandidate);
281
- }
282
-
283
- // ──────────────────────────────────────────────────────────────────────
284
- // The pool
285
- // ──────────────────────────────────────────────────────────────────────
286
-
287
- export class PackageLoadPool {
288
- private readonly workers: PoolWorker[] = [];
289
- private readonly queue: QueuedJob[] = [];
290
- private readonly jobs = new Map<string, JobContext>();
291
- private readonly maxWorkers: number;
292
- private nextWorkerId = 0;
293
- private nextJobId = 0;
294
- private shuttingDown = false;
295
- private readonly workerScript: URL;
296
-
297
- constructor(maxWorkers: number, workerScript?: URL) {
298
- if (!Number.isFinite(maxWorkers) || maxWorkers < 1) {
299
- throw new Error(
300
- `PackageLoadPool requires maxWorkers >= 1 (got ${maxWorkers}). ` +
301
- `The in-process compile fallback was removed in favour of a hard ` +
302
- `dependency on the worker pool; see getPackageLoadWorkerCount().`,
303
- );
304
- }
305
- this.maxWorkers = maxWorkers;
306
- this.workerScript = workerScript ?? resolveWorkerScript();
307
- }
308
-
309
- get size(): number {
310
- return this.workers.filter((w) => !w.exited).length;
311
- }
312
-
313
- /**
314
- * Submit a package for off-thread load. Returns when the worker
315
- * has produced a {@link LoadPackageOutcome}. Per-package compile
316
- * failures are surfaced **inside** the outcome (as
317
- * `model.compilationError`), not as a rejected Promise — the
318
- * Promise only rejects for whole-package errors (manifest missing,
319
- * worker crash, RPC timeout, etc.) so the caller can distinguish
320
- * "this package is broken; tell the user" from "the worker pool
321
- * itself failed and we have a bigger problem".
322
- */
323
- async loadPackage(request: LoadPackageJob): Promise<LoadPackageOutcome> {
324
- if (this.shuttingDown) {
325
- throw new Error("PackageLoadPool is shutting down");
326
- }
327
- return new Promise<LoadPackageOutcome>((resolve, reject) => {
328
- this.queue.push({ request, resolve, reject });
329
- this.tryDispatch();
330
- });
331
- }
332
-
333
- /**
334
- * Drain every in-flight job and terminate the workers. Safe to
335
- * call multiple times. Awaits each worker's `exit` event.
336
- */
337
- async shutdown(): Promise<void> {
338
- if (this.shuttingDown) return;
339
- this.shuttingDown = true;
340
- // Fail any queued jobs that never got dispatched.
341
- const queued = this.queue.splice(0, this.queue.length);
342
- for (const qj of queued) {
343
- qj.reject(new Error("PackageLoadPool: shutdown before dispatch"));
344
- }
345
- const exits = this.workers.map(
346
- (pw) =>
347
- new Promise<void>((resolve) => {
348
- if (pw.exited) {
349
- resolve();
350
- return;
351
- }
352
- pw.worker.once("exit", () => resolve());
353
- const msg: { type: "shutdown" } = { type: "shutdown" };
354
- pw.worker.postMessage(msg);
355
- // Hard ceiling — if a worker won't exit, terminate it.
356
- setTimeout(() => {
357
- if (!pw.exited) {
358
- void pw.worker.terminate().finally(() => resolve());
359
- }
360
- }, 10_000);
361
- }),
362
- );
363
- await Promise.all(exits);
364
- }
365
-
366
- // ────────────────────────────────────────────────────────────────
367
- // Internals: dispatch / queue
368
- // ────────────────────────────────────────────────────────────────
369
-
370
- /**
371
- * Greedy dispatcher: keep draining the queue while there's both
372
- * a queued job and an available worker. Called from `loadPackage`
373
- * (on submit) and from job completion / worker exit (to give the
374
- * next job a worker that just freed up).
375
- */
376
- private tryDispatch(): void {
377
- if (this.shuttingDown) return;
378
- while (this.queue.length > 0) {
379
- const worker = this.findIdleOrSpawnable();
380
- if (!worker) return; // saturated; wait for a job to finish
381
- const qj = this.queue.shift();
382
- if (!qj) return;
383
- void this.dispatchToWorker(worker, qj);
384
- }
385
- }
386
-
387
- /**
388
- * Return an idle worker, or spawn a new one if we're below
389
- * `maxWorkers`. With per-worker active=1, "idle" means
390
- * `inFlight.size === 0`. Returns null when the pool is saturated.
391
- */
392
- private findIdleOrSpawnable(): PoolWorker | null {
393
- const alive = this.workers.filter((w) => !w.exited);
394
- const idle = alive.find((w) => w.inFlight.size === 0);
395
- if (idle) return idle;
396
- if (alive.length < this.maxWorkers) {
397
- const pw = this.spawnWorker();
398
- this.workers.push(pw);
399
- return pw;
400
- }
401
- return null;
402
- }
403
-
404
- private async dispatchToWorker(
405
- pw: PoolWorker,
406
- qj: QueuedJob,
407
- ): Promise<void> {
408
- try {
409
- await pw.ready;
410
- } catch (err) {
411
- // Spawn failed; this worker is dead. Reject this job and
412
- // try the next queued one — a different worker may already
413
- // be alive, or `findIdleOrSpawnable` will lazily spawn one.
414
- qj.reject(err as Error);
415
- this.tryDispatch();
416
- return;
417
- }
418
-
419
- this.nextJobId += 1;
420
- const jobId = `job-${this.nextJobId}`;
421
-
422
- const timeout = setTimeout(() => {
423
- this.handleJobTimeout(pw, jobId, qj.request.packagePath);
424
- }, PACKAGE_LOAD_JOB_TIMEOUT_MS);
425
-
426
- this.jobs.set(jobId, {
427
- jobId,
428
- pw,
429
- connections: qj.request.malloyConfig.connections,
430
- urlReader: qj.request.urlReader ?? defaultUrlReader,
431
- resolve: (result) => {
432
- clearTimeout(timeout);
433
- qj.resolve(adaptResult(result));
434
- },
435
- reject: (err) => {
436
- clearTimeout(timeout);
437
- qj.reject(err);
438
- },
439
- timeout,
440
- });
441
-
442
- // Register inFlight BEFORE postMessage so the next concurrent
443
- // dispatcher pass sees this worker as busy (fixes Sha-Bang #1's
444
- // 4/1 skew where simultaneous callers all tiebreak to alive[0]).
445
- pw.inFlight.add(jobId);
446
-
447
- const message: LoadPackageRequest = {
448
- type: "load-package",
449
- requestId: jobId,
450
- packagePath: qj.request.packagePath,
451
- packageName: qj.request.packageName,
452
- defaultConnectionName: qj.request.defaultConnectionName,
453
- buildManifest: qj.request.buildManifest,
454
- };
455
- pw.worker.postMessage(message);
456
- }
457
-
458
- /**
459
- * On job timeout we DON'T silently fall back — that lets the
460
- * worker keep burning CPU on the doomed compile while the caller
461
- * also re-runs it on the main thread (the exact 2× CPU situation
462
- * the pool exists to avoid). Instead: terminate the worker,
463
- * reject the caller, and let the next call respawn lazily.
464
- */
465
- private handleJobTimeout(
466
- pw: PoolWorker,
467
- jobId: string,
468
- packagePath: string,
469
- ): void {
470
- logger.error(
471
- `PackageLoadPool: job ${jobId} (${packagePath}) timed out after ${PACKAGE_LOAD_JOB_TIMEOUT_MS}ms; terminating worker #${pw.id}`,
472
- );
473
- this.failJob(
474
- jobId,
475
- new Error(
476
- `Package-load worker timed out after ${PACKAGE_LOAD_JOB_TIMEOUT_MS}ms (package=${packagePath})`,
477
- ),
478
- );
479
- void pw.worker.terminate();
480
- // The exit handler will mark the worker exited, fail any
481
- // residual jobs on it, and trigger tryDispatch.
482
- }
483
-
484
- private spawnWorker(): PoolWorker {
485
- this.nextWorkerId += 1;
486
- const id = this.nextWorkerId;
487
- logger.info(
488
- `PackageLoadPool: spawning worker #${id} (script=${this.workerScript.toString()})`,
489
- );
490
- const worker = new Worker(this.workerScript, {
491
- name: `malloy-package-load-worker-${id}`,
492
- });
493
-
494
- let readyResolve!: () => void;
495
- let readyReject!: (err: Error) => void;
496
- let readySettled = false;
497
- const ready = new Promise<void>((resolve, reject) => {
498
- readyResolve = () => {
499
- if (readySettled) return;
500
- readySettled = true;
501
- resolve();
502
- };
503
- readyReject = (err: Error) => {
504
- if (readySettled) return;
505
- readySettled = true;
506
- reject(err);
507
- };
508
- });
509
- // Silence unhandled-rejection on `ready` itself — every caller
510
- // that consumes it (via dispatchToWorker) handles rejection
511
- // explicitly, but Node still tracks the original promise.
512
- ready.catch(() => {});
513
-
514
- const spawnTimer = setTimeout(() => {
515
- readyReject(
516
- new Error(
517
- `Package-load worker #${id} failed to become ready within ${WORKER_SPAWN_TIMEOUT_MS}ms`,
518
- ),
519
- );
520
- void worker.terminate();
521
- }, WORKER_SPAWN_TIMEOUT_MS);
522
-
523
- const pw: PoolWorker = {
524
- id,
525
- worker,
526
- ready,
527
- inFlight: new Set(),
528
- exited: false,
529
- };
530
-
531
- worker.on("message", (msg: WorkerToMainMessage) => {
532
- this.handleWorkerMessage(pw, msg, readyResolve, spawnTimer);
533
- });
534
-
535
- worker.on("error", (err) => {
536
- logger.error(`PackageLoadPool: worker #${id} errored`, {
537
- error: err,
538
- });
539
- clearTimeout(spawnTimer);
540
- readyReject(err);
541
- });
542
-
543
- worker.on("exit", (code) => {
544
- pw.exited = true;
545
- clearTimeout(spawnTimer);
546
- readyReject(
547
- new Error(
548
- `Package-load worker #${id} exited before becoming ready (code=${code})`,
549
- ),
550
- );
551
- logger.warn(
552
- `PackageLoadPool: worker #${id} exited (code=${code}, inFlight=${pw.inFlight.size})`,
553
- );
554
- // Fail any jobs that were running on this worker so callers
555
- // don't strand waiting for a result that will never come.
556
- for (const jobId of Array.from(pw.inFlight)) {
557
- this.failJob(
558
- jobId,
559
- new Error(
560
- `Package-load worker #${id} exited unexpectedly (code=${code})`,
561
- ),
562
- );
563
- }
564
- pw.inFlight.clear();
565
- // Prune the dead worker so size accounting is honest and
566
- // the next dispatch will see a free slot to respawn into.
567
- const idx = this.workers.indexOf(pw);
568
- if (idx >= 0) this.workers.splice(idx, 1);
569
- // Give queued jobs a chance to dispatch elsewhere.
570
- this.tryDispatch();
571
- });
572
-
573
- return pw;
574
- }
575
-
576
- // ────────────────────────────────────────────────────────────────
577
- // Internals: message dispatch
578
- // ────────────────────────────────────────────────────────────────
579
-
580
- private handleWorkerMessage(
581
- pw: PoolWorker,
582
- msg: WorkerToMainMessage,
583
- markReady: () => void,
584
- spawnTimer: ReturnType<typeof setTimeout>,
585
- ): void {
586
- switch (msg.type) {
587
- case "ready":
588
- clearTimeout(spawnTimer);
589
- markReady();
590
- return;
591
- case "load-package-result":
592
- this.completeJob(pw, msg);
593
- return;
594
- case "load-package-error":
595
- this.errorJob(pw, msg);
596
- return;
597
- case "connection-metadata":
598
- void this.handleConnectionMetadata(pw, msg);
599
- return;
600
- case "schema-for-tables":
601
- void this.handleSchemaForTables(pw, msg);
602
- return;
603
- case "schema-for-sql":
604
- void this.handleSchemaForSql(pw, msg);
605
- return;
606
- case "read-url":
607
- void this.handleReadUrl(pw, msg);
608
- return;
609
- default: {
610
- const exhaustive: never = msg;
611
- void exhaustive;
612
- return;
613
- }
614
- }
615
- }
616
-
617
- private completeJob(pw: PoolWorker, msg: LoadPackageResult): void {
618
- const ctx = this.jobs.get(msg.requestId);
619
- if (!ctx) return;
620
- this.jobs.delete(msg.requestId);
621
- pw.inFlight.delete(msg.requestId);
622
- ctx.resolve(msg);
623
- // Worker is idle now — give it the next queued job (if any).
624
- this.tryDispatch();
625
- }
626
-
627
- private errorJob(pw: PoolWorker, msg: LoadPackageError): void {
628
- const ctx = this.jobs.get(msg.requestId);
629
- if (!ctx) return;
630
- this.jobs.delete(msg.requestId);
631
- pw.inFlight.delete(msg.requestId);
632
- ctx.reject(deserializeError(msg.error));
633
- this.tryDispatch();
634
- }
635
-
636
- /** Fail a job out-of-band (timeout, worker exit). */
637
- private failJob(jobId: string, error: Error): void {
638
- const ctx = this.jobs.get(jobId);
639
- if (!ctx) return;
640
- this.jobs.delete(jobId);
641
- ctx.pw.inFlight.delete(jobId);
642
- ctx.reject(error);
643
- }
644
-
645
- private async handleConnectionMetadata(
646
- pw: PoolWorker,
647
- msg: ConnectionMetadataRequest,
648
- ): Promise<void> {
649
- const ctx = this.jobs.get(msg.jobId);
650
- const reply = (
651
- response: ConnectionMetadataResponse | RpcErrorResponse,
652
- ): void => {
653
- pw.worker.postMessage(response as MainToWorkerMessage);
654
- };
655
- if (!ctx) {
656
- reply({
657
- type: "rpc-error",
658
- requestId: msg.requestId,
659
- ok: false,
660
- error: { name: "Error", message: `Unknown jobId ${msg.jobId}` },
661
- });
662
- return;
663
- }
664
- try {
665
- const conn = await ctx.connections.lookupConnection(
666
- msg.connectionName,
667
- );
668
- reply({
669
- type: "connection-metadata-response",
670
- requestId: msg.requestId,
671
- ok: true,
672
- metadata: {
673
- name: msg.connectionName,
674
- dialectName: conn.dialectName,
675
- digest:
676
- typeof conn.getDigest === "function"
677
- ? conn.getDigest()
678
- : msg.connectionName,
679
- },
680
- });
681
- } catch (error) {
682
- reply({
683
- type: "rpc-error",
684
- requestId: msg.requestId,
685
- ok: false,
686
- error: serializeError(error),
687
- });
688
- }
689
- }
690
-
691
- private async handleSchemaForTables(
692
- pw: PoolWorker,
693
- msg: SchemaForTablesRequest,
694
- ): Promise<void> {
695
- const ctx = this.jobs.get(msg.jobId);
696
- const reply = (
697
- response: SchemaForTablesResponse | RpcErrorResponse,
698
- ): void => {
699
- pw.worker.postMessage(response as MainToWorkerMessage);
700
- };
701
- if (!ctx) {
702
- reply({
703
- type: "rpc-error",
704
- requestId: msg.requestId,
705
- ok: false,
706
- error: { name: "Error", message: `Unknown jobId ${msg.jobId}` },
707
- });
708
- return;
709
- }
710
- try {
711
- const conn = await ctx.connections.lookupConnection(
712
- msg.connectionName,
713
- );
714
- const result = await conn.fetchSchemaForTables(
715
- msg.tables,
716
- buildFetchOptions(msg.options),
717
- );
718
- reply({
719
- type: "schema-for-tables-response",
720
- requestId: msg.requestId,
721
- ok: true,
722
- schemas: result.schemas as Record<string, TableSourceDef>,
723
- errors: result.errors,
724
- });
725
- } catch (error) {
726
- reply({
727
- type: "rpc-error",
728
- requestId: msg.requestId,
729
- ok: false,
730
- error: serializeError(error),
731
- });
732
- }
733
- }
734
-
735
- private async handleSchemaForSql(
736
- pw: PoolWorker,
737
- msg: SchemaForSqlRequest,
738
- ): Promise<void> {
739
- const ctx = this.jobs.get(msg.jobId);
740
- const reply = (
741
- response: SchemaForSqlResponse | RpcErrorResponse,
742
- ): void => {
743
- pw.worker.postMessage(response as MainToWorkerMessage);
744
- };
745
- if (!ctx) {
746
- reply({
747
- type: "rpc-error",
748
- requestId: msg.requestId,
749
- ok: false,
750
- error: { name: "Error", message: `Unknown jobId ${msg.jobId}` },
751
- });
752
- return;
753
- }
754
- try {
755
- const conn = await ctx.connections.lookupConnection(
756
- msg.connectionName,
757
- );
758
- const result = await conn.fetchSchemaForSQLStruct(
759
- msg.sentence as Parameters<
760
- InfoConnection["fetchSchemaForSQLStruct"]
761
- >[0],
762
- buildFetchOptions(msg.options),
763
- );
764
- if (result.error !== undefined) {
765
- reply({
766
- type: "schema-for-sql-response",
767
- requestId: msg.requestId,
768
- ok: true,
769
- error: result.error,
770
- });
771
- } else {
772
- reply({
773
- type: "schema-for-sql-response",
774
- requestId: msg.requestId,
775
- ok: true,
776
- structDef: result.structDef as SQLSourceDef,
777
- });
778
- }
779
- } catch (error) {
780
- reply({
781
- type: "rpc-error",
782
- requestId: msg.requestId,
783
- ok: false,
784
- error: serializeError(error),
785
- });
786
- }
787
- }
788
-
789
- private async handleReadUrl(
790
- pw: PoolWorker,
791
- msg: ReadUrlRequest,
792
- ): Promise<void> {
793
- const ctx = this.jobs.get(msg.jobId);
794
- const reply = (response: ReadUrlResponse | RpcErrorResponse): void => {
795
- pw.worker.postMessage(response as MainToWorkerMessage);
796
- };
797
- if (!ctx) {
798
- reply({
799
- type: "rpc-error",
800
- requestId: msg.requestId,
801
- ok: false,
802
- error: { name: "Error", message: `Unknown jobId ${msg.jobId}` },
803
- });
804
- return;
805
- }
806
- try {
807
- const raw = await ctx.urlReader.readURL(new URL(msg.url));
808
- const contents = typeof raw === "string" ? raw : raw.contents;
809
- reply({
810
- type: "read-url-response",
811
- requestId: msg.requestId,
812
- ok: true,
813
- contents,
814
- });
815
- } catch (error) {
816
- reply({
817
- type: "rpc-error",
818
- requestId: msg.requestId,
819
- ok: false,
820
- error: serializeError(error),
821
- });
822
- }
823
- }
824
- }
825
-
826
- function buildFetchOptions(options: {
827
- refreshTimestamp?: number;
828
- }): FetchSchemaOptions {
829
- const out: FetchSchemaOptions = {};
830
- if (options.refreshTimestamp !== undefined) {
831
- out.refreshTimestamp = options.refreshTimestamp;
832
- }
833
- return out;
834
- }
835
-
836
- /**
837
- * Hydrate the wire-typed serialized model fields back into their
838
- * full Malloy types. Cheap (one pass, no copy); `filterMap` is
839
- * deliberately left as the wire array — `Model.fromSerialized`
840
- * rebuilds the `Map` lazily for the models it actually uses.
841
- */
842
- function adaptResult(result: LoadPackageResult): LoadPackageOutcome {
843
- return {
844
- packageMetadata: result.packageMetadata,
845
- models: result.models.map((m) => ({
846
- ...m,
847
- modelDef: m.modelDef as ModelDef | undefined,
848
- sourceInfos: m.sourceInfos as Malloy.SourceInfo[] | undefined,
849
- })),
850
- loadDurationMs: result.loadDurationMs,
851
- timings: result.timings,
852
- };
853
- }
854
-
855
- function serializeError(error: unknown): SerializedError {
856
- if (error instanceof Error) {
857
- return {
858
- name: error.name,
859
- message: error.message,
860
- stack: error.stack,
861
- };
862
- }
863
- return { name: "Error", message: String(error) };
864
- }
865
-
866
- /**
867
- * Reconstitute an Error from a serialized payload. When the original
868
- * was a Malloy compile error we re-wrap as `ModelCompilationError` so
869
- * downstream `instanceof` checks (which decide e.g. HTTP 424 vs 500)
870
- * keep firing across the worker boundary.
871
- */
872
- export function deserializeError(serialized: SerializedError): Error {
873
- const err = new Error(serialized.message);
874
- err.name = serialized.name;
875
- if (serialized.stack) err.stack = serialized.stack;
876
- if (serialized.malloyProblems) {
877
- (err as unknown as { problems: unknown }).problems =
878
- serialized.malloyProblems;
879
- }
880
- if (serialized.isCompilationError) {
881
- // ModelCompilationError's ctor expects a MalloyError-shaped
882
- // input but only reads `.message` at runtime. Cast through to
883
- // satisfy the nominal type without losing data.
884
- const wrapped = new ModelCompilationError(
885
- err as unknown as ConstructorParameters<
886
- typeof ModelCompilationError
887
- >[0],
888
- );
889
- if (serialized.stack) wrapped.stack = serialized.stack;
890
- return wrapped;
891
- }
892
- return err;
893
- }
894
-
895
- const defaultUrlReader = {
896
- readURL: async (url: URL): Promise<string> => {
897
- const { promises: fs } = await import("fs");
898
- const { fileURLToPath } = await import("url");
899
- const filePath =
900
- url.protocol === "file:" ? fileURLToPath(url) : url.toString();
901
- return fs.readFile(filePath, "utf8");
902
- },
903
- };
904
-
905
- // ──────────────────────────────────────────────────────────────────────
906
- // Singleton accessor
907
- // ──────────────────────────────────────────────────────────────────────
908
-
909
- let singleton: PackageLoadPool | null = null;
910
-
911
- export function getPackageLoadPool(): PackageLoadPool {
912
- if (singleton === null) {
913
- const n = getPackageLoadWorkerCount();
914
- singleton = new PackageLoadPool(n);
915
- logger.info(
916
- `Malloy package-load worker pool enabled (size=${n}). ` +
917
- `Override with PACKAGE_LOAD_WORKERS=N (N >= 1).`,
918
- );
919
- }
920
- return singleton;
921
- }
922
-
923
- /** Test-only: replace the singleton (and shut down the previous one). */
924
- export async function __setPackageLoadPoolForTests(
925
- pool: PackageLoadPool | null,
926
- ): Promise<void> {
927
- if (singleton && singleton !== pool) {
928
- await singleton.shutdown();
929
- }
930
- singleton = pool;
931
- }