@malloy-publisher/server 0.0.235 → 0.0.237

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 (319) hide show
  1. package/dist/app/api-doc.yaml +83 -27
  2. package/dist/app/assets/{EnvironmentPage-BsAnavYN.js → EnvironmentPage-DaP6CpgK.js} +1 -1
  3. package/dist/app/assets/{HomePage-CADE138j.js → HomePage-C9yNw3iu.js} +1 -1
  4. package/dist/app/assets/{LightMode-Cfh7KzN8.js → LightMode-Be4-G-dX.js} +1 -1
  5. package/dist/app/assets/{MainPage-CO3pRlnV.js → MainPage-J_rU2SgI.js} +2 -2
  6. package/dist/app/assets/{MaterializationsPage-p9YjkRXZ.js → MaterializationsPage-DHgPI4MO.js} +1 -1
  7. package/dist/app/assets/ModelPage-BzXfjTu5.js +1 -0
  8. package/dist/app/assets/{PackagePage-e4kN75YR.js → PackagePage-ByoSTLJ4.js} +1 -1
  9. package/dist/app/assets/RouteError-CrSeKN2r.js +1 -0
  10. package/dist/app/assets/{ThemeEditorPage-CciagFTq.js → ThemeEditorPage-Btxnmhtk.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-DNWmkCXa.js → WorkbookPage-D6JLPnWO.js} +1 -1
  12. package/dist/app/assets/{core-Rj_4rRnA.es-BZyvITuO.js → core-BSRgt1mG.es-Bc_D2Lq4.js} +1 -1
  13. package/dist/app/assets/{index-CH2AcDzc.js → index-BRijWj6K.js} +15 -15
  14. package/dist/app/assets/{index-DQa463gC.js → index-BX4D94xw.js} +1 -1
  15. package/dist/app/assets/{index-VBbcc8s6.js → index-Cvy0xtb7.js} +1 -1
  16. package/dist/app/assets/{index-DQpV7MyA.js → index-DMAhIJNC.js} +1 -1
  17. package/dist/app/index.html +1 -1
  18. package/dist/package_load_worker.mjs +59 -20
  19. package/dist/server.mjs +1342 -616
  20. package/package.json +8 -2
  21. package/.eslintrc.json +0 -44
  22. package/.prettierignore +0 -1
  23. package/.prettierrc +0 -4
  24. package/build.ts +0 -97
  25. package/dist/app/assets/ModelPage-C1OSTv-x.js +0 -1
  26. package/dist/app/assets/RouteError-CzbfOkng.js +0 -1
  27. package/eslint.config.mjs +0 -76
  28. package/k6-tests/README.md +0 -144
  29. package/k6-tests/bun.lock +0 -797
  30. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  31. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  32. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  33. package/k6-tests/load-test/load-test-crud.ts +0 -142
  34. package/k6-tests/load-test/load-test.ts +0 -784
  35. package/k6-tests/package.json +0 -27
  36. package/k6-tests/scripts/run-with-otel.sh +0 -50
  37. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  38. package/k6-tests/tsconfig.json +0 -29
  39. package/k6-tests/utils/client_factory.ts +0 -128
  40. package/k6-tests/utils/common.ts +0 -489
  41. package/k6-tests/utils/logger.ts +0 -62
  42. package/k6-tests/utils/types.d.ts +0 -35
  43. package/malloy_mcp.dxt +0 -0
  44. package/publisher.config.example.bigquery.json +0 -33
  45. package/publisher.config.example.duckdb.json +0 -23
  46. package/publisher.config.json +0 -23
  47. package/scripts/bake-duckdb-extensions.js +0 -110
  48. package/src/config.spec.ts +0 -1535
  49. package/src/config.theme.spec.ts +0 -178
  50. package/src/config.ts +0 -1142
  51. package/src/constants.ts +0 -105
  52. package/src/controller/compile.controller.ts +0 -40
  53. package/src/controller/connection.controller.spec.ts +0 -849
  54. package/src/controller/connection.controller.ts +0 -901
  55. package/src/controller/database.controller.ts +0 -24
  56. package/src/controller/materialization.controller.spec.ts +0 -293
  57. package/src/controller/materialization.controller.ts +0 -319
  58. package/src/controller/model.controller.ts +0 -164
  59. package/src/controller/package.controller.spec.ts +0 -273
  60. package/src/controller/package.controller.ts +0 -306
  61. package/src/controller/query.controller.ts +0 -152
  62. package/src/controller/theme.controller.ts +0 -83
  63. package/src/controller/watch-mode.controller.ts +0 -243
  64. package/src/data_styles.ts +0 -71
  65. package/src/default-publisher.config.json +0 -23
  66. package/src/ducklake_version.spec.ts +0 -163
  67. package/src/ducklake_version.ts +0 -153
  68. package/src/errors.spec.ts +0 -119
  69. package/src/errors.ts +0 -237
  70. package/src/filter_deprecation.spec.ts +0 -64
  71. package/src/filter_deprecation.ts +0 -42
  72. package/src/health.spec.ts +0 -123
  73. package/src/health.ts +0 -233
  74. package/src/heap_check.spec.ts +0 -144
  75. package/src/heap_check.ts +0 -144
  76. package/src/instrumentation.ts +0 -214
  77. package/src/json_utils.spec.ts +0 -51
  78. package/src/json_utils.ts +0 -33
  79. package/src/logger.spec.ts +0 -210
  80. package/src/logger.ts +0 -251
  81. package/src/malloy_pin_prereqs.spec.ts +0 -25
  82. package/src/materialization_metrics.spec.ts +0 -169
  83. package/src/materialization_metrics.ts +0 -346
  84. package/src/mcp/error_messages.spec.ts +0 -35
  85. package/src/mcp/error_messages.ts +0 -183
  86. package/src/mcp/handler_utils.spec.ts +0 -108
  87. package/src/mcp/handler_utils.ts +0 -251
  88. package/src/mcp/mcp_constants.ts +0 -6
  89. package/src/mcp/query_envelope.spec.ts +0 -229
  90. package/src/mcp/query_envelope.ts +0 -240
  91. package/src/mcp/server.protocol.spec.ts +0 -250
  92. package/src/mcp/server.ts +0 -87
  93. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  94. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  95. package/src/mcp/skills/skills_bundle.json +0 -1
  96. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  97. package/src/mcp/tool_response.spec.ts +0 -108
  98. package/src/mcp/tool_response.ts +0 -138
  99. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  100. package/src/mcp/tools/compile_tool.ts +0 -208
  101. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  102. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  103. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  104. package/src/mcp/tools/docs_search_tool.ts +0 -128
  105. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  106. package/src/mcp/tools/embedding_index.ts +0 -808
  107. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  108. package/src/mcp/tools/execute_query_tool.ts +0 -316
  109. package/src/mcp/tools/get_context_eval.ts +0 -275
  110. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  111. package/src/mcp/tools/get_context_tool.ts +0 -585
  112. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  113. package/src/mcp/tools/reload_package_tool.ts +0 -132
  114. package/src/mcp_config.spec.ts +0 -919
  115. package/src/mcp_config.ts +0 -425
  116. package/src/oom_guards.integration.spec.ts +0 -269
  117. package/src/package_load/package_load_pool.spec.ts +0 -252
  118. package/src/package_load/package_load_pool.ts +0 -931
  119. package/src/package_load/package_load_worker.ts +0 -1034
  120. package/src/package_load/protocol.ts +0 -364
  121. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  122. package/src/package_load/rpc_wait_accountant.ts +0 -76
  123. package/src/package_load_metrics.spec.ts +0 -114
  124. package/src/package_load_metrics.ts +0 -127
  125. package/src/path_safety.spec.ts +0 -158
  126. package/src/path_safety.ts +0 -146
  127. package/src/pg_helpers.spec.ts +0 -223
  128. package/src/pg_helpers.ts +0 -52
  129. package/src/query_cap_metrics.spec.ts +0 -89
  130. package/src/query_cap_metrics.ts +0 -116
  131. package/src/query_concurrency.spec.ts +0 -247
  132. package/src/query_concurrency.ts +0 -237
  133. package/src/query_metadata_metrics.ts +0 -49
  134. package/src/query_param_utils.ts +0 -29
  135. package/src/query_timeout.spec.ts +0 -224
  136. package/src/query_timeout.ts +0 -179
  137. package/src/runtime/publisher.js +0 -328
  138. package/src/server-old.ts +0 -988
  139. package/src/server.ts +0 -2059
  140. package/src/service/annotations.spec.ts +0 -118
  141. package/src/service/annotations.ts +0 -91
  142. package/src/service/authorize.spec.ts +0 -154
  143. package/src/service/authorize.ts +0 -498
  144. package/src/service/authorize_integration.spec.ts +0 -2041
  145. package/src/service/build_plan.spec.ts +0 -685
  146. package/src/service/build_plan.ts +0 -797
  147. package/src/service/compile_authorize.spec.ts +0 -85
  148. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  149. package/src/service/connection.spec.ts +0 -2360
  150. package/src/service/connection.ts +0 -2187
  151. package/src/service/connection_config.spec.ts +0 -734
  152. package/src/service/connection_config.ts +0 -899
  153. package/src/service/connection_federation.spec.ts +0 -184
  154. package/src/service/connection_fingerprint.spec.ts +0 -102
  155. package/src/service/connection_service.spec.ts +0 -711
  156. package/src/service/connection_service.ts +0 -291
  157. package/src/service/cron_evaluator.spec.ts +0 -79
  158. package/src/service/cron_evaluator.ts +0 -105
  159. package/src/service/db_utils.spec.ts +0 -1085
  160. package/src/service/db_utils.ts +0 -1502
  161. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  162. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  163. package/src/service/embedding_provider.spec.ts +0 -329
  164. package/src/service/embedding_provider.ts +0 -236
  165. package/src/service/environment.ts +0 -2191
  166. package/src/service/environment_admission.spec.ts +0 -344
  167. package/src/service/environment_compile.spec.ts +0 -197
  168. package/src/service/environment_store.spec.ts +0 -2274
  169. package/src/service/environment_store.ts +0 -2434
  170. package/src/service/environment_store_anchoring.spec.ts +0 -107
  171. package/src/service/environment_store_clone.spec.ts +0 -350
  172. package/src/service/explore_visibility.spec.ts +0 -434
  173. package/src/service/exports_probe.spec.ts +0 -107
  174. package/src/service/extension_fetch_policy.spec.ts +0 -256
  175. package/src/service/filter.spec.ts +0 -458
  176. package/src/service/filter.ts +0 -341
  177. package/src/service/filter_bypass.spec.ts +0 -418
  178. package/src/service/filter_integration.spec.ts +0 -1004
  179. package/src/service/freshness.spec.ts +0 -183
  180. package/src/service/freshness.ts +0 -112
  181. package/src/service/gcs_s3_utils.ts +0 -427
  182. package/src/service/given.ts +0 -105
  183. package/src/service/givens_integration.spec.ts +0 -219
  184. package/src/service/manifest_loader.spec.ts +0 -189
  185. package/src/service/manifest_loader.ts +0 -156
  186. package/src/service/materialization_build_session.spec.ts +0 -435
  187. package/src/service/materialization_build_session.ts +0 -681
  188. package/src/service/materialization_config_validation.spec.ts +0 -99
  189. package/src/service/materialization_config_validation.ts +0 -120
  190. package/src/service/materialization_eligibility.spec.ts +0 -158
  191. package/src/service/materialization_eligibility.ts +0 -305
  192. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  193. package/src/service/materialization_scheduler.spec.ts +0 -289
  194. package/src/service/materialization_scheduler.ts +0 -304
  195. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  196. package/src/service/materialization_serve_transform.ts +0 -779
  197. package/src/service/materialization_service.spec.ts +0 -2373
  198. package/src/service/materialization_service.ts +0 -2342
  199. package/src/service/materialization_test_fixtures.ts +0 -249
  200. package/src/service/model.spec.ts +0 -1157
  201. package/src/service/model.ts +0 -3370
  202. package/src/service/model_limits.spec.ts +0 -209
  203. package/src/service/model_limits.ts +0 -131
  204. package/src/service/model_storage_serve.spec.ts +0 -193
  205. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  206. package/src/service/package.spec.ts +0 -612
  207. package/src/service/package.ts +0 -1799
  208. package/src/service/package_manifest.spec.ts +0 -230
  209. package/src/service/package_manifest.ts +0 -241
  210. package/src/service/package_memory_governor.spec.ts +0 -173
  211. package/src/service/package_memory_governor.ts +0 -233
  212. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  213. package/src/service/package_race.spec.ts +0 -208
  214. package/src/service/package_reload_safety.spec.ts +0 -193
  215. package/src/service/package_rollback.spec.ts +0 -190
  216. package/src/service/package_worker_path.spec.ts +0 -645
  217. package/src/service/path_injection.spec.ts +0 -39
  218. package/src/service/persist_annotation_validation.spec.ts +0 -89
  219. package/src/service/persist_annotation_validation.ts +0 -59
  220. package/src/service/persistence_policy.spec.ts +0 -374
  221. package/src/service/proxy.spec.ts +0 -414
  222. package/src/service/proxy.ts +0 -248
  223. package/src/service/query_boundary.spec.ts +0 -470
  224. package/src/service/query_metadata.spec.ts +0 -408
  225. package/src/service/query_metadata.ts +0 -492
  226. package/src/service/query_metadata_identity.spec.ts +0 -149
  227. package/src/service/query_text.spec.ts +0 -79
  228. package/src/service/query_text.ts +0 -43
  229. package/src/service/quoting.spec.ts +0 -135
  230. package/src/service/quoting.ts +0 -78
  231. package/src/service/resolve_environment.ts +0 -15
  232. package/src/service/restricted_mode.spec.ts +0 -299
  233. package/src/service/source_extraction.ts +0 -225
  234. package/src/service/theme_store.ts +0 -199
  235. package/src/storage/DatabaseInterface.ts +0 -198
  236. package/src/storage/StorageManager.ts +0 -113
  237. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  238. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  239. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  240. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  241. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  242. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  243. package/src/storage/duckdb/PackageRepository.ts +0 -135
  244. package/src/storage/duckdb/schema.ts +0 -229
  245. package/src/stream_helpers.spec.ts +0 -280
  246. package/src/stream_helpers.ts +0 -162
  247. package/src/telemetry.ts +0 -20
  248. package/src/test_helpers/metrics_harness.ts +0 -166
  249. package/src/theme_key_parity.spec.ts +0 -57
  250. package/src/utils.ts +0 -29
  251. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  252. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  253. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  254. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  255. package/tests/fixtures/html-pages-test/data.csv +0 -3
  256. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  257. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  258. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  259. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  260. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  261. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  262. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  263. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  264. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  265. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  266. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  267. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  268. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  269. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  270. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  271. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  272. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  273. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  274. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  275. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  276. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  277. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  278. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  279. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  280. package/tests/fixtures/persist-test/publisher.json +0 -5
  281. package/tests/fixtures/publisher.config.json +0 -15
  282. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  283. package/tests/fixtures/query-givens/model.malloy +0 -34
  284. package/tests/fixtures/query-givens/publisher.json +0 -5
  285. package/tests/fixtures/xlsx/database.xlsx +0 -0
  286. package/tests/harness/mcp_test_setup.ts +0 -332
  287. package/tests/harness/rest_e2e.ts +0 -68
  288. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  289. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  290. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  291. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  292. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  293. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  294. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  295. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  296. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  297. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  298. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  299. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  300. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  301. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  302. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  303. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  304. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  305. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  306. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  307. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  308. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  309. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  310. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  311. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  312. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  313. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  314. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  315. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  316. package/tests/unit/duckdb/repositories.test.ts +0 -208
  317. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  318. package/tests/unit/service/theme_store.spec.ts +0 -139
  319. package/tsconfig.json +0 -32
