emdash 0.17.2 → 0.18.0

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 (289) hide show
  1. package/dist/api/route-utils.mjs +10 -10
  2. package/dist/api/schemas/index.d.mts +1 -1
  3. package/dist/{api-B7GATEYo.mjs → api-Cs7DAACP.mjs} +12 -12
  4. package/dist/{api-B7GATEYo.mjs.map → api-Cs7DAACP.mjs.map} +1 -1
  5. package/dist/{apply-BrVqULFe.mjs → apply-BWMV4Zmw.mjs} +16 -16
  6. package/dist/{apply-BrVqULFe.mjs.map → apply-BWMV4Zmw.mjs.map} +1 -1
  7. package/dist/astro/index.d.mts +2 -2
  8. package/dist/astro/index.mjs +1 -1
  9. package/dist/astro/middleware/auth.d.mts +2 -2
  10. package/dist/astro/middleware/auth.mjs +2 -2
  11. package/dist/astro/middleware/redirect.mjs +4 -4
  12. package/dist/astro/middleware.d.mts.map +1 -1
  13. package/dist/astro/middleware.mjs +250 -83
  14. package/dist/astro/middleware.mjs.map +1 -1
  15. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
  16. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
  17. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  18. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  19. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
  20. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
  21. package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
  22. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
  23. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +8 -8
  24. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +8 -8
  25. package/dist/astro/routes/api/admin/bylines/index.mjs +8 -8
  26. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
  27. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  28. package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
  29. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  30. package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
  31. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  32. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  33. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  34. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  35. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +25 -25
  36. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +25 -25
  37. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +25 -25
  38. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +25 -25
  39. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +25 -25
  40. package/dist/astro/routes/api/admin/plugins/index.mjs +25 -25
  41. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  42. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +25 -25
  43. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +25 -25
  44. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +25 -25
  45. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +25 -25
  46. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +26 -26
  47. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +25 -25
  48. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +26 -26
  49. package/dist/astro/routes/api/admin/plugins/updates.mjs +25 -25
  50. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +25 -25
  51. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  52. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +25 -25
  53. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  54. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  55. package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
  56. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  57. package/dist/astro/routes/api/admin/users/index.mjs +3 -3
  58. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  59. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  60. package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
  61. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  62. package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
  63. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  64. package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
  65. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  66. package/dist/astro/routes/api/auth/me.mjs +4 -4
  67. package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
  68. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  69. package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
  70. package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
  71. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
  72. package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
  73. package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
  74. package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
  75. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  76. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
  77. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  78. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  79. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  80. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  81. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
  82. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
  83. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  84. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  85. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
  86. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
  87. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  88. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  89. package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
  90. package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
  91. package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
  92. package/dist/astro/routes/api/dashboard.mjs +7 -7
  93. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  94. package/dist/astro/routes/api/import/probe.mjs +4 -4
  95. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  96. package/dist/astro/routes/api/import/wordpress/execute.d.mts +2 -2
  97. package/dist/astro/routes/api/import/wordpress/execute.mjs +7 -7
  98. package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
  99. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  100. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +5 -5
  101. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
  102. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +5 -5
  103. package/dist/astro/routes/api/manifest.mjs +3 -3
  104. package/dist/astro/routes/api/mcp.mjs +26 -26
  105. package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
  106. package/dist/astro/routes/api/media/_id_.mjs +4 -4
  107. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  108. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  109. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  110. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  111. package/dist/astro/routes/api/media/upload-url.mjs +4 -4
  112. package/dist/astro/routes/api/media.mjs +5 -5
  113. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
  114. package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
  115. package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
  116. package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
  117. package/dist/astro/routes/api/menus/_name_.mjs +5 -5
  118. package/dist/astro/routes/api/menus/index.mjs +5 -5
  119. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  120. package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
  121. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  122. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  123. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  124. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  125. package/dist/astro/routes/api/oauth/token.mjs +2 -2
  126. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  127. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  128. package/dist/astro/routes/api/redirects/404s/index.mjs +6 -6
  129. package/dist/astro/routes/api/redirects/404s/summary.mjs +6 -6
  130. package/dist/astro/routes/api/redirects/_id_.mjs +7 -7
  131. package/dist/astro/routes/api/redirects/index.mjs +7 -7
  132. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  133. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  134. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +25 -25
  135. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +25 -25
  136. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +25 -25
  137. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +25 -25
  138. package/dist/astro/routes/api/schema/collections/index.mjs +25 -25
  139. package/dist/astro/routes/api/schema/index.mjs +6 -6
  140. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +25 -25
  141. package/dist/astro/routes/api/schema/orphans/index.mjs +25 -25
  142. package/dist/astro/routes/api/search/enable.mjs +7 -7
  143. package/dist/astro/routes/api/search/index.mjs +6 -6
  144. package/dist/astro/routes/api/search/rebuild.mjs +7 -7
  145. package/dist/astro/routes/api/search/stats.mjs +6 -6
  146. package/dist/astro/routes/api/search/suggest.mjs +6 -6
  147. package/dist/astro/routes/api/sections/_slug_.mjs +6 -6
  148. package/dist/astro/routes/api/sections/index.mjs +6 -6
  149. package/dist/astro/routes/api/settings/email.mjs +4 -4
  150. package/dist/astro/routes/api/settings.mjs +8 -8
  151. package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
  152. package/dist/astro/routes/api/setup/admin.mjs +3 -3
  153. package/dist/astro/routes/api/setup/dev-bypass.mjs +15 -15
  154. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  155. package/dist/astro/routes/api/setup/index.mjs +16 -16
  156. package/dist/astro/routes/api/setup/status.mjs +3 -3
  157. package/dist/astro/routes/api/snapshot.mjs +3 -3
  158. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +8 -8
  159. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +8 -8
  160. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +8 -8
  161. package/dist/astro/routes/api/taxonomies/index.mjs +8 -8
  162. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  163. package/dist/astro/routes/api/typegen.mjs +5 -5
  164. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
  165. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -6
  166. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -6
  167. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  168. package/dist/astro/routes/api/widget-areas/index.mjs +6 -6
  169. package/dist/astro/routes/api/widget-components.mjs +2 -2
  170. package/dist/astro/routes/robots.txt.mjs +4 -4
  171. package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
  172. package/dist/astro/routes/sitemap.xml.mjs +4 -4
  173. package/dist/astro/types.d.mts +2 -2
  174. package/dist/{authorize-CLTmOUyx.mjs → authorize-CotM4Yiu.mjs} +2 -2
  175. package/dist/{authorize-CLTmOUyx.mjs.map → authorize-CotM4Yiu.mjs.map} +1 -1
  176. package/dist/{byline-CAhk4FrG.mjs → byline-CWQ9aSoz.mjs} +3 -3
  177. package/dist/{byline-CAhk4FrG.mjs.map → byline-CWQ9aSoz.mjs.map} +1 -1
  178. package/dist/{byline-fields-CR5hGLMw.d.mts → byline-fields-BNy7Ng1U.d.mts} +28 -28
  179. package/dist/{byline-fields-CR5hGLMw.d.mts.map → byline-fields-BNy7Ng1U.d.mts.map} +1 -1
  180. package/dist/{bylines-DCczH3AV.mjs → bylines-BJSva1Un.mjs} +4 -4
  181. package/dist/{bylines-DCczH3AV.mjs.map → bylines-BJSva1Un.mjs.map} +1 -1
  182. package/dist/{bylines-CbrD7STW.mjs → bylines-LJMgENMI.mjs} +3 -3
  183. package/dist/{bylines-CbrD7STW.mjs.map → bylines-LJMgENMI.mjs.map} +1 -1
  184. package/dist/{cache-DIHHyPkt.mjs → cache-lZL7SgVb.mjs} +2 -2
  185. package/dist/{cache-DIHHyPkt.mjs.map → cache-lZL7SgVb.mjs.map} +1 -1
  186. package/dist/{chunks-DnnHlRG3.mjs → chunks-BU-vP9Dh.mjs} +2 -2
  187. package/dist/{chunks-DnnHlRG3.mjs.map → chunks-BU-vP9Dh.mjs.map} +1 -1
  188. package/dist/cli/index.mjs +13 -13
  189. package/dist/{comment-DkAfGX9E.mjs → comment-C4jVbCM8.mjs} +2 -2
  190. package/dist/{comment-DkAfGX9E.mjs.map → comment-C4jVbCM8.mjs.map} +1 -1
  191. package/dist/{comments-DLFnXs7J.mjs → comments-BTAbC0Ek.mjs} +3 -3
  192. package/dist/{comments-DLFnXs7J.mjs.map → comments-BTAbC0Ek.mjs.map} +1 -1
  193. package/dist/{content-C7aJ7keg.mjs → content-CyqOmOzm.mjs} +3 -3
  194. package/dist/{content-C7aJ7keg.mjs.map → content-CyqOmOzm.mjs.map} +1 -1
  195. package/dist/{context-Ca0HkaIh.mjs → context-DZ7bEh5-.mjs} +7 -7
  196. package/dist/{context-Ca0HkaIh.mjs.map → context-DZ7bEh5-.mjs.map} +1 -1
  197. package/dist/{dashboard-BrfLIsX1.mjs → dashboard-B5WQpNTP.mjs} +4 -4
  198. package/dist/{dashboard-BrfLIsX1.mjs.map → dashboard-B5WQpNTP.mjs.map} +1 -1
  199. package/dist/db/index.mjs +1 -1
  200. package/dist/{error-Bk9s3Ism.mjs → error-DJOsMVSt.mjs} +2 -2
  201. package/dist/{error-Bk9s3Ism.mjs.map → error-DJOsMVSt.mjs.map} +1 -1
  202. package/dist/{fts-manager-XpDfbIKo.mjs → fts-manager-DR1ERA0c.mjs} +2 -2
  203. package/dist/{fts-manager-XpDfbIKo.mjs.map → fts-manager-DR1ERA0c.mjs.map} +1 -1
  204. package/dist/{index-C8ciqSMJ.d.mts → index-CjKdMZ3U.d.mts} +4 -4
  205. package/dist/{index-C8ciqSMJ.d.mts.map → index-CjKdMZ3U.d.mts.map} +1 -1
  206. package/dist/index.d.mts +2 -2
  207. package/dist/index.mjs +34 -34
  208. package/dist/{load-CF5oETkh.mjs → load-6ZrRhepW.mjs} +2 -2
  209. package/dist/{load-CF5oETkh.mjs.map → load-6ZrRhepW.mjs.map} +1 -1
  210. package/dist/{loader-BxyvbrZP.mjs → loader-Dyx8dhFV.mjs} +3 -3
  211. package/dist/{loader-BxyvbrZP.mjs.map → loader-Dyx8dhFV.mjs.map} +1 -1
  212. package/dist/media/local-runtime.d.mts +2 -2
  213. package/dist/media/local-runtime.mjs +4 -4
  214. package/dist/{media-Cyz5BhSN.mjs → media-C-oovGCG.mjs} +2 -2
  215. package/dist/{media-Cyz5BhSN.mjs.map → media-C-oovGCG.mjs.map} +1 -1
  216. package/dist/{menus-CIdZ_Q6U.mjs → menus-BKkxXCmd.mjs} +30 -11
  217. package/dist/menus-BKkxXCmd.mjs.map +1 -0
  218. package/dist/{menus-PFp8FDuO.mjs → menus-DugoYwTX.mjs} +2 -2
  219. package/dist/{menus-PFp8FDuO.mjs.map → menus-DugoYwTX.mjs.map} +1 -1
  220. package/dist/{parse-B-K21lvm.mjs → parse-BBkFmLVr.mjs} +2 -2
  221. package/dist/{parse-B-K21lvm.mjs.map → parse-BBkFmLVr.mjs.map} +1 -1
  222. package/dist/plugin-utils.d.mts +2 -2
  223. package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
  224. package/dist/{query-Cc649nDl.mjs → query-Ctlq1aOk.mjs} +10 -10
  225. package/dist/{query-Cc649nDl.mjs.map → query-Ctlq1aOk.mjs.map} +1 -1
  226. package/dist/{rate-limit-BI1OdpQH.mjs → rate-limit-CH6W6ikK.mjs} +2 -2
  227. package/dist/{rate-limit-BI1OdpQH.mjs.map → rate-limit-CH6W6ikK.mjs.map} +1 -1
  228. package/dist/{redirect-C-FeA4j9.mjs → redirect-C6tJA7tk.mjs} +2 -2
  229. package/dist/{redirect-C-FeA4j9.mjs.map → redirect-C6tJA7tk.mjs.map} +1 -1
  230. package/dist/{redirects-C1UgU9E0.mjs → redirects-CacE9eQa.mjs} +3 -3
  231. package/dist/{redirects-C1UgU9E0.mjs.map → redirects-CacE9eQa.mjs.map} +1 -1
  232. package/dist/{registry-C-T_PWgp.mjs → registry-CIDxZbhh.mjs} +4 -4
  233. package/dist/{registry-C-T_PWgp.mjs.map → registry-CIDxZbhh.mjs.map} +1 -1
  234. package/dist/runner-DM1yR5qd.d.mts.map +1 -1
  235. package/dist/{runner-BiuUfx-V.mjs → runner-pt6Wl-l-.mjs} +8 -3
  236. package/dist/{runner-BiuUfx-V.mjs.map → runner-pt6Wl-l-.mjs.map} +1 -1
  237. package/dist/runtime.d.mts +2 -2
  238. package/dist/runtime.mjs +2 -2
  239. package/dist/{schema-BpCJh2lU.mjs → schema-B4tk0HAG.mjs} +4 -4
  240. package/dist/{schema-BpCJh2lU.mjs.map → schema-B4tk0HAG.mjs.map} +1 -1
  241. package/dist/{search-BrF7k0Ho.mjs → search-f-fNfwab.mjs} +4 -4
  242. package/dist/{search-BrF7k0Ho.mjs.map → search-f-fNfwab.mjs.map} +1 -1
  243. package/dist/{sections-8DEa-dWt.mjs → sections-biElLfT9.mjs} +3 -3
  244. package/dist/{sections-8DEa-dWt.mjs.map → sections-biElLfT9.mjs.map} +1 -1
  245. package/dist/seed/index.mjs +13 -13
  246. package/dist/{seo-CKr7pLfA.mjs → seo-BR39kvTF.mjs} +2 -2
  247. package/dist/{seo-CKr7pLfA.mjs.map → seo-BR39kvTF.mjs.map} +1 -1
  248. package/dist/{service-9P2cdyR_.mjs → service-BhR2acnc.mjs} +2 -2
  249. package/dist/{service-9P2cdyR_.mjs.map → service-BhR2acnc.mjs.map} +1 -1
  250. package/dist/{settings-DYVzINdn.mjs → settings-D_NJvjgN.mjs} +3 -3
  251. package/dist/{settings-DYVzINdn.mjs.map → settings-D_NJvjgN.mjs.map} +1 -1
  252. package/dist/{settings-Jro4YcUb.mjs → settings-b5zW1R1T.mjs} +3 -3
  253. package/dist/{settings-Jro4YcUb.mjs.map → settings-b5zW1R1T.mjs.map} +1 -1
  254. package/dist/{taxonomies-CGD6y79Q.mjs → taxonomies-Crtzy4MT.mjs} +8 -7
  255. package/dist/taxonomies-Crtzy4MT.mjs.map +1 -0
  256. package/dist/{taxonomies-C0bVme_m.mjs → taxonomies-Mhn9rjTQ.mjs} +4 -4
  257. package/dist/{taxonomies-C0bVme_m.mjs.map → taxonomies-Mhn9rjTQ.mjs.map} +1 -1
  258. package/dist/{taxonomy-Db5xwphL.mjs → taxonomy-DTZrIQpi.mjs} +3 -3
  259. package/dist/{taxonomy-Db5xwphL.mjs.map → taxonomy-DTZrIQpi.mjs.map} +1 -1
  260. package/dist/{types-CfyYQ7eY.mjs → types-K3MDsxpy.mjs} +2 -2
  261. package/dist/{types-CfyYQ7eY.mjs.map → types-K3MDsxpy.mjs.map} +1 -1
  262. package/dist/{user-tLdHUEXV.mjs → user-DzEUl5zA.mjs} +2 -2
  263. package/dist/{user-tLdHUEXV.mjs.map → user-DzEUl5zA.mjs.map} +1 -1
  264. package/dist/{validate-DWmnRg6E.mjs → validate-JCXcsqiY.mjs} +2 -2
  265. package/dist/{validate-DWmnRg6E.mjs.map → validate-JCXcsqiY.mjs.map} +1 -1
  266. package/dist/{validation-BQ_TP-On.mjs → validation-Bq-VyKJg.mjs} +5 -5
  267. package/dist/{validation-BQ_TP-On.mjs.map → validation-Bq-VyKJg.mjs.map} +1 -1
  268. package/dist/version-CnS-Cr8A.mjs +7 -0
  269. package/dist/{version-CgcnMvqS.mjs.map → version-CnS-Cr8A.mjs.map} +1 -1
  270. package/dist/{widgets-DzlINGI6.mjs → widgets-Bap1eS1X.mjs} +2 -2
  271. package/dist/{widgets-DzlINGI6.mjs.map → widgets-Bap1eS1X.mjs.map} +1 -1
  272. package/dist/{zod-generator-MMm56Prt.mjs → zod-generator-BSDpkqSH.mjs} +4 -3
  273. package/dist/zod-generator-BSDpkqSH.mjs.map +1 -0
  274. package/package.json +7 -7
  275. package/src/astro/middleware/stream-end-metrics.ts +96 -0
  276. package/src/astro/middleware.ts +80 -32
  277. package/src/components/EmDashImage.astro +1 -0
  278. package/src/database/migrations/runner.ts +7 -2
  279. package/src/emdash-runtime.ts +177 -126
  280. package/src/menus/index.ts +27 -9
  281. package/src/plugins/hooks.ts +35 -6
  282. package/src/plugins/manager.ts +1 -0
  283. package/src/schema/zod-generator.ts +6 -2
  284. package/src/taxonomies/index.ts +12 -8
  285. package/src/utils/init-lock.ts +143 -0
  286. package/dist/menus-CIdZ_Q6U.mjs.map +0 -1
  287. package/dist/taxonomies-CGD6y79Q.mjs.map +0 -1
  288. package/dist/version-CgcnMvqS.mjs +0 -7
  289. package/dist/zod-generator-MMm56Prt.mjs.map +0 -1
