@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,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" });