@@ -1,1034 +0,0 @@
1
- /**
2
- * Package-load worker entry point.
3
- *
4
- * Runs inside a worker_threads `Worker`. Owns **no native connection
5
- * state of any kind** — every connection lookup, schema fetch, SQL
6
- * block schema, and non-file URL read is proxied back to the main
7
- * thread via correlated RPC messages. The worker is intentionally
8
- * pure-CPU: only Malloy parser / type-checker / IR-builder runs here.
9
- *
10
- * Why no DuckDB in the worker?
11
- * ----------------------------
12
- * DuckDB's native bindings cannot be safely loaded into more than one
13
- * isolate of the same Node/Bun process (we hit Bun crash 0x20131 when
14
- * the worker isolate and the main isolate both `dlopen` duckdb-native).
15
- * Even if Bun fixes that, holding a duckdb handle in the worker
16
- * duplicates the in-memory DB state and adds native-module load
17
- * latency to every worker spawn. Database probing (`readDatabases`)
18
- * stays on the main thread where it can reuse the package's existing
19
- * DuckDB connection.
20
- *
21
- * Boundary
22
- * --------
23
- * 1. Worker: read `publisher.json` (package manifest).
24
- * 2. Worker: compile every `.malloy` and `.malloynb` via Malloy.
25
- * All `lookupConnection(name)` / schema-fetch calls Malloy makes
26
- * during compile are proxied to the main thread's live
27
- * connection pool over RPC.
28
- * 3. Worker: for each model, capture the structured-clonable fields
29
- * the main-thread `Model` constructor needs: `modelDef`,
30
- * `sourceInfos`, `sources`, `queries`, `filterMap`, `givens`,
31
- * dataStyles, plus per-cell `modelDef` + `queryDef` for
32
- * notebooks (so per-cell materializers / runnables can be
33
- * hydrated on the main thread via `Runtime._loadModelFromModelDef`
34
- * / `ModelMaterializer._loadQueryFromQueryDef` — no recompile).
35
- * 4. Main thread: probe embedded `.parquet` / `.csv` databases
36
- * against the package's existing DuckDB connection.
37
- *
38
- * Per-model compile failures are returned in-band on
39
- * `SerializedModel.compilationError`; the rest of the package keeps
40
- * loading. Whole-package failures (e.g. manifest missing) come back
41
- * as `LoadPackageError`.
42
- *
43
- * Bundled separately by `build.ts` as `dist/package_load_worker.mjs`
44
- * so `new Worker(...)` can load it without dragging in the entire
45
- * server module graph.
46
- */
47
- import {
48
- contextOverlay,
49
- type BuildManifestEntry,
50
- type Connection,
51
- type FetchSchemaOptions,
52
- type LookupConnection,
53
- MalloyConfig,
54
- MalloyError,
55
- type ModelDef,
56
- type ModelMaterializer,
57
- modelDefToModelInfo,
58
- type NamedQueryDef,
59
- type Query,
60
- Runtime,
61
- type SQLSourceDef,
62
- type SQLSourceRequest,
63
- type TableSourceDef,
64
- } from "@malloydata/malloy";
65
- import * as Malloy from "@malloydata/malloy-interfaces";
66
- import {
67
- MalloySQLParser,
68
- MalloySQLStatementType,
69
- } from "@malloydata/malloy-sql";
70
- import * as fs from "fs";
71
- import * as path from "path";
72
- import { parentPort, threadId } from "node:worker_threads";
73
- import recursive from "recursive-readdir";
74
- import { fileURLToPath, pathToFileURL } from "url";
75
-
76
- import {
77
- MODEL_FILE_SUFFIX,
78
- normalizeModelPath,
79
- NOTEBOOK_FILE_SUFFIX,
80
- PACKAGE_MANIFEST_NAME,
81
- } from "../constants";
82
- import { HackyDataStylesAccumulator } from "../data_styles";
83
- import { ModelCompilationError } from "../errors";
84
- import { validateAuthorizeProbes } from "../service/authorize";
85
- import { type FilterDefinition } from "../service/filter";
86
- import {
87
- PackageMaterializationConfig,
88
- PackageScope,
89
- packageMaterializationWarnings,
90
- parsePackageMaterialization,
91
- resolvePackageScope,
92
- } from "../service/package_manifest";
93
- import {
94
- extractQueriesFromModelDef,
95
- extractSourcesFromModelDef,
96
- } from "../service/source_extraction";
97
- import {
98
- malloyGivenToApi,
99
- type MalloyGiven,
100
- type MalloyGivenApi,
101
- } from "../service/given";
102
- import { ignoreDotfiles } from "../utils";
103
- import { RpcWaitAccountant } from "./rpc_wait_accountant";
104
- import type {
105
- ConnectionMetadata,
106
- ConnectionMetadataRequest,
107
- ConnectionMetadataResponse,
108
- LoadPackageError,
109
- LoadPackageRequest,
110
- LoadPackageResult,
111
- MainToWorkerMessage,
112
- ReadUrlRequest,
113
- ReadUrlResponse,
114
- SchemaForSqlRequest,
115
- SchemaForSqlResponse,
116
- SchemaForTablesRequest,
117
- SchemaForTablesResponse,
118
- SerializedError,
119
- SerializedModel,
120
- SerializedNotebookCell,
121
- } from "./protocol";
122
-
123
- if (!parentPort) {
124
- throw new Error(
125
- "package_load_worker.ts must be loaded inside a worker_threads Worker",
126
- );
127
- }
128
-
129
- const port = parentPort;
130
-
131
- // ──────────────────────────────────────────────────────────────────────
132
- // RPC plumbing for worker → main calls
133
- // ──────────────────────────────────────────────────────────────────────
134
-
135
- let nextRpcId = 0;
136
- const pendingRpc = new Map<
137
- string,
138
- { resolve: (value: unknown) => void; reject: (err: Error) => void }
139
- >();
140
-
141
- function newRpcId(): string {
142
- nextRpcId += 1;
143
- return `w${threadId}-rpc-${nextRpcId}`;
144
- }
145
-
146
- function callMain<T>(send: (requestId: string) => void): Promise<T> {
147
- const requestId = newRpcId();
148
- return new Promise<T>((resolve, reject) => {
149
- pendingRpc.set(requestId, {
150
- resolve: (value) => resolve(value as T),
151
- reject,
152
- });
153
- send(requestId);
154
- });
155
- }
156
-
157
- // Per-load phase timing: brackets the wall-clock spent awaiting proxied schema
158
- // fetches so `loadPackage` can separate connection I/O from compile CPU. The
159
- // pool runs one load per worker at a time, so a single module-level instance
160
- // is safe; it's still keyed by jobId to shrug off a straggler fetch from a
161
- // prior, reused-worker load. See {@link RpcWaitAccountant}.
162
- const schemaWait = new RpcWaitAccountant();
163
-
164
- function dispatchMainResponse(message: MainToWorkerMessage): void {
165
- if (
166
- message.type === "schema-for-tables-response" ||
167
- message.type === "schema-for-sql-response" ||
168
- message.type === "read-url-response" ||
169
- message.type === "connection-metadata-response"
170
- ) {
171
- const pending = pendingRpc.get(message.requestId);
172
- if (!pending) return;
173
- pendingRpc.delete(message.requestId);
174
- pending.resolve(message);
175
- return;
176
- }
177
- if (message.type === "rpc-error") {
178
- const pending = pendingRpc.get(message.requestId);
179
- if (!pending) return;
180
- pendingRpc.delete(message.requestId);
181
- pending.reject(deserializeError(message.error));
182
- return;
183
- }
184
- }
185
-
186
- // ──────────────────────────────────────────────────────────────────────
187
- // Proxy connection: stand-in for non-duckdb connections at compile time
188
- // ──────────────────────────────────────────────────────────────────────
189
-
190
- class ProxyConnection {
191
- public readonly name: string;
192
- public readonly dialectName: string;
193
- private readonly digest: string;
194
- private readonly jobId: string;
195
-
196
- constructor(metadata: ConnectionMetadata, jobId: string) {
197
- this.name = metadata.name;
198
- this.dialectName = metadata.dialectName;
199
- this.digest = metadata.digest;
200
- this.jobId = jobId;
201
- }
202
-
203
- getDigest(): string {
204
- return this.digest;
205
- }
206
-
207
- async fetchSchemaForTables(
208
- tables: Record<string, string>,
209
- options: FetchSchemaOptions,
210
- ): Promise<{
211
- schemas: Record<string, TableSourceDef>;
212
- errors: Record<string, string>;
213
- }> {
214
- schemaWait.noteStart(this.jobId);
215
- try {
216
- const response = await callMain<SchemaForTablesResponse>(
217
- (requestId) => {
218
- const req: SchemaForTablesRequest = {
219
- type: "schema-for-tables",
220
- requestId,
221
- jobId: this.jobId,
222
- connectionName: this.name,
223
- tables,
224
- options: serializeFetchOptions(options),
225
- };
226
- port.postMessage(req);
227
- },
228
- );
229
- return { schemas: response.schemas, errors: response.errors };
230
- } finally {
231
- schemaWait.noteSettle(this.jobId);
232
- }
233
- }
234
-
235
- async fetchSchemaForSQLStruct(
236
- sentence: SQLSourceRequest,
237
- options: FetchSchemaOptions,
238
- ): Promise<
239
- | { structDef: SQLSourceDef; error?: undefined }
240
- | { error: string; structDef?: undefined }
241
- > {
242
- schemaWait.noteStart(this.jobId);
243
- let response: SchemaForSqlResponse;
244
- try {
245
- response = await callMain<SchemaForSqlResponse>((requestId) => {
246
- const req: SchemaForSqlRequest = {
247
- type: "schema-for-sql",
248
- requestId,
249
- jobId: this.jobId,
250
- connectionName: this.name,
251
- sentence: sentence as unknown,
252
- options: serializeFetchOptions(options),
253
- };
254
- port.postMessage(req);
255
- });
256
- } finally {
257
- schemaWait.noteSettle(this.jobId);
258
- }
259
- if (response.error !== undefined) return { error: response.error };
260
- if (response.structDef === undefined) {
261
- return { error: "Empty SQL schema response from main thread" };
262
- }
263
- return { structDef: response.structDef };
264
- }
265
-
266
- // Compile path never calls these on a non-duckdb connection (the
267
- // worker doesn't execute non-duckdb SQL). We throw rather than no-op
268
- // so a misrouted call surfaces loudly.
269
- async runSQL(): Promise<never> {
270
- throw new Error(
271
- `ProxyConnection(${this.name}): runSQL is not available in package-load workers`,
272
- );
273
- }
274
- isPool(): false {
275
- return false;
276
- }
277
- canPersist(): false {
278
- return false;
279
- }
280
- canStream(): false {
281
- return false;
282
- }
283
- async close(): Promise<void> {
284
- /* no-op */
285
- }
286
- async idle(): Promise<void> {
287
- /* no-op */
288
- }
289
- async estimateQueryCost(): Promise<never> {
290
- throw new Error(
291
- `ProxyConnection(${this.name}): estimateQueryCost not available in package-load workers`,
292
- );
293
- }
294
- async fetchMetadata(): Promise<Record<string, unknown>> {
295
- return {};
296
- }
297
- async fetchTableMetadata(): Promise<Record<string, unknown>> {
298
- return {};
299
- }
300
- }
301
-
302
- function serializeFetchOptions(options: FetchSchemaOptions): {
303
- refreshTimestamp?: number;
304
- } {
305
- const out: {
306
- refreshTimestamp?: number;
307
- } = {};
308
- if (options.refreshTimestamp !== undefined) {
309
- out.refreshTimestamp = options.refreshTimestamp;
310
- }
311
- return out;
312
- }
313
-
314
- // ──────────────────────────────────────────────────────────────────────
315
- // URLReader: file:// → fs; everything else proxies to main thread
316
- // ──────────────────────────────────────────────────────────────────────
317
-
318
- function makeWorkerUrlReader(jobId: string): {
319
- readURL: (url: URL) => Promise<string>;
320
- } {
321
- return {
322
- readURL: async (url: URL): Promise<string> => {
323
- if (url.protocol === "file:") {
324
- const filePath = fileURLToPath(url);
325
- return fs.promises.readFile(filePath, "utf8");
326
- }
327
- const response = await callMain<ReadUrlResponse>((requestId) => {
328
- const req: ReadUrlRequest = {
329
- type: "read-url",
330
- requestId,
331
- jobId,
332
- url: url.toString(),
333
- };
334
- port.postMessage(req);
335
- });
336
- return response.contents;
337
- },
338
- };
339
- }
340
-
341
- // ──────────────────────────────────────────────────────────────────────
342
- // MalloyConfig assembly inside the worker
343
- // ──────────────────────────────────────────────────────────────────────
344
-
345
- /**
346
- * Build the package's MalloyConfig inside the worker. Every
347
- * connection name — including `"duckdb"` — resolves to a
348
- * {@link ProxyConnection} that RPCs schema fetches back to the main
349
- * thread. The worker never holds a native connection handle, which
350
- * keeps it pure-CPU and avoids dlopen'ing duckdb in a second isolate.
351
- *
352
- * Concurrent `lookupConnection(name)` calls for the same name are
353
- * deduped via `inflight` — Malloy's compile pipeline can fan out
354
- * many schema fetches that all hit `lookupConnection` before any
355
- * resolve, and we don't want to N-multiply the metadata RPC.
356
- */
357
- function buildWorkerMalloyConfig(job: LoadPackageRequest): MalloyConfig {
358
- const proxies = new Map<string, ProxyConnection>();
359
- const inflight = new Map<string, Promise<ProxyConnection>>();
360
-
361
- const config = new MalloyConfig(
362
- { connections: {} },
363
- { config: contextOverlay({ rootDirectory: job.packagePath }) },
364
- );
365
- config.wrapConnections(
366
- (_base: LookupConnection<Connection>): LookupConnection<Connection> => ({
367
- lookupConnection: async (name?: string): Promise<Connection> => {
368
- const effectiveName = name ?? job.defaultConnectionName ?? "duckdb";
369
- const cached = proxies.get(effectiveName);
370
- if (cached) return cached as unknown as Connection;
371
- let pending = inflight.get(effectiveName);
372
- if (!pending) {
373
- pending = (async () => {
374
- const response = await callMain<ConnectionMetadataResponse>(
375
- (requestId) => {
376
- const req: ConnectionMetadataRequest = {
377
- type: "connection-metadata",
378
- requestId,
379
- jobId: job.requestId,
380
- connectionName: effectiveName,
381
- };
382
- port.postMessage(req);
383
- },
384
- );
385
- const proxy = new ProxyConnection(
386
- response.metadata,
387
- job.requestId,
388
- );
389
- proxies.set(effectiveName, proxy);
390
- inflight.delete(effectiveName);
391
- return proxy;
392
- })();
393
- inflight.set(effectiveName, pending);
394
- }
395
- return (await pending) as unknown as Connection;
396
- },
397
- }),
398
- );
399
- return config;
400
- }
401
-
402
- // ──────────────────────────────────────────────────────────────────────
403
- // Filesystem helpers (replicated from service/package.ts so the worker
404
- // stays decoupled from the main-thread service module graph)
405
- // ──────────────────────────────────────────────────────────────────────
406
-
407
- async function readPackageMetadata(packagePath: string): Promise<{
408
- name?: string;
409
- description?: string;
410
- explores?: string[];
411
- queryableSources?: "declared" | "all";
412
- manifestLocation?: string | null;
413
- materialization?: PackageMaterializationConfig | null;
414
- scope?: PackageScope;
415
- manifestWarnings?: string[];
416
- }> {
417
- const manifestPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
418
- const contents = await fs.promises.readFile(manifestPath, "utf8");
419
- const parsed = JSON.parse(contents) as {
420
- name?: string;
421
- description?: string;
422
- explores?: string[];
423
- queryableSources?: unknown;
424
- manifestLocation?: unknown;
425
- materialization?: unknown;
426
- scope?: unknown;
427
- };
428
- // Scope has two homes (canonical `materialization.scope`, deprecated root);
429
- // an invalid value or a conflict between the two throws and fails the load,
430
- // and the deprecation rides back as a warning.
431
- const scope = resolvePackageScope(parsed.scope, parsed.materialization);
432
- const manifestWarnings = [
433
- ...scope.warnings,
434
- ...packageMaterializationWarnings(parsed.materialization),
435
- ];
436
- return {
437
- name: parsed.name,
438
- description: parsed.description,
439
- explores: Array.isArray(parsed.explores)
440
- ? parsed.explores.map(normalizeModelPath)
441
- : undefined,
442
- // Default + invalid fall back to "declared" (fail-safe: queryable ==
443
- // discoverable). Only an explicit "all" opts out of the query boundary.
444
- queryableSources: parsed.queryableSources === "all" ? "all" : "declared",
445
- // URI (gs://, s3://, file://, or local path) of the control-plane-computed
446
- // build manifest. The main thread fetches + binds it after load.
447
- manifestLocation:
448
- typeof parsed.manifestLocation === "string"
449
- ? parsed.manifestLocation
450
- : null,
451
- // Package-level Malloy Persistence policy; surfaced to the control plane,
452
- // which owns scheduling. `schedule`/`freshness` are for the control plane;
453
- // `queryMetadata` is the publisher's own package-level layer.
454
- materialization: parsePackageMaterialization(parsed.materialization),
455
- // Package-level persist scope mode; defaults to "package".
456
- scope: scope.scope,
457
- manifestWarnings:
458
- manifestWarnings.length > 0 ? manifestWarnings : undefined,
459
- };
460
- }
461
-
462
- async function listPackageFiles(packagePath: string): Promise<string[]> {
463
- const files = await recursive(packagePath, [ignoreDotfiles]);
464
- return files.map((full: string) =>
465
- path.relative(packagePath, full).replace(/\\/g, "/"),
466
- );
467
- }
468
-
469
- function filterModelPaths(allRelative: string[]): string[] {
470
- return allRelative.filter(
471
- (p) => p.endsWith(MODEL_FILE_SUFFIX) || p.endsWith(NOTEBOOK_FILE_SUFFIX),
472
- );
473
- }
474
-
475
- // `normalizeModelPath` (shared, from ../constants) runs here at parse time so
476
- // the keys stored in Package.models are already normalized, and the API
477
- // publish/update path normalizes its `explores` input through the same helper —
478
- // so on-disk and API-written manifests share one representation.
479
-
480
- // explores validation is intentionally NOT done here. The worker is the
481
- // shared load path (startup, reload, AND publish), but the policy differs by
482
- // context — strict-reject at publish, warn-and-fail-safe at load — so it lives
483
- // on the main thread (see Package.getInvalidExplores / loadViaWorker and the
484
- // publish path in package.controller). An unmatched entry simply lists nothing
485
- // (listModels filters by set membership), which is the fail-safe outcome.
486
-
487
- // ──────────────────────────────────────────────────────────────────────
488
- // Model compile (mirrors service/model.ts but produces SerializedModel)
489
- // ──────────────────────────────────────────────────────────────────────
490
-
491
- interface ApiSourceWire {
492
- name: string;
493
- annotations?: string[];
494
- views?: { name: string; annotations?: string[] }[];
495
- filters?: unknown[];
496
- givens?: unknown[];
497
- authorize?: string[];
498
- }
499
- interface ApiQueryWire {
500
- name: string;
501
- sourceName?: string;
502
- annotations?: string[];
503
- }
504
- type ApiGivenWire = MalloyGivenApi;
505
-
506
- async function collectImportedSourceInfos(
507
- modelDef: ModelDef,
508
- runtime: Runtime,
509
- importBaseURL: URL,
510
- ): Promise<{
511
- sourceInfos: Malloy.SourceInfo[];
512
- importedNames: Set<string>;
513
- }> {
514
- const sourceInfos: Malloy.SourceInfo[] = [];
515
- const importedNames = new Set<string>();
516
- const imports = modelDef.imports ?? [];
517
- for (const importLocation of imports) {
518
- try {
519
- const modelString = await runtime.urlReader.readURL(
520
- new URL(importLocation.importURL),
521
- );
522
- const importedModelDef = (
523
- await runtime
524
- .loadModel(modelString as string, { importBaseURL })
525
- .getModel()
526
- )._modelDef;
527
- const importedInfo = modelDefToModelInfo(importedModelDef);
528
- const importedSources = importedInfo.entries.filter(
529
- (entry) => entry.kind === "source",
530
- ) as Malloy.SourceInfo[];
531
- for (const source of importedSources) {
532
- if (!importedNames.has(source.name)) {
533
- sourceInfos.push(source);
534
- importedNames.add(source.name);
535
- }
536
- }
537
- } catch {
538
- // Best-effort, matches the in-process Model.create behaviour
539
- // of warning-and-skipping when an import can't be loaded.
540
- }
541
- }
542
- return { sourceInfos, importedNames };
543
- }
544
-
545
- function appendLocalSourceInfos(
546
- modelDef: ModelDef,
547
- target: Malloy.SourceInfo[],
548
- importedNames: Set<string>,
549
- ): void {
550
- const localInfo = modelDefToModelInfo(modelDef);
551
- const localSources = localInfo.entries.filter(
552
- (entry) => entry.kind === "source",
553
- ) as Malloy.SourceInfo[];
554
- for (const source of localSources) {
555
- if (!importedNames.has(source.name)) target.push(source);
556
- }
557
- }
558
-
559
- // Source / query introspection is shared with the in-process path; see
560
- // service/source_extraction.ts. The worker has no logger, so a filter parse
561
- // failure is swallowed silently (no `onParseError` callback) — matching the
562
- // prior worker-local behavior.
563
- function extractSources(
564
- modelDef: ModelDef,
565
- givens: ApiGivenWire[] | undefined,
566
- ): { sources: ApiSourceWire[]; filterMap: Map<string, FilterDefinition[]> } {
567
- const { sources, filterMap } = extractSourcesFromModelDef(modelDef, givens);
568
- return { sources: sources as unknown as ApiSourceWire[], filterMap };
569
- }
570
-
571
- function extractQueries(modelDef: ModelDef): ApiQueryWire[] {
572
- return extractQueriesFromModelDef(modelDef) as ApiQueryWire[];
573
- }
574
-
575
- function buildRuntimeForModel(
576
- job: LoadPackageRequest,
577
- malloyConfig: MalloyConfig,
578
- jobId: string,
579
- ): { runtime: Runtime; urlReader: HackyDataStylesAccumulator } {
580
- const urlReader = new HackyDataStylesAccumulator(makeWorkerUrlReader(jobId));
581
- const runtime = new Runtime({
582
- urlReader,
583
- config: malloyConfig,
584
- buildManifest:
585
- job.buildManifest !== undefined && job.buildManifest !== null
586
- ? {
587
- entries: job.buildManifest as Record<
588
- string,
589
- BuildManifestEntry
590
- >,
591
- strict: false,
592
- }
593
- : undefined,
594
- });
595
- return { runtime, urlReader };
596
- }
597
-
598
- async function compileMalloyModel(
599
- job: LoadPackageRequest,
600
- malloyConfig: MalloyConfig,
601
- modelPath: string,
602
- ): Promise<SerializedModel> {
603
- const compileStart = performance.now();
604
- const fullPath = path.join(job.packagePath, modelPath);
605
- // `pathToFileURL` produces a valid URL on every platform; the
606
- // naïve `file://${fullPath}` template parses host=`D:` on Windows.
607
- const modelURL = pathToFileURL(fullPath);
608
- const importBaseURL = new URL(".", modelURL);
609
-
610
- const { runtime, urlReader } = buildRuntimeForModel(
611
- job,
612
- malloyConfig,
613
- job.requestId,
614
- );
615
- const mm = runtime.loadModel(modelURL, { importBaseURL });
616
- const compiled = await mm.getModel();
617
- const modelDef = compiled._modelDef;
618
-
619
- const malloyGivens = Array.from(compiled.givens.values());
620
- const givens =
621
- malloyGivens.length > 0
622
- ? malloyGivens.map((g) => malloyGivenToApi(g as MalloyGiven))
623
- : undefined;
624
-
625
- const { sourceInfos, importedNames } = await collectImportedSourceInfos(
626
- modelDef,
627
- runtime,
628
- importBaseURL,
629
- );
630
- appendLocalSourceInfos(modelDef, sourceInfos, importedNames);
631
-
632
- const { sources, filterMap } = extractSources(modelDef, givens);
633
- const queries = extractQueries(modelDef);
634
- // Validate #(authorize) at compile time (shared with Model.create). Throws
635
- // on an unknown given / source-field reference; compileOneModel's catch
636
- // turns it into this model's compilationError.
637
- await validateAuthorizeProbes(mm, sources);
638
-
639
- return {
640
- modelPath,
641
- modelType: "model",
642
- modelDef,
643
- modelInfo: modelDefToModelInfo(modelDef),
644
- sourceInfos,
645
- // `sources`/`queries` ship complete (authorize + filter enforcement and
646
- // join resolution read the full set); the Model's discovery accessors
647
- // filter them down to the export closure (`modelDef.exports`) to match
648
- // `modelInfo`/`sourceInfos`.
649
- sources,
650
- queries,
651
- filterMap: Array.from(filterMap.entries()),
652
- givens,
653
- dataStyles: urlReader.getHackyAccumulatedDataStyles(),
654
- compileDurationMs: performance.now() - compileStart,
655
- };
656
- }
657
-
658
- async function compileNotebookModel(
659
- job: LoadPackageRequest,
660
- malloyConfig: MalloyConfig,
661
- modelPath: string,
662
- ): Promise<SerializedModel> {
663
- const compileStart = performance.now();
664
- const fullPath = path.join(job.packagePath, modelPath);
665
- // See compileMalloyModel above: `pathToFileURL` is the only
666
- // cross-platform way to build a file URL from an OS path.
667
- const modelURL = pathToFileURL(fullPath);
668
- const importBaseURL = new URL(".", modelURL);
669
-
670
- const { runtime, urlReader } = buildRuntimeForModel(
671
- job,
672
- malloyConfig,
673
- job.requestId,
674
- );
675
-
676
- const fileContents = await fs.promises.readFile(modelURL, "utf8");
677
- const parse = MalloySQLParser.parse(fileContents, modelPath);
678
-
679
- // Build the extendModel chain synchronously so per-cell materializers
680
- // line up with statement order. Matches the in-process flow in
681
- // Model.getNotebookModelMaterializer.
682
- let mm: ModelMaterializer | undefined;
683
- const perCellMM: (ModelMaterializer | undefined)[] = parse.statements.map(
684
- (stmt) => {
685
- if (stmt.type === MalloySQLStatementType.MALLOY) {
686
- mm =
687
- mm === undefined
688
- ? runtime.loadModel(stmt.text, { importBaseURL })
689
- : mm.extendModel(stmt.text, { importBaseURL });
690
- }
691
- return mm;
692
- },
693
- );
694
-
695
- const oldImports: string[] = [];
696
- const oldSources: Record<string, Malloy.SourceInfo> = {};
697
- const notebookCells: SerializedNotebookCell[] = [];
698
- for (let i = 0; i < parse.statements.length; i++) {
699
- const stmt = parse.statements[i];
700
- if (stmt.type === MalloySQLStatementType.MARKDOWN) {
701
- notebookCells.push({ type: "markdown", text: stmt.text });
702
- continue;
703
- }
704
- if (stmt.type !== MalloySQLStatementType.MALLOY) continue;
705
-
706
- const localMM = perCellMM[i];
707
- if (!localMM) {
708
- // Shouldn't happen for a MALLOY statement, but guard rather
709
- // than crash a whole notebook compile on one corrupt cell.
710
- continue;
711
- }
712
- const currentModelDef = (await localMM.getModel())._modelDef;
713
-
714
- // newSources via the import chain — mirrors in-process logic.
715
- let newSources: Malloy.SourceInfo[] = [];
716
- const newImports = currentModelDef.imports?.slice(oldImports.length);
717
- if (newImports) {
718
- for (const importLocation of newImports) {
719
- try {
720
- const modelString = await runtime.urlReader.readURL(
721
- new URL(importLocation.importURL),
722
- );
723
- const importModel = (
724
- await runtime
725
- .loadModel(modelString as string, { importBaseURL })
726
- .getModel()
727
- )._modelDef;
728
- const importInfo = modelDefToModelInfo(importModel);
729
- newSources = importInfo.entries
730
- .filter((e) => e.kind === "source")
731
- .filter(
732
- (s) => !(s.name in oldSources),
733
- ) as Malloy.SourceInfo[];
734
- oldImports.push(importLocation.importURL.toString());
735
- } catch {
736
- // Same best-effort policy as the in-process path.
737
- }
738
- }
739
- }
740
- const currentInfo = modelDefToModelInfo(currentModelDef);
741
- newSources = newSources.concat(
742
- currentInfo.entries
743
- .filter((e) => e.kind === "source")
744
- .filter((s) => !(s.name in oldSources)) as Malloy.SourceInfo[],
745
- );
746
- for (const s of newSources) oldSources[s.name] = s;
747
-
748
- // Capture the per-cell final-query queryDef so the main thread can
749
- // hydrate a QueryMaterializer via
750
- // `ModelMaterializer._loadQueryFromQueryDef` without a recompile.
751
- const runnable = localMM.loadFinalQuery();
752
- let cellQueryDef: Query | undefined;
753
- let queryInfo: Malloy.QueryInfo | undefined;
754
- try {
755
- const prepared = await runnable.getPreparedQuery();
756
- cellQueryDef = prepared._query;
757
- const queryName =
758
- (prepared._query as NamedQueryDef).as ||
759
- (prepared._query as NamedQueryDef).name;
760
- const anonymous =
761
- currentInfo.anonymous_queries[
762
- currentInfo.anonymous_queries.length - 1
763
- ];
764
- if (anonymous) {
765
- queryInfo = {
766
- name: queryName,
767
- schema: anonymous.schema,
768
- annotations: anonymous.annotations,
769
- definition: anonymous.definition,
770
- code: anonymous.code,
771
- location: anonymous.location,
772
- } as Malloy.QueryInfo;
773
- }
774
- } catch {
775
- // Some cells (source-only) have no final query; that's fine.
776
- }
777
-
778
- notebookCells.push({
779
- type: "code",
780
- text: stmt.text,
781
- cellModelDef: currentModelDef,
782
- cellQueryDef,
783
- newSources,
784
- queryInfo,
785
- });
786
- }
787
-
788
- // Aggregate (notebook-level) artifacts — derived from the final mm
789
- // if any MALLOY statements were present. If the notebook is all
790
- // markdown, modelDef stays undefined and the main thread treats
791
- // this as a notebook with no compiled content.
792
- let finalModelDef: ModelDef | undefined;
793
- let finalSources: ApiSourceWire[] | undefined;
794
- let finalQueries: ApiQueryWire[] | undefined;
795
- let finalSourceInfos: Malloy.SourceInfo[] | undefined;
796
- let finalFilterMap: Map<string, FilterDefinition[]> | undefined;
797
- let finalGivens: ApiGivenWire[] | undefined;
798
- if (mm) {
799
- const compiled = await mm.getModel();
800
- finalModelDef = compiled._modelDef;
801
- const malloyGivens = Array.from(compiled.givens.values());
802
- finalGivens =
803
- malloyGivens.length > 0
804
- ? malloyGivens.map((g) => malloyGivenToApi(g as MalloyGiven))
805
- : undefined;
806
- const collected = await collectImportedSourceInfos(
807
- finalModelDef,
808
- runtime,
809
- importBaseURL,
810
- );
811
- appendLocalSourceInfos(
812
- finalModelDef,
813
- collected.sourceInfos,
814
- collected.importedNames,
815
- );
816
- finalSourceInfos = collected.sourceInfos;
817
- const extracted = extractSources(finalModelDef, finalGivens);
818
- finalSources = extracted.sources;
819
- finalFilterMap = extracted.filterMap;
820
- finalQueries = extractQueries(finalModelDef);
821
- // Validate #(authorize) at compile time (shared with Model.create).
822
- await validateAuthorizeProbes(mm, finalSources);
823
- }
824
-
825
- return {
826
- modelPath,
827
- modelType: "notebook",
828
- modelDef: finalModelDef,
829
- modelInfo: finalModelDef ? modelDefToModelInfo(finalModelDef) : undefined,
830
- sourceInfos: finalSourceInfos,
831
- sources: finalSources,
832
- queries: finalQueries,
833
- filterMap: finalFilterMap
834
- ? Array.from(finalFilterMap.entries())
835
- : undefined,
836
- givens: finalGivens,
837
- notebookCells,
838
- dataStyles: urlReader.getHackyAccumulatedDataStyles(),
839
- compileDurationMs: performance.now() - compileStart,
840
- };
841
- }
842
-
843
- async function compileOneModel(
844
- job: LoadPackageRequest,
845
- malloyConfig: MalloyConfig,
846
- modelPath: string,
847
- ): Promise<SerializedModel> {
848
- try {
849
- if (modelPath.endsWith(MODEL_FILE_SUFFIX)) {
850
- return await compileMalloyModel(job, malloyConfig, modelPath);
851
- }
852
- if (modelPath.endsWith(NOTEBOOK_FILE_SUFFIX)) {
853
- return await compileNotebookModel(job, malloyConfig, modelPath);
854
- }
855
- return {
856
- modelPath,
857
- modelType: "model",
858
- compilationError: {
859
- name: "Error",
860
- message: `Unknown model file suffix: ${modelPath}`,
861
- },
862
- };
863
- } catch (error) {
864
- const modelType: SerializedModel["modelType"] = modelPath.endsWith(
865
- NOTEBOOK_FILE_SUFFIX,
866
- )
867
- ? "notebook"
868
- : "model";
869
- return {
870
- modelPath,
871
- modelType,
872
- compilationError: serializeError(error),
873
- };
874
- }
875
- }
876
-
877
- // ──────────────────────────────────────────────────────────────────────
878
- // The actual load-package job
879
- // ──────────────────────────────────────────────────────────────────────
880
-
881
- async function loadPackage(
882
- job: LoadPackageRequest,
883
- ): Promise<LoadPackageResult> {
884
- const loadStart = performance.now();
885
-
886
- const packageMetadata = await readPackageMetadata(job.packagePath);
887
- const malloyConfig = buildWorkerMalloyConfig(job);
888
-
889
- const allFiles = await listPackageFiles(job.packagePath);
890
- const modelPaths = filterModelPaths(allFiles);
891
-
892
- // Bracket the compile region: only work from here on is compilation +
893
- // proxied schema fetches. The setup above (manifest read + file listing)
894
- // is excluded so it can't inflate the compile figure — it stays in the
895
- // derivable remainder (loadDuration - compile - schemaFetch). Begin the
896
- // schema-fetch accounting HERE, at the region boundary, not at load start:
897
- // that keeps `compileDurationMs = compileRegion - schemaFetchWait` exact
898
- // regardless of whether any fetch ever happens during setup (none do
899
- // today, but this stops that assumption from silently mattering).
900
- const compileRegionStart = performance.now();
901
- schemaWait.begin(job.requestId);
902
- const models = await Promise.all(
903
- modelPaths.map((modelPath) =>
904
- compileOneModel(job, malloyConfig, modelPath),
905
- ),
906
- );
907
-
908
- const loadEnd = performance.now();
909
- const schemaFetchDurationMs = schemaWait.waitMs;
910
- return {
911
- type: "load-package-result",
912
- requestId: job.requestId,
913
- packageMetadata,
914
- models,
915
- loadDurationMs: loadEnd - loadStart,
916
- timings: {
917
- // Compile-region wall minus the schema-fetch wait it contains — a
918
- // conservative ceiling on compile CPU (clamped: rounding can nudge
919
- // it slightly negative).
920
- compileDurationMs: Math.max(
921
- 0,
922
- loadEnd - compileRegionStart - schemaFetchDurationMs,
923
- ),
924
- schemaFetchDurationMs,
925
- schemaFetchCount: schemaWait.fetches,
926
- },
927
- };
928
- }
929
-
930
- // ──────────────────────────────────────────────────────────────────────
931
- // Error serialization
932
- // ──────────────────────────────────────────────────────────────────────
933
-
934
- function serializeError(error: unknown): SerializedError {
935
- if (error instanceof MalloyError) {
936
- return {
937
- name: error.name,
938
- message: error.message,
939
- stack: error.stack,
940
- malloyProblems: error.problems as unknown[],
941
- isCompilationError: true,
942
- };
943
- }
944
- // ModelCompilationError (e.g. an invalid #(authorize) annotation caught by
945
- // validateAuthorizeProbes) carries no Malloy `problems`, but it must keep its
946
- // compilation-error classification across the worker boundary so the main
947
- // thread re-wraps it as a 424, not a generic 500.
948
- if (error instanceof ModelCompilationError) {
949
- return {
950
- name: error.name,
951
- message: error.message,
952
- stack: error.stack,
953
- isCompilationError: true,
954
- };
955
- }
956
- if (error instanceof Error) {
957
- return {
958
- name: error.name,
959
- message: error.message,
960
- stack: error.stack,
961
- };
962
- }
963
- return { name: "Error", message: String(error) };
964
- }
965
-
966
- function deserializeError(serialized: SerializedError): Error {
967
- const err = new Error(serialized.message);
968
- err.name = serialized.name;
969
- if (serialized.stack) err.stack = serialized.stack;
970
- return err;
971
- }
972
-
973
- // ──────────────────────────────────────────────────────────────────────
974
- // Message dispatcher
975
- // ──────────────────────────────────────────────────────────────────────
976
-
977
- let shuttingDown = false;
978
- const inFlightJobs = new Set<string>();
979
-
980
- port.on("message", (message: MainToWorkerMessage) => {
981
- if (message.type === "shutdown") {
982
- shuttingDown = true;
983
- maybeExit();
984
- return;
985
- }
986
- if (message.type === "load-package") {
987
- if (shuttingDown) {
988
- const errMsg: LoadPackageError = {
989
- type: "load-package-error",
990
- requestId: message.requestId,
991
- error: {
992
- name: "ShuttingDown",
993
- message: "Package-load worker is shutting down",
994
- },
995
- };
996
- port.postMessage(errMsg);
997
- return;
998
- }
999
- inFlightJobs.add(message.requestId);
1000
- void runJob(message);
1001
- return;
1002
- }
1003
- dispatchMainResponse(message);
1004
- });
1005
-
1006
- async function runJob(job: LoadPackageRequest): Promise<void> {
1007
- try {
1008
- const result = await loadPackage(job);
1009
- port.postMessage(result);
1010
- } catch (error) {
1011
- const errMsg: LoadPackageError = {
1012
- type: "load-package-error",
1013
- requestId: job.requestId,
1014
- error: serializeError(error),
1015
- };
1016
- port.postMessage(errMsg);
1017
- } finally {
1018
- inFlightJobs.delete(job.requestId);
1019
- maybeExit();
1020
- }
1021
- }
1022
-
1023
- function maybeExit(): void {
1024
- if (shuttingDown && inFlightJobs.size === 0 && pendingRpc.size === 0) {
1025
- // Give the postMessage queue a tick to flush before exit so the
1026
- // last result actually reaches the parent.
1027
- setImmediate(() => process.exit(0));
1028
- }
1029
- }
1030
-
1031
- // Announce readiness — the pool waits for this before dispatching jobs
1032
- // to a newly-spawned worker so we don't race the worker's module-init
1033
- // time.
1034
- port.postMessage({ type: "ready" });