@@ -27,12 +27,14 @@ import { sandboxedPlugins as virtualSandboxedPlugins } from "virtual:emdash/sand
27
27
  // @ts-ignore - virtual module
28
28
  import { createStorage as virtualCreateStorage } from "virtual:emdash/storage";
29
29
 
30
+ import { after } from "../after.js";
30
31
  import {
31
32
  createRecorder,
32
33
  flushRecorder,
33
34
  isInstrumentationEnabled,
34
35
  } from "../database/instrumentation.js";
35
36
  import {
37
+ DB_INIT_DEADLINE_MS,
36
38
  EmDashRuntime,
37
39
  type RuntimeDependencies,
38
40
  type SandboxedPluginEntry,
@@ -51,17 +53,20 @@ import {
51
53
  runWithContext,
52
54
  } from "../request-context.js";
53
55
  import { isMissingTableError } from "../utils/db-errors.js";
56
+ import { createInitLock, type InitLock, initWithLock } from "../utils/init-lock.js";
54
57
  import type { EmDashConfig } from "./integration/runtime.js";
58
+ import { wrapBodyForStreamMetrics } from "./middleware/stream-end-metrics.js";
55
59
  import { createPublicPluginApiRouteHandler } from "./public-plugin-api-routes.js";
56
60
  import type { EmDashHandlers } from "./types.js";
57
61
 
58
- // Cached runtime instance (persists across requests within worker)
59
- let runtimeInstance: EmDashRuntime | null = null;
60
- // Whether initialization is in progress (prevents concurrent init attempts)
61
- let runtimeInitializing = false;
62
-
63
- /** Whether i18n config has been initialized from the virtual module */
64
- let i18nInitialized = false;
62
+ /**
63
+ * Runtime init lock reclaim deadline. Must be strictly larger than the db
64
+ * init deadline: this lock wraps EmDashRuntime.create() getDatabase()
65
+ * the db init lock, and equal deadlines would let this outer lock reclaim
66
+ * (spawning a second cron scheduler and sandbox runner) while the inner db
67
+ * init is legitimately still working through a contended migration.
68
+ */
69
+ const RUNTIME_INIT_DEADLINE_MS = DB_INIT_DEADLINE_MS + 15_000;
65
70
 
66
71
  /**
67
72
  * Whether we've verified the database has been set up.
@@ -89,6 +94,32 @@ function markSetupVerified(): void {
89
94
  setupFlagStore[SETUP_VERIFIED_KEY] = true;
90
95
  }
91
96
 
97
+ /**
98
+ * The runtime singleton and its init lock live on globalThis behind a
99
+ * Symbol — same reasoning as SETUP_VERIFIED_KEY above: the bundler can
100
+ * duplicate this module across SSR chunks, and a duplicated instance/lock
101
+ * would mean multiple runtimes (each with its own cron scheduler) per
102
+ * isolate, initializing and reclaiming independently.
103
+ */
104
+ const RUNTIME_HOLDER_KEY = Symbol.for("emdash:runtime-holder");
105
+ interface RuntimeHolder {
106
+ instance: EmDashRuntime | null;
107
+ lock: InitLock;
108
+ }
109
+
110
+ function getRuntimeHolder(): RuntimeHolder {
111
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below
112
+ let holder = setupFlagStore[RUNTIME_HOLDER_KEY] as RuntimeHolder | undefined;
113
+ if (!holder) {
114
+ holder = { instance: null, lock: createInitLock() };
115
+ setupFlagStore[RUNTIME_HOLDER_KEY] = holder;
116
+ }
117
+ return holder;
118
+ }
119
+
120
+ /** Whether i18n config has been initialized from the virtual module */
121
+ let i18nInitialized = false;
122
+
92
123
  /**
93
124
  * Get EmDash configuration from virtual module
94
125
  */
@@ -176,29 +207,40 @@ async function getRuntime(
176
207
  config: EmDashConfig,
177
208
  initTimings?: Array<{ name: string; dur: number; desc?: string }>,
178
209
  ): Promise<EmDashRuntime> {
179
- // Return cached instance if available
180
- if (runtimeInstance) {
181
- return runtimeInstance;
182
- }
183
-
184
- // If another request is already initializing, wait and retry.
185
- // We don't share the promise across requests because workerd flags
186
- // cross-request promise resolution (causes warnings + potential hangs).
187
- if (runtimeInitializing) {
188
- // Poll until the initializing request finishes
189
- await new Promise((resolve) => setTimeout(resolve, 50));
190
- return getRuntime(config, initTimings);
191
- }
192
-
193
- runtimeInitializing = true;
194
- try {
195
- const deps = buildDependencies(config);
196
- const runtime = await EmDashRuntime.create(deps, initTimings);
197
- runtimeInstance = runtime;
198
- return runtime;
199
- } finally {
200
- runtimeInitializing = false;
201
- }
210
+ // Waiters poll rather than awaiting the initializing request's promise —
211
+ // workerd flags cross-request promise resolution (warnings + potential
212
+ // hangs). If the initializing request is cancelled mid-create (client
213
+ // disconnect tears down its continuation, skipping any `finally`), the
214
+ // anchored init keeps running under waitUntil and populates the cache;
215
+ // failing that, the stale lock is reclaimed after a deadline instead of
216
+ // hanging every subsequent request in the isolate until eviction.
217
+ const holder = getRuntimeHolder();
218
+ return initWithLock(
219
+ holder.lock,
220
+ () => holder.instance,
221
+ async (isCurrentClaim) => {
222
+ const deps = buildDependencies(config);
223
+ const runtime = await EmDashRuntime.create(deps, initTimings);
224
+ if (isCurrentClaim()) {
225
+ holder.instance = runtime;
226
+ } else {
227
+ // This init was reclaimed mid-flight (it ran past the deadline
228
+ // and a waiter started its own). Don't overwrite the
229
+ // reclaimer's published runtime, and stop this one's cron
230
+ // scheduler so it doesn't keep firing unreferenced. The
231
+ // runtime is still returned — it's fully functional for the
232
+ // request that built it.
233
+ runtime.stopCron().catch((error: unknown) => {
234
+ console.error("[emdash] failed to stop superseded runtime's cron:", error);
235
+ });
236
+ }
237
+ return runtime;
238
+ },
239
+ {
240
+ deadlineMs: RUNTIME_INIT_DEADLINE_MS,
241
+ anchor: (promise) => after(() => promise),
242
+ },
243
+ );
202
244
  }
203
245
 
204
246
  /**
@@ -429,7 +471,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
429
471
  timings.push({ name: "render", dur: performance.now() - t0, desc: "Page render" });
430
472
  timings.push({ name: "mw", dur: performance.now() - mwStart, desc: "Total middleware" });
431
473
  pushMetricsTimings(timings, metrics);
432
- return finalizeResponse(response, timings);
474
+ // Server-Timing only sees pre-stream queries; the stream-end
475
+ // wrapper (instrumentation-gated, no-op otherwise) emits the
476
+ // final counters once the body finishes streaming.
477
+ return wrapBodyForStreamMetrics(finalizeResponse(response, timings));
433
478
  };
434
479
  if (anonScoped) {
435
480
  const parent = getRequestContext();
@@ -596,7 +641,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
596
641
  timings.push({ name: "render", dur: performance.now() - t0, desc: "Page render" });
597
642
  timings.push({ name: "mw", dur: performance.now() - mwStart, desc: "Total middleware" });
598
643
  pushMetricsTimings(timings, metrics);
599
- return finalizeResponse(response, timings);
644
+ // Server-Timing only sees pre-stream queries; the stream-end
645
+ // wrapper (instrumentation-gated, no-op otherwise) emits the
646
+ // final counters once the body finishes streaming.
647
+ return wrapBodyForStreamMetrics(finalizeResponse(response, timings));
600
648
  };
601
649
 
602
650
  if (scoped) {
@@ -170,6 +170,7 @@ const imgProps: Record<string, unknown> = {
170
170
  height: finalHeight,
171
171
  alt: finalAlt,
172
172
  loading: priority ? "eager" : "lazy",
173
+ fetchpriority: priority ? "high" : undefined,
173
174
  decoding: "async",
174
175
  style: placeholderStyle ? `${baseStyle} ${placeholderStyle}` : baseStyle,
175
176
  ...attrs,
@@ -178,8 +178,13 @@ const MIGRATION_RACE_PATTERN = new RegExp(
178
178
  "i",
179
179
  );
180
180
 
181
- /** How long to wait for a concurrent migrator to finish before giving up. */
182
- const MIGRATION_RACE_WAIT_MS = 10_000;
181
+ /**
182
+ * How long to wait for a concurrent migrator to finish before giving up.
183
+ * Exported because the db init lock's reclaim deadline must comfortably
184
+ * exceed it (see DB_INIT_DEADLINE_MS in emdash-runtime.ts) — a healthy
185
+ * init can legitimately block this long inside waitForConcurrentMigrator.
186
+ */
187
+ export const MIGRATION_RACE_WAIT_MS = 10_000;
183
188
  /** Polling interval while waiting for a concurrent migrator. */
184
189
  const MIGRATION_RACE_POLL_MS = 100;
185
190
 
@@ -22,7 +22,7 @@ import { getAuthMode } from "./auth/mode.js";
22
22
  import { getTrustedProxyHeaders } from "./auth/trusted-proxy.js";
23
23
  import { isSqlite } from "./database/dialect-helpers.js";
24
24
  import { kyselyLogOption } from "./database/instrumentation.js";
25
- import { runMigrations } from "./database/migrations/runner.js";
25
+ import { MIGRATION_RACE_WAIT_MS, runMigrations } from "./database/migrations/runner.js";
26
26
  import { RevisionRepository } from "./database/repositories/revision.js";
27
27
  import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
28
28
  import { validateIdentifier } from "./database/validate.js";
@@ -41,6 +41,7 @@ import type {
41
41
  } from "./plugins/types.js";
42
42
  import type { FieldType } from "./schema/types.js";
43
43
  import { hashString } from "./utils/hash.js";
44
+ import { createInitLock, type InitLock, initWithLock } from "./utils/init-lock.js";
44
45
  import { COMMIT, VERSION } from "./version.js";
45
46
 
46
47
  const LEADING_SLASH_PATTERN = /^\//;
@@ -310,9 +311,38 @@ function contentItemToRecord(item: ContentItemInternal): Record<string, unknown>
310
311
  return { ...item };
311
312
  }
312
313
 
313
- // Module-level caches (persist across requests within worker)
314
- const dbCache = new Map<string, Kysely<Database>>();
315
- let dbInitPromise: Promise<Kysely<Database>> | null = null;
314
+ /**
315
+ * Db init lock reclaim deadline. Derived from the migration race wait so
316
+ * they can't drift apart: a healthy init can legitimately block for the
317
+ * full MIGRATION_RACE_WAIT_MS inside waitForConcurrentMigrator, plus cold
318
+ * connect and migrator work, before it should be presumed dead. The outer
319
+ * runtime init lock (middleware.ts) must use a strictly larger deadline —
320
+ * it wraps create() → getDatabase() → this lock, and equal deadlines would
321
+ * let the outer reclaim while the inner is legitimately still working.
322
+ */
323
+ export const DB_INIT_DEADLINE_MS = MIGRATION_RACE_WAIT_MS + 20_000;
324
+
325
+ /**
326
+ * Db cache + its init lock live on globalThis behind a Symbol: the bundler
327
+ * can duplicate this module across SSR chunks (same reasoning as
328
+ * request-cache.ts), and a duplicated cache/lock would mean concurrent
329
+ * independent db inits — and duplicate migrators — per isolate.
330
+ */
331
+ const DB_HOLDER_KEY = Symbol.for("emdash:db-cache");
332
+ interface DbHolder {
333
+ cache: Map<string, Kysely<Database>>;
334
+ lock: InitLock;
335
+ }
336
+ const globalSymbolStore = globalThis as Record<symbol, unknown>;
337
+ function getDbHolder(): DbHolder {
338
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis symbol slot, written only below
339
+ let holder = globalSymbolStore[DB_HOLDER_KEY] as DbHolder | undefined;
340
+ if (!holder) {
341
+ holder = { cache: new Map<string, Kysely<Database>>(), lock: createInitLock() };
342
+ globalSymbolStore[DB_HOLDER_KEY] = holder;
343
+ }
344
+ return holder;
345
+ }
316
346
  const storageCache = new Map<string, Storage>();
317
347
  const sandboxedPluginCache = new Map<string, SandboxedPluginInstance>();
318
348
  /**
@@ -887,19 +917,45 @@ export class EmDashRuntime {
887
917
  // Initialize storage (sync)
888
918
  const storage = EmDashRuntime.getStorage(deps);
889
919
 
890
- // Fetch plugin states from database
920
+ // Fetch plugin states and site info concurrently — independent reads
921
+ // against different tables (_plugin_state vs options), so they share
922
+ // one round-trip window instead of paying two sequential ones. Each
923
+ // phase() wrapper still records that phase's own duration, and each
924
+ // body keeps its own non-fatal catch.
891
925
  let pluginStates: Map<string, string> = new Map();
892
- await phase("rt.plugins", "Plugin states", async () => {
893
- try {
894
- const states = await db
895
- .selectFrom("_plugin_state")
896
- .select(["plugin_id", "status"])
897
- .execute();
898
- pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
899
- } catch {
900
- // Plugin state table may not exist yet
901
- }
902
- });
926
+ let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
927
+ await Promise.all([
928
+ // Fetch plugin states from database
929
+ phase("rt.plugins", "Plugin states", async () => {
930
+ try {
931
+ const states = await db
932
+ .selectFrom("_plugin_state")
933
+ .select(["plugin_id", "status"])
934
+ .execute();
935
+ pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
936
+ } catch {
937
+ // Plugin state table may not exist yet
938
+ }
939
+ }),
940
+ // Load site info for plugin context extensions (1 batch query instead of 3)
941
+ phase("rt.site", "Site info options", async () => {
942
+ try {
943
+ const optionsRepo = new OptionsRepository(db);
944
+ const siteOpts = await optionsRepo.getMany<string>([
945
+ "emdash:site_title",
946
+ "emdash:site_url",
947
+ "emdash:locale",
948
+ ]);
949
+ siteInfo = {
950
+ siteName: siteOpts.get("emdash:site_title") ?? undefined,
951
+ siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
952
+ locale: siteOpts.get("emdash:locale") ?? undefined,
953
+ };
954
+ } catch {
955
+ // Options table may not exist yet (pre-setup)
956
+ }
957
+ }),
958
+ ]);
903
959
 
904
960
  // Build set of enabled plugins
905
961
  const enabledPlugins = new Set<string>();
@@ -910,26 +966,6 @@ export class EmDashRuntime {
910
966
  }
911
967
  }
912
968
 
913
- // Load site info for plugin context extensions (1 batch query instead of 3)
914
- let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
915
- await phase("rt.site", "Site info options", async () => {
916
- try {
917
- const optionsRepo = new OptionsRepository(db);
918
- const siteOpts = await optionsRepo.getMany<string>([
919
- "emdash:site_title",
920
- "emdash:site_url",
921
- "emdash:locale",
922
- ]);
923
- siteInfo = {
924
- siteName: siteOpts.get("emdash:site_title") ?? undefined,
925
- siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
926
- locale: siteOpts.get("emdash:locale") ?? undefined,
927
- };
928
- } catch {
929
- // Options table may not exist yet (pre-setup)
930
- }
931
- });
932
-
933
969
  // Build the full list of pipeline-eligible plugins: all configured
934
970
  // plugins (regardless of current enabled status) plus built-in plugins.
935
971
  // rebuildHookPipeline() filters this to only enabled plugins.
@@ -1050,32 +1086,43 @@ export class EmDashRuntime {
1050
1086
  EmDashRuntime.loadSandboxedPlugins(deps, db, storage),
1051
1087
  );
1052
1088
 
1053
- // Cold-start: load marketplace-installed plugins from site R2 via
1054
- // the sandbox runner. In bypass mode this was already handled above.
1089
+ // Cold-start: load marketplace- and registry-installed plugins from
1090
+ // site R2 via the sandbox runner. The two tiers only depend on the
1091
+ // sandbox phase above, not on each other, so when both are enabled
1092
+ // they run concurrently instead of paying two sequential loads.
1093
+ // In bypass mode marketplace plugins were already handled above.
1094
+ const installedTierPhases: Promise<void>[] = [];
1055
1095
  if (deps.config.marketplace && storage && !deps.sandboxBypassed) {
1056
- await phase("rt.market", "Marketplace plugins", () =>
1057
- EmDashRuntime.loadInstalledSandboxedPlugins(
1058
- "marketplace",
1059
- db,
1060
- storage,
1061
- deps,
1062
- sandboxedPlugins,
1096
+ installedTierPhases.push(
1097
+ phase("rt.market", "Marketplace plugins", () =>
1098
+ EmDashRuntime.loadInstalledSandboxedPlugins(
1099
+ "marketplace",
1100
+ db,
1101
+ storage,
1102
+ deps,
1103
+ sandboxedPlugins,
1104
+ ),
1063
1105
  ),
1064
1106
  );
1065
1107
  }
1066
1108
 
1067
1109
  // Cold-start: load registry-installed plugins from site R2
1068
1110
  if (deps.config.experimental?.registry && storage) {
1069
- await phase("rt.registry", "Registry plugins", () =>
1070
- EmDashRuntime.loadInstalledSandboxedPlugins(
1071
- "registry",
1072
- db,
1073
- storage,
1074
- deps,
1075
- sandboxedPlugins,
1111
+ installedTierPhases.push(
1112
+ phase("rt.registry", "Registry plugins", () =>
1113
+ EmDashRuntime.loadInstalledSandboxedPlugins(
1114
+ "registry",
1115
+ db,
1116
+ storage,
1117
+ deps,
1118
+ sandboxedPlugins,
1119
+ ),
1076
1120
  ),
1077
1121
  );
1078
1122
  }
1123
+ if (installedTierPhases.length > 0) {
1124
+ await Promise.all(installedTierPhases);
1125
+ }
1079
1126
 
1080
1127
  // Initialize media providers
1081
1128
  const mediaProviders = new Map<string, MediaProvider>();
@@ -1270,83 +1317,86 @@ export class EmDashRuntime {
1270
1317
 
1271
1318
  const cacheKey = dbConfig.entrypoint;
1272
1319
 
1273
- // Return cached instance if available
1274
- const cached = dbCache.get(cacheKey);
1275
- if (cached) {
1276
- return cached;
1277
- }
1278
-
1279
- // Use initialization lock to prevent race conditions.
1280
- // Sharing this promise across requests is safe because the Kysely instance
1281
- // doesn't hold a request-scoped resource — the DO dialect uses a getStub()
1282
- // factory that creates a fresh stub per query execution.
1283
- if (dbInitPromise) {
1284
- return dbInitPromise;
1285
- }
1286
-
1287
- dbInitPromise = (async () => {
1288
- const dialect = deps.createDialect(dbConfig.config);
1289
- const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
1290
-
1291
- await runMigrations(db);
1292
-
1293
- // Note: legacy installs may carry a stray `emdash:manifest_cache`
1294
- // row in the options table from versions that persisted a JSON
1295
- // manifest. The runtime no longer reads or writes it. We do not
1296
- // proactively delete it: the row is a few hundred bytes of dead
1297
- // weight and is never on the read path, whereas a one-shot
1298
- // cleanup-flag check costs an extra `options.get()` on every
1299
- // isolate cold boot forever. Cheaper to leave it.
1300
-
1301
- // Auto-seed schema if no collections exist and setup hasn't run.
1302
- // This covers first-load on sites that skip the setup wizard.
1303
- // Dev-bypass and the wizard apply seeds explicitly.
1304
- try {
1305
- const [collectionCount, setupOption] = await Promise.all([
1306
- db
1307
- .selectFrom("_emdash_collections")
1308
- .select((eb) => eb.fn.countAll<number>().as("count"))
1309
- .executeTakeFirstOrThrow(),
1310
- db
1311
- .selectFrom("options")
1312
- .select("value")
1313
- .where("name", "=", "emdash:setup_complete")
1314
- .executeTakeFirst(),
1315
- ]);
1316
-
1317
- const setupDone = (() => {
1318
- try {
1319
- return setupOption && JSON.parse(setupOption.value) === true;
1320
- } catch {
1321
- return false;
1322
- }
1323
- })();
1324
-
1325
- if (collectionCount.count === 0 && !setupDone) {
1326
- const { applySeed } = await import("./seed/apply.js");
1327
- const { loadSeed } = await import("./seed/load.js");
1328
- const { validateSeed } = await import("./seed/validate.js");
1329
-
1330
- const seed = await loadSeed();
1331
- const validation = validateSeed(seed);
1332
- if (validation.valid) {
1333
- await applySeed(db, seed, { onConflict: "skip" });
1334
- console.log("Auto-seeded default collections");
1320
+ // Waiters poll the cache rather than sharing the initializing request's
1321
+ // promise: if the request that owns the init is cancelled mid-await
1322
+ // (e.g. client disconnect during cold migrations), a shared promise
1323
+ // never settles — and the owner's `finally` that would clear it never
1324
+ // runs — deadlocking every later request in the isolate. Prevention:
1325
+ // the in-flight init is anchored via after()/waitUntil so a cancelled
1326
+ // owner's init still completes and populates the cache. Net: a stale
1327
+ // lock is reclaimed after a deadline.
1328
+ const holder = getDbHolder();
1329
+ return initWithLock(
1330
+ holder.lock,
1331
+ () => holder.cache.get(cacheKey),
1332
+ async (isCurrentClaim) => {
1333
+ const dialect = deps.createDialect(dbConfig.config);
1334
+ const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
1335
+
1336
+ await runMigrations(db);
1337
+
1338
+ // Note: legacy installs may carry a stray `emdash:manifest_cache`
1339
+ // row in the options table from versions that persisted a JSON
1340
+ // manifest. The runtime no longer reads or writes it. We do not
1341
+ // proactively delete it: the row is a few hundred bytes of dead
1342
+ // weight and is never on the read path, whereas a one-shot
1343
+ // cleanup-flag check costs an extra `options.get()` on every
1344
+ // isolate cold boot forever. Cheaper to leave it.
1345
+
1346
+ // Auto-seed schema if no collections exist and setup hasn't run.
1347
+ // This covers first-load on sites that skip the setup wizard.
1348
+ // Dev-bypass and the wizard apply seeds explicitly.
1349
+ try {
1350
+ const [collectionCount, setupOption] = await Promise.all([
1351
+ db
1352
+ .selectFrom("_emdash_collections")
1353
+ .select((eb) => eb.fn.countAll<number>().as("count"))
1354
+ .executeTakeFirstOrThrow(),
1355
+ db
1356
+ .selectFrom("options")
1357
+ .select("value")
1358
+ .where("name", "=", "emdash:setup_complete")
1359
+ .executeTakeFirst(),
1360
+ ]);
1361
+
1362
+ const setupDone = (() => {
1363
+ try {
1364
+ return setupOption && JSON.parse(setupOption.value) === true;
1365
+ } catch {
1366
+ return false;
1367
+ }
1368
+ })();
1369
+
1370
+ if (collectionCount.count === 0 && !setupDone) {
1371
+ const { applySeed } = await import("./seed/apply.js");
1372
+ const { loadSeed } = await import("./seed/load.js");
1373
+ const { validateSeed } = await import("./seed/validate.js");
1374
+
1375
+ const seed = await loadSeed();
1376
+ const validation = validateSeed(seed);
1377
+ if (validation.valid) {
1378
+ await applySeed(db, seed, { onConflict: "skip" });
1379
+ console.log("Auto-seeded default collections");
1380
+ }
1335
1381
  }
1382
+ } catch {
1383
+ // Tables may not exist yet. Non-fatal.
1336
1384
  }
1337
- } catch {
1338
- // Tables may not exist yet. Non-fatal.
1339
- }
1340
1385
 
1341
- dbCache.set(cacheKey, db);
1342
- return db;
1343
- })();
1344
-
1345
- try {
1346
- return await dbInitPromise;
1347
- } finally {
1348
- dbInitPromise = null;
1349
- }
1386
+ // Publish only while still the current owner: a reclaimed slow
1387
+ // init must not flip the cached Kysely identity back after the
1388
+ // reclaimer has published its own. The unpublished instance is
1389
+ // still returned and fully valid for the request that built it.
1390
+ if (isCurrentClaim()) {
1391
+ holder.cache.set(cacheKey, db);
1392
+ }
1393
+ return db;
1394
+ },
1395
+ {
1396
+ deadlineMs: DB_INIT_DEADLINE_MS,
1397
+ anchor: (promise) => after(() => promise),
1398
+ },
1399
+ );
1350
1400
  }
1351
1401
 
1352
1402
  /**
@@ -1778,6 +1828,7 @@ export class EmDashRuntime {
1778
1828
  pipeline,
1779
1829
  isActive: () => true,
1780
1830
  getOption: (key) => optionsRepo.get<string>(key),
1831
+ getOptions: (keys) => optionsRepo.getMany<string>(keys),
1781
1832
  setOption: (key, value) => optionsRepo.set(key, value),
1782
1833
  deleteOption: async (key) => {
1783
1834
  await optionsRepo.delete(key);
@@ -136,15 +136,10 @@ async function buildMenuTree(
136
136
  }
137
137
  }
138
138
 
139
- const urlPatterns = new Map<string, string | null>();
140
- if (collectionSlugs.size > 0) {
141
- const rows = await db
142
- .selectFrom("_emdash_collections")
143
- .select(["slug", "url_pattern"])
144
- .where("slug", "in", [...collectionSlugs])
145
- .execute();
146
- for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
147
- }
139
+ const urlPatterns =
140
+ collectionSlugs.size > 0
141
+ ? await getCollectionUrlPatterns(db, collectionSlugs)
142
+ : new Map<string, string | null>();
148
143
 
149
144
  const resolvedItems = await Promise.all(
150
145
  items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
@@ -173,6 +168,29 @@ async function buildMenuTree(
173
168
  return rootItems;
174
169
  }
175
170
 
171
+ /**
172
+ * Look up the `url_pattern` for a set of collection slugs, request-cached so
173
+ * a page rendering several menus (header, footer, ...) only pays for the
174
+ * lookup once per distinct slug set. Callers must treat the returned map as
175
+ * read-only — it is shared across cache hits within the request.
176
+ */
177
+ function getCollectionUrlPatterns(
178
+ db: Kysely<Database>,
179
+ collectionSlugs: Set<string>,
180
+ ): Promise<Map<string, string | null>> {
181
+ const key = `menu-collection-patterns:${[...collectionSlugs].toSorted().join(",")}`;
182
+ return requestCached(key, async () => {
183
+ const rows = await db
184
+ .selectFrom("_emdash_collections")
185
+ .select(["slug", "url_pattern"])
186
+ .where("slug", "in", [...collectionSlugs])
187
+ .execute();
188
+ const urlPatterns = new Map<string, string | null>();
189
+ for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
190
+ return urlPatterns;
191
+ });
192
+ }
193
+
176
194
  /**
177
195
  * Resolve a single menu item's URL. `reference_id` is a translation_group
178
196
  * (migration 036 remapped all existing references); we join it against