@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.20

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 (275) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-B9VYGA3N.js +1 -0
  3. package/.output/public/assets/HashValueDisplay-D880zntR.js +1 -0
  4. package/.output/public/assets/InsightsInfo-By_sPjTE.js +1 -0
  5. package/.output/public/assets/InsightsPageHeader-9_kx_LL7.js +1 -0
  6. package/.output/public/assets/NotFoundPage-Dys8C0cW.js +1 -0
  7. package/.output/public/assets/api-CtarlV0l.js +1 -0
  8. package/.output/public/assets/api-keys-DFF_QqtN.js +1 -0
  9. package/.output/public/assets/api-keys-evfsQ4sN.js +1 -0
  10. package/.output/public/assets/arrow-left-BP9D-ta_.js +1 -0
  11. package/.output/public/assets/arrow-up-right-CDP9Tevw.js +1 -0
  12. package/.output/public/assets/card-DzCPSRsh.js +1 -0
  13. package/.output/public/assets/console-features-api-yv7rm3DX.js +1 -0
  14. package/.output/public/assets/dialog-BDvCy_QO.js +1 -0
  15. package/.output/public/assets/dist-Clb6cQ9l.js +1 -0
  16. package/.output/public/assets/empty-C5x9mn_s.js +1 -0
  17. package/.output/public/assets/field-Bou7shoO.js +1 -0
  18. package/.output/public/assets/index-0eg3l8aC.js +10 -0
  19. package/.output/public/assets/input-group-DVtWFUUF.js +1 -0
  20. package/.output/public/assets/insights-2MbdiSNv.js +1 -0
  21. package/.output/public/assets/insights-C6tmCZHk.js +2 -0
  22. package/.output/public/assets/insights-CadS_aIA.js +42 -0
  23. package/.output/public/assets/insights-recovery-rpc-wvdyM4LD.js +1 -0
  24. package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
  25. package/.output/public/assets/insights-usage-rpc-qUkNtrWr.js +1 -0
  26. package/.output/public/assets/insights_.distribution-BhBs6PNA.js +1 -0
  27. package/.output/public/assets/insights_.distribution-CItZw3-8.js +1 -0
  28. package/.output/public/assets/insights_.distribution-CJUH_wAS.js +2 -0
  29. package/.output/public/assets/installations-CH2H7JQt.js +2 -0
  30. package/.output/public/assets/installations-DMeMuwsW.js +1 -0
  31. package/.output/public/assets/installations-DnvxI9Dt.js +1 -0
  32. package/.output/public/assets/loader-circle-Ca7C9YY1.js +1 -0
  33. package/.output/public/assets/mutation-x9L28_CD.js +1 -0
  34. package/.output/public/assets/preload-helper-BN-UkjOE.js +1 -0
  35. package/.output/public/assets/refresh-cw-CVGqvy8Z.js +1 -0
  36. package/.output/public/assets/routes-BhK8DuZA.js +1 -0
  37. package/.output/public/assets/routes-veKtDW39.js +2 -0
  38. package/.output/public/assets/sidebar-B45aMqRm.js +3 -0
  39. package/.output/public/assets/styles-Dn0pK1EX.css +2 -0
  40. package/.output/public/assets/table-CwLnN6KX.js +1 -0
  41. package/.output/public/assets/useCompositeListItem-CF7vsfcA.js +1 -0
  42. package/.output/public/assets/useMutation-CN1ygYEr.js +1 -0
  43. package/.output/public/assets/useOpenInteractionType-AnAMWYIY.js +1 -0
  44. package/.output/public/assets/valueToPercent-C-t8FT0y.js +1 -0
  45. package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-CUQLz7hs.mjs} +52 -40
  46. package/.output/server/_libs/@base-ui/react+[...].mjs +7044 -3622
  47. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  48. package/.output/server/_libs/@tanstack/react-router+[...].mjs +49 -3
  49. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +4 -4
  50. package/.output/server/_libs/d3-array.mjs +231 -0
  51. package/.output/server/_libs/d3-color.mjs +330 -0
  52. package/.output/server/_libs/d3-format.mjs +252 -0
  53. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  54. package/.output/server/_libs/d3-path.mjs +77 -0
  55. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  56. package/.output/server/_libs/d3-shape.mjs +818 -0
  57. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  58. package/.output/server/_libs/es-toolkit.mjs +1159 -0
  59. package/.output/server/_libs/eventemitter3.mjs +236 -0
  60. package/.output/server/_libs/immer.mjs +532 -0
  61. package/.output/server/_libs/lucide-react.mjs +46 -32
  62. package/.output/server/_libs/next-themes.mjs +2 -2
  63. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  64. package/.output/server/_libs/recharts+[...].mjs +14350 -0
  65. package/.output/server/_libs/sonner.mjs +2 -2
  66. package/.output/server/_libs/tanstack__react-query.mjs +8 -3
  67. package/.output/server/_libs/verkit.mjs +99 -1
  68. package/.output/server/_runtime.mjs +10 -1
  69. package/.output/server/_ssr/ConsoleFeatureUnavailable-DoxET2AT.mjs +58 -0
  70. package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-BplboHEb.mjs} +3 -3
  71. package/.output/server/_ssr/InsightsInfo-o2YPd5Qc.mjs +37 -0
  72. package/.output/server/_ssr/InsightsPageHeader-NExCJL1I.mjs +154 -0
  73. package/.output/server/_ssr/NotFoundPage-Cd-A8ock.mjs +37 -0
  74. package/.output/server/_ssr/api-B1Ro0uys.mjs +202 -0
  75. package/.output/server/_ssr/api-C0G9MTMI.mjs +4205 -0
  76. package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-CT3HG7KT.mjs} +49 -9
  77. package/.output/server/_ssr/api-keys-Dvvy1sem.mjs +5 -0
  78. package/.output/server/_ssr/api-keys-rpc-b1qTTFfG.mjs +52 -0
  79. package/.output/server/_ssr/api-rpc-Cbns2MGx.mjs +338 -0
  80. package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-DChS7m9y.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-Ccqz2iVv.mjs +37 -0
  82. package/.output/server/_ssr/bundleActivity-D17Pb_h5.mjs +19 -0
  83. package/.output/server/_ssr/card-DMbcvlPz.mjs +49 -0
  84. package/.output/server/_ssr/config-DMEJdhoW.mjs +13 -0
  85. package/.output/server/_ssr/config.server-DyypN6cQ.mjs +60 -0
  86. package/.output/server/_ssr/console-access-KpPv5lin.mjs +17 -0
  87. package/.output/server/_ssr/console-features-CsbyHYfK.mjs +55 -0
  88. package/.output/server/_ssr/console-features-api-B-KVuo4p.mjs +43 -0
  89. package/.output/server/_ssr/console-features-rpc-Djyo5740.mjs +19 -0
  90. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-B_I3Ll4M.mjs} +28 -2
  91. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-DuNm5mmo.mjs} +2 -2
  92. package/.output/server/_ssr/databaseRows-CXxf_VIB.mjs +132 -0
  93. package/.output/server/_ssr/{deleteBundle-BTGwBgzx.mjs → deleteBundle-Dcc21Lbd.mjs} +9 -19
  94. package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-cOmLTUV2.mjs} +2 -2
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-B-r4u3Lg.mjs} +28 -5
  96. package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-CexOFoZ4.mjs} +6 -7
  97. package/.output/server/_ssr/empty-BHoY_5jt.mjs +49 -0
  98. package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-mjhcNEXD.mjs} +63 -51
  99. package/.output/server/_ssr/getBundleChildren-DbDXe9wB.mjs +22 -0
  100. package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-96H30HWC.mjs} +2 -2
  101. package/.output/server/_ssr/insights-D25VDVaz.mjs +5 -0
  102. package/.output/server/_ssr/insights-D359GJFL.mjs +14 -0
  103. package/.output/server/_ssr/insights-DgFv1NsO.mjs +1238 -0
  104. package/.output/server/_ssr/insights-api-ChAKrbhG.mjs +105 -0
  105. package/.output/server/_ssr/insights-failures-WQ-ttjOx.mjs +44 -0
  106. package/.output/server/_ssr/insights-failures-rpc-BFsqlltN.mjs +21 -0
  107. package/.output/server/_ssr/insights-recovery-DkL6bxMW.mjs +28 -0
  108. package/.output/server/_ssr/insights-recovery-rpc-2P5m8F62.mjs +46 -0
  109. package/.output/server/_ssr/insights-recovery-rpc-Tj8vNC19.mjs +20 -0
  110. package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-DrkVmV9o.mjs} +22 -14
  111. package/.output/server/_ssr/insights-search-BsalnNw-.mjs +25 -0
  112. package/.output/server/_ssr/insights-usage-1nVTQrLy.mjs +29 -0
  113. package/.output/server/_ssr/insights-usage-rpc-Ct_4xHhZ.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-DaYIRtFK.mjs +41 -0
  115. package/.output/server/_ssr/insightsRecovery-CpGucWN1.mjs +45 -0
  116. package/.output/server/_ssr/insightsUsage-JYpKZAF7.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-Bcu6uYs_.mjs +5 -0
  118. package/.output/server/_ssr/insights_.distribution-D3Csuyjw.mjs +14 -0
  119. package/.output/server/_ssr/insights_.distribution-jQZEF07J.mjs +335 -0
  120. package/.output/server/_ssr/installations-B8Vh54lH.mjs +69 -0
  121. package/.output/server/_ssr/installations-BI5kelHv.mjs +5 -0
  122. package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-Cxikllmx.mjs} +517 -54
  123. package/.output/server/_ssr/listReleases-DqU9t1l3.mjs +79 -0
  124. package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
  125. package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-CRkm-wHD.mjs} +117 -124
  126. package/.output/server/_ssr/routes-B3IJo_jL.mjs +292 -0
  127. package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-CNAi0Mmv.mjs} +176 -455
  128. package/.output/server/_ssr/runtime.server-NucbYjc7.mjs +1372 -0
  129. package/.output/server/_ssr/select-BkPAJFL7.mjs +107 -0
  130. package/.output/server/_ssr/{server-D_C2rMzf.mjs → server-DKJYsaCP.mjs} +6 -32
  131. package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-Cw1VuC7d.mjs} +117 -63
  132. package/.output/server/_ssr/ssr.mjs +1 -1
  133. package/.output/server/_ssr/table-q0P9wz7x.mjs +53 -0
  134. package/.output/server/_ssr/updateFailures-DMU9f0pK.mjs +24 -0
  135. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-BzSO-fr-.mjs} +2 -2
  136. package/.output/server/_tanstack-start-manifest_v-Bb0gKDbb.mjs +124 -0
  137. package/.output/server/index.mjs +273 -168
  138. package/README.md +71 -65
  139. package/lib/index.d.mts +13 -11
  140. package/lib/vite-internal.mjs +6 -3
  141. package/package.json +8 -8
  142. package/src/components/AppSidebar.tsx +91 -71
  143. package/src/components/ConsoleAccessPage.tsx +5 -1
  144. package/src/components/ConsoleFeatureUnavailable.tsx +82 -0
  145. package/src/components/NotFoundPage.tsx +7 -7
  146. package/src/components/features/FeatureSlots.tsx +99 -0
  147. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  148. package/src/components/features/bundles/BundleMetadata.tsx +7 -4
  149. package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
  150. package/src/components/features/insights/AppUsage.tsx +370 -0
  151. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  152. package/src/components/features/insights/EventDetails.tsx +117 -37
  153. package/src/components/features/insights/EventHistoryCard.tsx +112 -26
  154. package/src/components/features/insights/EventHistoryList.tsx +2 -2
  155. package/src/components/features/insights/InsightsControls.tsx +265 -102
  156. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  157. package/src/components/features/insights/InsightsInfo.tsx +48 -0
  158. package/src/components/features/insights/InsightsOverview.tsx +176 -173
  159. package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
  160. package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
  161. package/src/components/features/insights/InstallationHistoryCard.tsx +52 -10
  162. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  163. package/src/components/features/insights/ReleaseActivity.tsx +188 -0
  164. package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
  165. package/src/components/features/insights/UpdateFailures.tsx +319 -0
  166. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  167. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -1
  168. package/src/components/ui/card.tsx +1 -1
  169. package/src/components/ui/empty.tsx +101 -0
  170. package/src/components/ui/scroll-area.tsx +52 -0
  171. package/src/components/ui/sidebar.tsx +22 -29
  172. package/src/components/ui/tabs.tsx +81 -0
  173. package/src/index.ts +12 -17
  174. package/src/lib/api-keys-api.ts +0 -28
  175. package/src/lib/api-keys-rpc.ts +27 -38
  176. package/src/lib/api-rpc.ts +197 -221
  177. package/src/lib/api.ts +11 -13
  178. package/src/lib/bundle-activity.ts +31 -0
  179. package/src/lib/console-access.ts +20 -0
  180. package/src/lib/console-features-api.ts +68 -0
  181. package/src/lib/console-features-rpc.ts +13 -0
  182. package/src/lib/console-features.ts +91 -0
  183. package/src/lib/insights-api.ts +37 -1
  184. package/src/lib/insights-failures-rpc.ts +21 -0
  185. package/src/lib/insights-failures.ts +129 -0
  186. package/src/lib/insights-recovery-rpc.ts +51 -0
  187. package/src/lib/insights-recovery.ts +68 -0
  188. package/src/lib/insights-retention.ts +21 -0
  189. package/src/lib/insights-rpc.ts +30 -12
  190. package/src/lib/insights-search.ts +58 -0
  191. package/src/lib/insights-usage-rpc.ts +29 -0
  192. package/src/lib/insights-usage.ts +74 -0
  193. package/src/lib/insights-view.ts +38 -2
  194. package/src/lib/scroll-restoration.ts +21 -0
  195. package/src/lib/server/adminInsights.ts +124 -0
  196. package/src/lib/server/auth.server.ts +13 -5
  197. package/src/lib/server/bundleActivity.ts +37 -0
  198. package/src/lib/server/config.server.ts +16 -32
  199. package/src/lib/server/console-feature-apis.ts +59 -0
  200. package/src/lib/server/console-runtime.server.ts +6 -40
  201. package/src/lib/server/deleteBundle.ts +16 -25
  202. package/src/lib/server/downloadBundle.ts +7 -10
  203. package/src/lib/server/getBundleChildren.ts +31 -122
  204. package/src/lib/server/insightsRecovery.ts +55 -0
  205. package/src/lib/server/insightsUsage.ts +63 -0
  206. package/src/lib/server/listReleases.ts +109 -44
  207. package/src/lib/server/releaseReachability.ts +4 -3
  208. package/src/lib/server/runtime.server.ts +132 -31
  209. package/src/lib/server/updateFailures.ts +31 -0
  210. package/src/routeTree.gen.ts +21 -21
  211. package/src/router.tsx +3 -2
  212. package/src/routes/-installations-search.ts +32 -8
  213. package/src/routes/-releases-search.ts +76 -29
  214. package/src/routes/__root.tsx +10 -4
  215. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  216. package/src/routes/api-keys.tsx +5 -2
  217. package/src/routes/index.tsx +167 -104
  218. package/src/routes/insights.tsx +100 -59
  219. package/src/routes/insights_.distribution.tsx +398 -0
  220. package/src/routes/installations.tsx +98 -41
  221. package/src/vite-internal.ts +6 -3
  222. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
  223. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
  224. package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
  225. package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
  226. package/.output/public/assets/card-Dhr_eFYj.js +0 -1
  227. package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
  228. package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
  229. package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
  230. package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
  231. package/.output/public/assets/dist-C_NydL0e.js +0 -1
  232. package/.output/public/assets/index-DdAz-CXv.js +0 -10
  233. package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
  234. package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
  235. package/.output/public/assets/installations-CE33wgbY.js +0 -2
  236. package/.output/public/assets/installations-CStZja8x.js +0 -1
  237. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
  238. package/.output/public/assets/link-CI8idAUi.js +0 -1
  239. package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
  240. package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
  241. package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
  242. package/.output/public/assets/routes-DGVXdzpw.js +0 -2
  243. package/.output/public/assets/routes-DTlwOr8I.js +0 -1
  244. package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
  245. package/.output/public/assets/signing-CMf4aqDO.js +0 -1
  246. package/.output/public/assets/styles-BCvbtKsm.css +0 -2
  247. package/.output/public/assets/table-2vcV89QB.js +0 -1
  248. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
  249. package/.output/public/assets/x-DCd0j8c6.js +0 -1
  250. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
  251. package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
  252. package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
  253. package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
  254. package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
  255. package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
  256. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
  257. package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
  258. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  259. package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
  260. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +0 -45
  261. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  262. package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
  263. package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
  264. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
  265. package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
  266. package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
  267. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
  268. package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
  269. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
  270. package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
  271. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  272. package/src/lib/bundle-signing-api.ts +0 -14
  273. package/src/lib/bundle-signing-rpc.ts +0 -16
  274. package/src/lib/server/bundle-signing.server.ts +0 -23
  275. package/src/routes/signing.tsx +0 -37
@@ -0,0 +1,4205 @@
1
+ import { C as isUUIDv7, D as parseReleaseCatalogScopeKey, E as normalizeRolloutCohortCount, T as normalizeCohortValue, a as RELEASE_CATALOG_FALLBACK_POLICY, b as getRolledOutNumericCohorts, c as encodeChannelKey, f as getManifestFileHash, h as getNumericCohortValue, l as getAssetBaseStorageUri, n as MAX_COMPILED_CATALOG_BYTES, o as createReleaseCatalogScopeKey, p as getManifestStorageUri, r as NIL_UUID, s as decodeChannelKey, u as getBundlePatch, x as isCustomCohort } from "./dist-B-r4u3Lg.mjs";
2
+ import { a as bundleToRow, c as rowToBundle, i as bundleToPatchRows, r as DatabaseRowReferencedError, s as releaseRowToRelease, t as DatabaseBundleNotFoundError } from "./databaseRows-CXxf_VIB.mjs";
3
+ import { t as ReleaseManagementError } from "./releaseManagement-B3e7I8J9.mjs";
4
+ import { n as createUUIDv7After, t as createUUIDv7 } from "./uuidv7-BzSO-fr-.mjs";
5
+ import { n as rangesIntersect, t as normalizeRange } from "../_libs/verkit.mjs";
6
+ //#region node_modules/.nitro/vite/services/ssr/assets/api-C0G9MTMI.js
7
+ var SHA256_FILE_HASH_RE = /^[0-9a-f]{64}$/;
8
+ var isContentAddressedAssetFileHash = (value) => {
9
+ return typeof value === "string" && SHA256_FILE_HASH_RE.test(value);
10
+ };
11
+ var getContentAddressedAssetStoragePath = ({ assetPath, fileHash }) => {
12
+ const extension = assetPath.endsWith(".br") ? ".br" : assetPath.includes(".") ? `.${assetPath.split(".").pop()}` : "";
13
+ return `sha256/${fileHash.slice(0, 2)}/${fileHash}${extension}`;
14
+ };
15
+ var normalizeStorageKeyParts = (values) => {
16
+ if (values.some((value) => value.includes("\\"))) throw new Error("Storage keys must use '/' as the hierarchy separator.");
17
+ const segments = values.flatMap((value) => value.split("/").filter(Boolean));
18
+ if (segments.some((segment) => segment === "." || segment === "..")) throw new Error("Storage key segments must not be '.' or '..'.");
19
+ return segments;
20
+ };
21
+ var createStorageKeyBuilder = (basePath) => {
22
+ const baseSegments = normalizeStorageKeyParts([basePath ?? ""]);
23
+ return (...args) => [...baseSegments, ...normalizeStorageKeyParts(args)].join("/");
24
+ };
25
+ var PROTOCOL_PATTERN = /^[a-z][a-z\d+.-]*$/;
26
+ var INVALID_BUCKET_CHARACTER_PATTERN = /[\s/?#%@:\\]/;
27
+ var assertProtocol = (protocol) => {
28
+ if (!PROTOCOL_PATTERN.test(protocol)) throw new Error(`Invalid storage URI protocol: ${protocol}`);
29
+ };
30
+ var assertBucket = (bucket) => {
31
+ if (bucket.length === 0 || INVALID_BUCKET_CHARACTER_PATTERN.test(bucket)) throw new Error(`Invalid storage URI bucket: ${bucket}`);
32
+ };
33
+ var getKeySegments = (key) => {
34
+ if (key.length === 0) throw new Error("Invalid storage URI key: key must not be empty");
35
+ const segments = key.split("/");
36
+ if (segments.some((segment) => segment.length === 0 || segment === "." || segment === ".." || segment.includes("\\"))) throw new Error("Invalid storage URI key: segments must not be empty, '.', '..' or contain '\\'");
37
+ return segments;
38
+ };
39
+ /**
40
+ * Creates a hierarchical storage identifier in the form
41
+ * `protocol://bucket/slash-separated-encoded-key`.
42
+ */
43
+ var createStorageUri = ({ protocol, bucket, key }) => {
44
+ assertProtocol(protocol);
45
+ assertBucket(bucket);
46
+ return `${protocol}://${bucket}/${getKeySegments(key).map(encodeURIComponent).join("/")}`;
47
+ };
48
+ /**
49
+ * Parses a hierarchical storage identifier and decodes each key segment.
50
+ *
51
+ * Search parameters and fragments are not part of a storage identifier.
52
+ */
53
+ function parseStorageUri(storageUri, expectedProtocol) {
54
+ assertProtocol(expectedProtocol);
55
+ if (storageUri.includes("?") || storageUri.includes("#")) throw new Error(`Invalid storage URI format: ${storageUri}`);
56
+ const match = /^([a-z][a-z\d+.-]*):\/\/([^/]+)\/(.*)$/.exec(storageUri);
57
+ if (!match) throw new Error(`Invalid storage URI format: ${storageUri}`);
58
+ const [, protocol, bucket, encodedKey] = match;
59
+ if (protocol !== expectedProtocol) throw new Error(`Invalid storage URI protocol. Expected ${expectedProtocol}, got ${protocol}`);
60
+ assertBucket(bucket);
61
+ let segments;
62
+ try {
63
+ segments = getKeySegments(encodedKey).map((segment) => {
64
+ const decoded = decodeURIComponent(segment);
65
+ if (decoded.includes("/") || decoded.includes("\\")) throw new Error("encoded hierarchy separator");
66
+ if (encodeURIComponent(decoded) !== segment) throw new Error("non-canonical segment");
67
+ return decoded;
68
+ });
69
+ } catch {
70
+ throw new Error(`Invalid storage URI format: ${storageUri}`);
71
+ }
72
+ return {
73
+ protocol,
74
+ bucket,
75
+ key: segments.join("/")
76
+ };
77
+ }
78
+ var isBrotliManifestAssetPath = (assetPath) => /(^|\/)index\.[^/]+\.bundle$/.test(assetPath.replace(/\\/g, "/"));
79
+ var getManifestAssetDownloadPath = (assetPath) => isBrotliManifestAssetPath(assetPath) ? `${assetPath}.br` : assetPath;
80
+ var createStorageUriWithRelativePath = ({ baseStorageUri, relativePath }) => {
81
+ const parsed = parseStorageUri(baseStorageUri, new URL(baseStorageUri).protocol.replace(":", ""));
82
+ const childKey = createStorageKeyBuilder(void 0)(relativePath);
83
+ if (!childKey) throw new Error("Relative storage key must not be empty.");
84
+ return createStorageUri({
85
+ protocol: parsed.protocol,
86
+ bucket: parsed.bucket,
87
+ key: createStorageKeyBuilder(parsed.key)(childKey)
88
+ });
89
+ };
90
+ var replaceStorageUriKeySuffix = ({ storageUri, keySuffix, replacement }) => {
91
+ const parsed = parseStorageUri(storageUri, new URL(storageUri).protocol.replace(":", ""));
92
+ const normalizedSuffix = createStorageKeyBuilder(void 0)(keySuffix);
93
+ const normalizedReplacement = createStorageKeyBuilder(void 0)(replacement);
94
+ if (!normalizedSuffix || !normalizedReplacement) throw new Error("Storage URI key suffix and replacement must not be empty.");
95
+ const keySegments = parsed.key.split("/");
96
+ const suffixSegments = normalizedSuffix.split("/");
97
+ const suffixOffset = keySegments.length - suffixSegments.length;
98
+ if (suffixOffset < 0 || suffixSegments.some((segment, index) => keySegments[suffixOffset + index] !== segment)) throw new Error("Storage URI key does not end with the expected suffix.");
99
+ const prefix = keySegments.slice(0, suffixOffset).join("/");
100
+ return createStorageUri({
101
+ protocol: parsed.protocol,
102
+ bucket: parsed.bucket,
103
+ key: createStorageKeyBuilder(prefix)(normalizedReplacement)
104
+ });
105
+ };
106
+ var resolveManifestAssetStorageFileHash = ({ downloadFileHash, fileHash }) => isContentAddressedAssetFileHash(downloadFileHash) ? downloadFileHash : fileHash;
107
+ var getManifestAssetStoragePath = ({ assetPath, downloadFileHash, fileHash }) => getContentAddressedAssetStoragePath({
108
+ assetPath,
109
+ fileHash: resolveManifestAssetStorageFileHash({
110
+ downloadFileHash,
111
+ fileHash
112
+ })
113
+ });
114
+ var resolveManifestAssetStorageUri = ({ assetBaseStorageUri, assetPath, downloadFileHash, fileHash }) => createStorageUriWithRelativePath({
115
+ baseStorageUri: assetBaseStorageUri,
116
+ relativePath: getManifestAssetStoragePath({
117
+ assetPath,
118
+ downloadFileHash,
119
+ fileHash
120
+ })
121
+ });
122
+ var BUNDLE_STORAGE_PREFIX = "bundles";
123
+ var createBundleStorageKey = (bundleId, ...relativePaths) => [
124
+ BUNDLE_STORAGE_PREFIX,
125
+ bundleId,
126
+ ...relativePaths
127
+ ].filter(Boolean).join("/");
128
+ var getBundleArchiveStorageUri = ({ manifestStorageUri, bundleId }) => replaceStorageUriKeySuffix({
129
+ storageUri: manifestStorageUri,
130
+ keySuffix: createBundleStorageKey(bundleId, "manifest.json"),
131
+ replacement: createBundleStorageKey(bundleId, "bundle.tar.br")
132
+ });
133
+ var LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
134
+ var NUMERIC_IDENTIFIER = String.raw`0|[1-9]\d*`;
135
+ var NUMERIC_IDENTIFIER_LOOSE = String.raw`\d+`;
136
+ var NON_NUMERIC_IDENTIFIER = String.raw`\d*[a-zA-Z-]${LETTER_DASH_NUMBER}*`;
137
+ var MAIN_VERSION = String.raw`(${NUMERIC_IDENTIFIER})\.(${NUMERIC_IDENTIFIER})\.(${NUMERIC_IDENTIFIER})`;
138
+ var MAIN_VERSION_LOOSE = String.raw`(${NUMERIC_IDENTIFIER_LOOSE})\.(${NUMERIC_IDENTIFIER_LOOSE})\.(${NUMERIC_IDENTIFIER_LOOSE})`;
139
+ var PRERELEASE_IDENTIFIER = `(?:${NON_NUMERIC_IDENTIFIER}|${NUMERIC_IDENTIFIER})`;
140
+ var PRERELEASE_IDENTIFIER_LOOSE = `(?:${NON_NUMERIC_IDENTIFIER}|${NUMERIC_IDENTIFIER_LOOSE})`;
141
+ var PRERELEASE = String.raw`(?:-(${PRERELEASE_IDENTIFIER}(?:\.${PRERELEASE_IDENTIFIER})*))`;
142
+ var PRERELEASE_LOOSE = String.raw`(?:-?(${PRERELEASE_IDENTIFIER_LOOSE}(?:\.${PRERELEASE_IDENTIFIER_LOOSE})*))`;
143
+ var BUILD_IDENTIFIER = `${LETTER_DASH_NUMBER}+`;
144
+ var BUILD = String.raw`(?:\+(${BUILD_IDENTIFIER}(?:\.${BUILD_IDENTIFIER})*))`;
145
+ var FULL_PLAIN = `v?${MAIN_VERSION}${PRERELEASE}?${BUILD}?`;
146
+ var LOOSE_PLAIN = String.raw`[v=\s]*${MAIN_VERSION_LOOSE}${PRERELEASE_LOOSE}?${BUILD}?`;
147
+ var GREATER_LESS_THAN = "((?:<|>)?=?)";
148
+ var XRANGE_IDENTIFIER = String.raw`${NUMERIC_IDENTIFIER}|x|X|\*`;
149
+ var XRANGE_IDENTIFIER_LOOSE = String.raw`${NUMERIC_IDENTIFIER_LOOSE}|x|X|\*`;
150
+ var XRANGE_PLAIN = String.raw`[v=\s]*(${XRANGE_IDENTIFIER})(?:\.(${XRANGE_IDENTIFIER})(?:\.(${XRANGE_IDENTIFIER})(?:${PRERELEASE})?${BUILD}?)?)?`;
151
+ var XRANGE_PLAIN_LOOSE = String.raw`[v=\s]*(${XRANGE_IDENTIFIER_LOOSE})(?:\.(${XRANGE_IDENTIFIER_LOOSE})(?:\.(${XRANGE_IDENTIFIER_LOOSE})(?:${PRERELEASE_LOOSE})?${BUILD}?)?)?`;
152
+ var LONE_TILDE = "(?:~>?)";
153
+ var LONE_CARET = String.raw`(?:\^)`;
154
+ var COERCE_PLAIN = String.raw`(^|[^\d])(\d{1,${16}})(?:\.(\d{1,${16}}))?(?:\.(\d{1,${16}}))?`;
155
+ var COERCE = String.raw`${COERCE_PLAIN}(?:$|[^\d])`;
156
+ var COERCE_FULL = String.raw`${COERCE_PLAIN}(?:${PRERELEASE})?(?:${BUILD})?(?:$|[^\d])`;
157
+ function makeSafeRegexSource(source) {
158
+ const replacements = [
159
+ [String.raw`\s`, 1],
160
+ [String.raw`\d`, 256],
161
+ [LETTER_DASH_NUMBER, 250]
162
+ ];
163
+ for (const [token, maximum] of replacements) source = source.split(`${token}*`).join(`${token}{0,${maximum}}`).split(`${token}+`).join(`${token}{1,${maximum}}`);
164
+ return source;
165
+ }
166
+ function safeRegex(source, flags) {
167
+ return new RegExp(makeSafeRegexSource(source), flags);
168
+ }
169
+ var FULL = safeRegex(`^${FULL_PLAIN}$`);
170
+ var LOOSE = safeRegex(`^${LOOSE_PLAIN}$`);
171
+ var NUMERIC$1 = /^\d+$/;
172
+ function formatComparableVersion(version) {
173
+ const base = `${version.major}.${version.minor}.${version.patch}`;
174
+ return version.prerelease?.length ? `${base}-${version.prerelease.join(".")}` : base;
175
+ }
176
+ function formatFullVersion(version) {
177
+ const comparable = formatComparableVersion(version);
178
+ return version.build?.length ? `${comparable}+${version.build.join(".")}` : comparable;
179
+ }
180
+ function parse(version, options = {}) {
181
+ if (typeof version !== "string") return version;
182
+ if (version.length > 256) throw new TypeError(`Version exceeds the maximum length of 256 characters`);
183
+ const match = version.trim().match(options.loose ? LOOSE : FULL);
184
+ if (!match) throw new TypeError(`Invalid version syntax: ${version}`);
185
+ const major = Number(match[1]);
186
+ const minor = Number(match[2]);
187
+ const patch = Number(match[3]);
188
+ if (major > Number.MAX_SAFE_INTEGER || major < 0) throw new TypeError(`Invalid major version: ${match[1]}`);
189
+ if (minor > Number.MAX_SAFE_INTEGER || minor < 0) throw new TypeError(`Invalid minor version: ${match[2]}`);
190
+ if (patch > Number.MAX_SAFE_INTEGER || patch < 0) throw new TypeError(`Invalid patch version: ${match[3]}`);
191
+ const prerelease = match[4] ? match[4].split(".").map((identifier) => {
192
+ if (NUMERIC$1.test(identifier)) {
193
+ const numeric = Number(identifier);
194
+ if (numeric >= 0 && numeric < Number.MAX_SAFE_INTEGER) return numeric;
195
+ }
196
+ return identifier;
197
+ }) : void 0;
198
+ return {
199
+ build: match[5]?.split("."),
200
+ major,
201
+ minor,
202
+ patch,
203
+ prerelease
204
+ };
205
+ }
206
+ function tryParse(version, options = {}) {
207
+ try {
208
+ return parse(version, options);
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ var NUMERIC = /^\d+$/;
214
+ function compareIdentifiers(left, right) {
215
+ if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
216
+ const leftNumeric = NUMERIC.test(String(left));
217
+ const rightNumeric = NUMERIC.test(String(right));
218
+ const normalizedLeft = leftNumeric ? Number(left) : left;
219
+ const normalizedRight = rightNumeric ? Number(right) : right;
220
+ return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
221
+ }
222
+ function compareMainParsed(left, right) {
223
+ return left.major === right.major ? left.minor === right.minor ? left.patch === right.patch ? 0 : left.patch < right.patch ? -1 : 1 : left.minor < right.minor ? -1 : 1 : left.major < right.major ? -1 : 1;
224
+ }
225
+ function comparePrereleaseParsed(left, right) {
226
+ const leftPrerelease = left.prerelease;
227
+ const rightPrerelease = right.prerelease;
228
+ if (leftPrerelease?.length && !rightPrerelease?.length) return -1;
229
+ if (!leftPrerelease?.length && rightPrerelease?.length) return 1;
230
+ if (!leftPrerelease?.length && !rightPrerelease?.length) return 0;
231
+ for (let index = 0;; index++) {
232
+ const leftIdentifier = leftPrerelease?.[index];
233
+ const rightIdentifier = rightPrerelease?.[index];
234
+ if (leftIdentifier === void 0 && rightIdentifier === void 0) return 0;
235
+ if (rightIdentifier === void 0) return 1;
236
+ if (leftIdentifier === void 0) return -1;
237
+ if (leftIdentifier !== rightIdentifier) return compareIdentifiers(leftIdentifier, rightIdentifier);
238
+ }
239
+ }
240
+ function compareParsed(left, right) {
241
+ return compareMainParsed(left, right) || comparePrereleaseParsed(left, right);
242
+ }
243
+ function compare(left, right, options = {}) {
244
+ return compareParsed(parse(left, options), parse(right, options));
245
+ }
246
+ var STRICT_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${FULL_PLAIN})$|^$`);
247
+ var LOOSE_COMPARATOR$1 = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
248
+ function parseComparator(comparator, options = {}) {
249
+ if (typeof comparator !== "string") return comparator;
250
+ const normalized = comparator.trim().replaceAll(/\s+/g, " ");
251
+ const match = normalized.match(options.loose ? LOOSE_COMPARATOR$1 : STRICT_COMPARATOR);
252
+ if (!match) throw new TypeError(`Invalid comparator: ${normalized}`);
253
+ const operator = match[1] === "=" ? "" : match[1] || "";
254
+ const version = match[2] ? parse(match[2], options) : null;
255
+ return {
256
+ operator,
257
+ options,
258
+ value: version ? `${operator}${formatComparableVersion(version)}` : "",
259
+ version
260
+ };
261
+ }
262
+ var BUILD_STRIP = new RegExp(BUILD, "g");
263
+ var BUILD_SAFE = safeRegex(BUILD);
264
+ var STRICT_HYPHEN = safeRegex(String.raw`^\s*(${XRANGE_PLAIN})\s+-\s+(${XRANGE_PLAIN})\s*$`);
265
+ var LOOSE_HYPHEN = safeRegex(String.raw`^\s*(${XRANGE_PLAIN_LOOSE})\s+-\s+(${XRANGE_PLAIN_LOOSE})\s*$`);
266
+ var COMPARATOR_TRIM = safeRegex(String.raw`(\s*)${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN}|${XRANGE_PLAIN})`, "g");
267
+ var TILDE_TRIM = safeRegex(String.raw`(\s*)${LONE_TILDE}\s+`, "g");
268
+ var CARET_TRIM = safeRegex(String.raw`(\s*)${LONE_CARET}\s+`, "g");
269
+ var STRICT_TILDE = safeRegex(`^${LONE_TILDE}${XRANGE_PLAIN}$`);
270
+ var LOOSE_TILDE = safeRegex(`^${LONE_TILDE}${XRANGE_PLAIN_LOOSE}$`);
271
+ var STRICT_CARET = safeRegex(`^${LONE_CARET}${XRANGE_PLAIN}$`);
272
+ var LOOSE_CARET = safeRegex(`^${LONE_CARET}${XRANGE_PLAIN_LOOSE}$`);
273
+ var STRICT_XRANGE = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*${XRANGE_PLAIN}$`);
274
+ var LOOSE_XRANGE = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*${XRANGE_PLAIN_LOOSE}$`);
275
+ var STAR = safeRegex(String.raw`(<|>)?=?\s*\*`);
276
+ var GTE_ZERO = /^\s*>=\s*0\.0\.0\s*$/;
277
+ var GTE_ZERO_PRERELEASE = /^\s*>=\s*0\.0\.0-0\s*$/;
278
+ var LOOSE_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
279
+ function isWildcard(value) {
280
+ return !value || String(value).toLowerCase() === "x" || String(value) === "*";
281
+ }
282
+ function hasInvalidWildcardOrder(major, minor, patch) {
283
+ return isWildcard(major) && !isWildcard(minor) || isWildcard(minor) && Boolean(patch) && !isWildcard(patch);
284
+ }
285
+ function replaceTilde(comparator, options) {
286
+ const expression = options.loose ? LOOSE_TILDE : STRICT_TILDE;
287
+ const lowerPrerelease = options.includePrerelease ? "-0" : "";
288
+ return comparator.replace(expression, (_match, major, minor, patch, prerelease) => {
289
+ if (isWildcard(major)) return "";
290
+ if (isWildcard(minor)) return `>=${major}.0.0${lowerPrerelease} <${Number(major) + 1}.0.0-0`;
291
+ if (isWildcard(patch)) return `>=${major}.${minor}.0${lowerPrerelease} <${major}.${Number(minor) + 1}.0-0`;
292
+ return prerelease ? `>=${major}.${minor}.${patch}-${prerelease} <${major}.${Number(minor) + 1}.0-0` : `>=${major}.${minor}.${patch} <${major}.${Number(minor) + 1}.0-0`;
293
+ });
294
+ }
295
+ function replaceTildes(comparator, options) {
296
+ return comparator.trim().split(/\s+/).map((part) => replaceTilde(part, options)).join(" ");
297
+ }
298
+ function replaceCaret(comparator, options) {
299
+ const expression = options.loose ? LOOSE_CARET : STRICT_CARET;
300
+ const lowerPrerelease = options.includePrerelease ? "-0" : "";
301
+ return comparator.replace(expression, (_match, major, minor, patch, prerelease) => {
302
+ if (isWildcard(major)) return "";
303
+ if (isWildcard(minor)) return `>=${major}.0.0${lowerPrerelease} <${Number(major) + 1}.0.0-0`;
304
+ if (isWildcard(patch)) return major === "0" ? `>=${major}.${minor}.0${lowerPrerelease} <${major}.${Number(minor) + 1}.0-0` : `>=${major}.${minor}.0${lowerPrerelease} <${Number(major) + 1}.0.0-0`;
305
+ if (prerelease) return major === "0" ? minor === "0" ? `>=${major}.${minor}.${patch}-${prerelease} <${major}.${minor}.${Number(patch) + 1}-0` : `>=${major}.${minor}.${patch}-${prerelease} <${major}.${Number(minor) + 1}.0-0` : `>=${major}.${minor}.${patch}-${prerelease} <${Number(major) + 1}.0.0-0`;
306
+ return major === "0" ? minor === "0" ? `>=${major}.${minor}.${patch} <${major}.${minor}.${Number(patch) + 1}-0` : `>=${major}.${minor}.${patch} <${major}.${Number(minor) + 1}.0-0` : `>=${major}.${minor}.${patch} <${Number(major) + 1}.0.0-0`;
307
+ });
308
+ }
309
+ function replaceCarets(comparator, options) {
310
+ return comparator.trim().split(/\s+/).map((part) => replaceCaret(part, options)).join(" ");
311
+ }
312
+ function replaceXRange(comparator, options) {
313
+ const expression = options.loose ? LOOSE_XRANGE : STRICT_XRANGE;
314
+ return comparator.trim().replace(expression, (match, rawOperator, rawMajor, rawMinor, rawPatch) => {
315
+ let operator = rawOperator;
316
+ let major = rawMajor;
317
+ let minor = rawMinor;
318
+ let patch = rawPatch;
319
+ if (hasInvalidWildcardOrder(String(major), minor === void 0 ? void 0 : String(minor), patch === void 0 ? void 0 : String(patch))) return comparator;
320
+ const wildcardMajor = isWildcard(major);
321
+ const wildcardMinor = wildcardMajor || isWildcard(minor);
322
+ const wildcardPatch = wildcardMinor || isWildcard(patch);
323
+ if (operator === "=" && wildcardPatch) operator = "";
324
+ if (wildcardMajor) return operator === ">" || operator === "<" ? "<0.0.0-0" : "*";
325
+ let prerelease = options.includePrerelease ? "-0" : "";
326
+ if (operator && wildcardPatch) {
327
+ if (wildcardMinor) minor = 0;
328
+ patch = 0;
329
+ if (operator === ">") {
330
+ operator = ">=";
331
+ if (wildcardMinor) {
332
+ major = Number(major) + 1;
333
+ minor = 0;
334
+ } else minor = Number(minor) + 1;
335
+ } else if (operator === "<=") {
336
+ operator = "<";
337
+ if (wildcardMinor) major = Number(major) + 1;
338
+ else minor = Number(minor) + 1;
339
+ }
340
+ if (operator === "<") prerelease = "-0";
341
+ return `${operator}${major}.${minor}.${patch}${prerelease}`;
342
+ }
343
+ if (wildcardMinor) return `>=${major}.0.0${prerelease} <${Number(major) + 1}.0.0-0`;
344
+ if (wildcardPatch) return `>=${major}.${minor}.0${prerelease} <${major}.${Number(minor) + 1}.0-0`;
345
+ return match;
346
+ });
347
+ }
348
+ function replaceXRanges(comparator, options) {
349
+ return comparator.split(/\s+/).map((part) => replaceXRange(part, options)).join(" ");
350
+ }
351
+ function replaceHyphenRange(range, options) {
352
+ const expression = options.loose ? LOOSE_HYPHEN : STRICT_HYPHEN;
353
+ return range.replace(expression, (_match, rawFrom, fromMajor, fromMinor, fromPatch, fromPrerelease, _fromBuild, rawTo, toMajor, toMinor, toPatch, toPrerelease) => {
354
+ let from = rawFrom;
355
+ let to = rawTo;
356
+ if (isWildcard(fromMajor)) from = "";
357
+ else if (isWildcard(fromMinor)) from = `>=${fromMajor}.0.0${options.includePrerelease ? "-0" : ""}`;
358
+ else if (isWildcard(fromPatch)) from = `>=${fromMajor}.${fromMinor}.0${options.includePrerelease ? "-0" : ""}`;
359
+ else if (fromPrerelease) from = `>=${from}`;
360
+ else from = `>=${from}${options.includePrerelease ? "-0" : ""}`;
361
+ if (isWildcard(toMajor)) to = "";
362
+ else if (isWildcard(toMinor)) to = `<${Number(toMajor) + 1}.0.0-0`;
363
+ else if (isWildcard(toPatch)) to = `<${toMajor}.${Number(toMinor) + 1}.0-0`;
364
+ else if (toPrerelease) to = `<=${toMajor}.${toMinor}.${toPatch}-${toPrerelease}`;
365
+ else if (options.includePrerelease) to = `<${toMajor}.${toMinor}.${Number(toPatch) + 1}-0`;
366
+ else to = `<=${to}`;
367
+ return `${from} ${to}`.trim();
368
+ });
369
+ }
370
+ function expandComparator(comparator, options) {
371
+ return replaceXRanges(replaceTildes(replaceCarets(comparator.replace(BUILD_SAFE, ""), options), options), options).trim().replace(STAR, "");
372
+ }
373
+ function parseSimpleRange(input, options) {
374
+ let parts = replaceHyphenRange(input.replace(BUILD_STRIP, ""), options).replace(COMPARATOR_TRIM, "$1$2$3").replace(TILDE_TRIM, "$1~").replace(CARET_TRIM, "$1^").split(" ").map((part) => expandComparator(part, options)).join(" ").split(/\s+/).map((part) => part.trim().replace(options.includePrerelease ? GTE_ZERO_PRERELEASE : GTE_ZERO, ""));
375
+ if (options.loose) parts = parts.filter((part) => LOOSE_COMPARATOR.test(part));
376
+ const unique = /* @__PURE__ */ new Map();
377
+ for (const comparator of parts.map((part) => parseComparator(part, options))) {
378
+ if (comparator.value === "<0.0.0-0") return [comparator];
379
+ unique.set(comparator.value, comparator);
380
+ }
381
+ if (unique.size > 1) unique.delete("");
382
+ return [...unique.values()];
383
+ }
384
+ function parseRange(range, options = {}) {
385
+ if (typeof range !== "string") return range;
386
+ const parsedOptions = { ...options };
387
+ const normalizedRange = range.trim().replaceAll(/\s+/g, " ");
388
+ let sets = normalizedRange.split("||").map((part) => parseSimpleRange(part.trim(), parsedOptions)).filter((set) => set.length);
389
+ if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${normalizedRange}`);
390
+ if (sets.length > 1) {
391
+ const first = sets[0];
392
+ sets = sets.filter((set) => set[0]?.value !== "<0.0.0-0");
393
+ if (!sets.length) sets = [first];
394
+ else if (sets.length > 1) {
395
+ const any = sets.find((set) => set.length === 1 && set[0]?.value === "");
396
+ if (any) sets = [any];
397
+ }
398
+ }
399
+ return {
400
+ options: parsedOptions,
401
+ sets
402
+ };
403
+ }
404
+ var COERCE_EXACT = safeRegex(COERCE);
405
+ var COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
406
+ safeRegex(`^${PRERELEASE}$`);
407
+ safeRegex(`^${PRERELEASE_LOOSE}$`);
408
+ function normalizeFull(version, options = {}) {
409
+ const parsed = tryParse(version, options);
410
+ return parsed ? formatFullVersion(parsed) : null;
411
+ }
412
+ function normalize(version, options = {}) {
413
+ const parsed = tryParse(version, options);
414
+ return parsed ? formatComparableVersion(parsed) : null;
415
+ }
416
+ function coerce(value, options = {}) {
417
+ if (typeof value === "object") return value;
418
+ const input = typeof value === "number" ? String(value) : value;
419
+ let match = null;
420
+ if (options.rtl) {
421
+ const expression = safeRegex(options.includePrerelease ? COERCE_FULL : COERCE, "g");
422
+ let next;
423
+ while ((next = expression.exec(input)) && (!match || match.index + match[0].length !== input.length)) {
424
+ if (!match || next.index + next[0].length !== match.index + match[0].length) match = next;
425
+ expression.lastIndex = next.index + next[1].length + next[2].length;
426
+ }
427
+ } else match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
428
+ if (!match) return null;
429
+ const major = match[2];
430
+ return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
431
+ }
432
+ var ReleaseCatalogCompilationError = class extends Error {
433
+ code;
434
+ constructor(code, message) {
435
+ super(message);
436
+ this.name = "ReleaseCatalogCompilationError";
437
+ this.code = code;
438
+ }
439
+ };
440
+ function compareBounds(left, right) {
441
+ return compare(left, right);
442
+ }
443
+ function strongerLower(current, candidate) {
444
+ if (current === null) return candidate;
445
+ const compared = compareBounds(candidate.version, current.version);
446
+ if (compared > 0) return candidate;
447
+ if (compared < 0) return current;
448
+ return {
449
+ version: current.version,
450
+ inclusive: current.inclusive && candidate.inclusive
451
+ };
452
+ }
453
+ function strongerUpper(current, candidate) {
454
+ if (current === null) return candidate;
455
+ const compared = compareBounds(candidate.version, current.version);
456
+ if (compared < 0) return candidate;
457
+ if (compared > 0) return current;
458
+ return {
459
+ version: current.version,
460
+ inclusive: current.inclusive && candidate.inclusive
461
+ };
462
+ }
463
+ function comparatorVersion(comparator) {
464
+ const version = comparator.version && normalize(comparator.version);
465
+ if (version === null) throw new ReleaseCatalogCompilationError("INVALID_TARGET_RANGE", `Invalid comparator: ${comparator.value}`);
466
+ return version;
467
+ }
468
+ function comparatorSetToInterval(comparators) {
469
+ let lower = null;
470
+ let upper = null;
471
+ for (const comparator of comparators) {
472
+ if (comparator.version === null) continue;
473
+ const version = comparatorVersion(comparator);
474
+ if (comparator.operator === "") {
475
+ lower = strongerLower(lower, {
476
+ inclusive: true,
477
+ version
478
+ });
479
+ upper = strongerUpper(upper, {
480
+ inclusive: true,
481
+ version
482
+ });
483
+ } else if (comparator.operator === ">") lower = strongerLower(lower, {
484
+ inclusive: false,
485
+ version
486
+ });
487
+ else if (comparator.operator === ">=") lower = strongerLower(lower, {
488
+ inclusive: true,
489
+ version
490
+ });
491
+ else if (comparator.operator === "<") upper = strongerUpper(upper, {
492
+ inclusive: false,
493
+ version
494
+ });
495
+ else if (comparator.operator === "<=") upper = strongerUpper(upper, {
496
+ inclusive: true,
497
+ version
498
+ });
499
+ }
500
+ if (lower !== null && upper !== null) {
501
+ const compared = compareBounds(lower.version, upper.version);
502
+ if (compared > 0 || compared === 0 && (!lower.inclusive || !upper.inclusive)) return null;
503
+ }
504
+ return {
505
+ lower,
506
+ upper
507
+ };
508
+ }
509
+ function rangeToIntervals(range) {
510
+ try {
511
+ return parseRange(range).sets.flatMap((comparators) => {
512
+ const interval = comparatorSetToInterval(comparators);
513
+ return interval === null ? [] : [interval];
514
+ });
515
+ } catch {
516
+ throw new ReleaseCatalogCompilationError("INVALID_TARGET_RANGE", `Invalid target app-version range: ${range}`);
517
+ }
518
+ }
519
+ function releaseDescriptor(release) {
520
+ return {
521
+ releaseId: release.id,
522
+ kind: release.kind,
523
+ bundleId: release.bundleId,
524
+ rolloutCohortCount: release.rolloutCohortCount,
525
+ targetCohorts: [...new Set(release.targetCohorts)].sort(),
526
+ shouldForceUpdate: release.shouldForceUpdate,
527
+ message: release.message
528
+ };
529
+ }
530
+ function validateReleases(releases, strategy) {
531
+ const enabled = releases.filter((release) => release.enabled).sort((left, right) => right.id.localeCompare(left.id));
532
+ const seenIds = /* @__PURE__ */ new Set();
533
+ for (const release of enabled) {
534
+ if (!isUUIDv7(release.id)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} must use a canonical lowercase UUIDv7 ID`);
535
+ if (seenIds.has(release.id)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Duplicate Release ID: ${release.id}`);
536
+ seenIds.add(release.id);
537
+ if (release.strategy !== strategy) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has the wrong strategy`);
538
+ if (release.kind === "BUNDLE" && release.bundleId === null || release.kind === "EMBEDDED" && release.bundleId !== null) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has an invalid kind/Bundle combination`);
539
+ if (release.targetCohorts.length > 100) throw new ReleaseCatalogCompilationError("TARGET_COHORT_LIMIT", `Release ${release.id} exceeds 100 target cohorts`);
540
+ if (strategy === "APP_VERSION" && (release.targetAppVersion === null || release.fingerprintHash !== null)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has an invalid app-version target`);
541
+ if (strategy === "FINGERPRINT" && (release.fingerprintHash === null || release.targetAppVersion !== null)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has an invalid fingerprint target`);
542
+ }
543
+ return enabled.filter((release) => release.kind === "BUNDLE");
544
+ }
545
+ function collectFrontierReleaseIds(releases) {
546
+ if (releases.every((release) => normalizeRolloutCohortCount(release.rolloutCohortCount) >= 1e3)) {
547
+ const retained = /* @__PURE__ */ new Set();
548
+ const bundleIds = /* @__PURE__ */ new Set();
549
+ let embedded = false;
550
+ for (const release of releases) {
551
+ if (release.kind === "EMBEDDED") {
552
+ if (!embedded) {
553
+ embedded = true;
554
+ retained.add(release.id);
555
+ }
556
+ } else if (release.bundleId !== null && bundleIds.size < 11 && !bundleIds.has(release.bundleId)) {
557
+ bundleIds.add(release.bundleId);
558
+ retained.add(release.id);
559
+ }
560
+ if (embedded && bundleIds.size >= 11) break;
561
+ }
562
+ return retained;
563
+ }
564
+ const explicitTargetCohorts = new Set(releases.flatMap((release) => release.targetCohorts));
565
+ const cohortStates = [
566
+ void 0,
567
+ ...Array.from({ length: 1e3 }, (_, index) => String(index + 1)),
568
+ ...[...explicitTargetCohorts].sort(),
569
+ "release-catalog-non-targeted-cohort"
570
+ ].map((cohort) => {
571
+ const normalized = cohort === void 0 ? void 0 : normalizeCohortValue(cohort);
572
+ return {
573
+ isCustom: normalized !== void 0 && isCustomCohort(normalized),
574
+ normalized,
575
+ numeric: normalized === void 0 ? null : getNumericCohortValue(normalized),
576
+ bundleIds: /* @__PURE__ */ new Set(),
577
+ embedded: false,
578
+ complete: false
579
+ };
580
+ });
581
+ const stateIndexesByCohort = /* @__PURE__ */ new Map();
582
+ const fullRolloutStateIndexes = [];
583
+ for (const [index, state] of cohortStates.entries()) {
584
+ if (state.normalized !== void 0) {
585
+ const indexes = stateIndexesByCohort.get(state.normalized) ?? [];
586
+ indexes.push(index);
587
+ stateIndexesByCohort.set(state.normalized, indexes);
588
+ }
589
+ if (state.normalized === void 0 || state.numeric !== null || state.isCustom) fullRolloutStateIndexes.push(index);
590
+ }
591
+ const retained = /* @__PURE__ */ new Set();
592
+ let incompleteStateCount = cohortStates.length;
593
+ for (const release of releases) {
594
+ const rolloutCohortCount = normalizeRolloutCohortCount(release.rolloutCohortCount);
595
+ const eligibleStateIndexes = /* @__PURE__ */ new Set();
596
+ if (rolloutCohortCount >= 1e3) for (const index of fullRolloutStateIndexes) eligibleStateIndexes.add(index);
597
+ else if (rolloutCohortCount > 0) for (const numericCohort of getRolledOutNumericCohorts(release.id, rolloutCohortCount)) for (const index of stateIndexesByCohort.get(String(numericCohort)) ?? []) eligibleStateIndexes.add(index);
598
+ for (const targetCohort of new Set(release.targetCohorts.map(normalizeCohortValue))) for (const index of stateIndexesByCohort.get(targetCohort) ?? []) eligibleStateIndexes.add(index);
599
+ for (const index of eligibleStateIndexes) {
600
+ const state = cohortStates[index];
601
+ if (state.complete) continue;
602
+ if (release.kind === "EMBEDDED") {
603
+ if (!state.embedded) {
604
+ state.embedded = true;
605
+ retained.add(release.id);
606
+ }
607
+ } else if (release.bundleId !== null && state.bundleIds.size < 11 && !state.bundleIds.has(release.bundleId)) {
608
+ state.bundleIds.add(release.bundleId);
609
+ retained.add(release.id);
610
+ }
611
+ if (state.embedded && state.bundleIds.size >= 11) {
612
+ state.complete = true;
613
+ incompleteStateCount -= 1;
614
+ }
615
+ }
616
+ if (incompleteStateCount === 0) break;
617
+ }
618
+ return retained;
619
+ }
620
+ function intervalContainsSegment(interval, segment) {
621
+ if (interval.lower !== null) {
622
+ if (segment.lower === null) return false;
623
+ const compared = compareBounds(segment.lower.version, interval.lower.version);
624
+ if (compared < 0 || compared === 0 && segment.lower.inclusive && !interval.lower.inclusive) return false;
625
+ }
626
+ if (interval.upper !== null) {
627
+ if (segment.upper === null) return false;
628
+ const compared = compareBounds(segment.upper.version, interval.upper.version);
629
+ if (compared > 0 || compared === 0 && segment.upper.inclusive && !interval.upper.inclusive) return false;
630
+ }
631
+ return true;
632
+ }
633
+ function atomicVersionSegments(intervals) {
634
+ const endpoints = [...new Set(intervals.flatMap((interval) => [interval.lower?.version, interval.upper?.version].filter((version) => version !== void 0)))].sort(compareBounds);
635
+ if (endpoints.length === 0) return [{
636
+ lower: null,
637
+ upper: null
638
+ }];
639
+ const segments = [{
640
+ lower: null,
641
+ upper: {
642
+ inclusive: false,
643
+ version: endpoints[0]
644
+ }
645
+ }];
646
+ for (let index = 0; index < endpoints.length; index += 1) {
647
+ const endpoint = endpoints[index];
648
+ segments.push({
649
+ lower: {
650
+ inclusive: true,
651
+ version: endpoint
652
+ },
653
+ upper: {
654
+ inclusive: true,
655
+ version: endpoint
656
+ }
657
+ });
658
+ const next = endpoints[index + 1];
659
+ if (next !== void 0) segments.push({
660
+ lower: {
661
+ inclusive: false,
662
+ version: endpoint
663
+ },
664
+ upper: {
665
+ inclusive: false,
666
+ version: next
667
+ }
668
+ });
669
+ }
670
+ segments.push({
671
+ lower: {
672
+ inclusive: false,
673
+ version: endpoints.at(-1)
674
+ },
675
+ upper: null
676
+ });
677
+ return segments;
678
+ }
679
+ function sameIndexes(left, right) {
680
+ return left.length === right.length && left.every((value, index) => value === right[index]);
681
+ }
682
+ function mergeSegments(segments) {
683
+ const merged = [];
684
+ for (const segment of segments) {
685
+ const previous = merged.at(-1);
686
+ if (previous && sameIndexes(previous.releaseIndexes, segment.releaseIndexes) && sameIndexes(previous.rollbackReleaseIndexes, segment.rollbackReleaseIndexes)) merged[merged.length - 1] = {
687
+ ...previous,
688
+ upper: segment.upper
689
+ };
690
+ else merged.push(segment);
691
+ }
692
+ return merged;
693
+ }
694
+ function compileAppVersion(releases) {
695
+ const ranges = /* @__PURE__ */ new Map();
696
+ for (const release of releases) ranges.set(release.id, rangeToIntervals(release.targetAppVersion));
697
+ const segmentReleases = atomicVersionSegments([...ranges.values()].flat()).map((segment) => {
698
+ const applicable = releases.filter((release) => ranges.get(release.id).some((range) => intervalContainsSegment(range, segment)));
699
+ return {
700
+ rollbackIds: new Set(applicable.map(({ id }) => id)),
701
+ segment,
702
+ retainedIds: collectFrontierReleaseIds(applicable)
703
+ };
704
+ });
705
+ const retainedIds = new Set(segmentReleases.flatMap(({ retainedIds, rollbackIds }) => [...retainedIds, ...rollbackIds]));
706
+ const retainedReleases = releases.filter((release) => retainedIds.has(release.id));
707
+ const descriptorIndex = new Map(retainedReleases.map((release, index) => [release.id, index]));
708
+ return {
709
+ envelope: {
710
+ fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
711
+ schemaVersion: 1,
712
+ segments: mergeSegments(segmentReleases.map(({ segment, retainedIds, rollbackIds }) => ({
713
+ ...segment,
714
+ releaseIndexes: releases.filter((release) => retainedIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
715
+ rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
716
+ }))).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0),
717
+ strategy: "APP_VERSION"
718
+ },
719
+ retainedReleases
720
+ };
721
+ }
722
+ function compileFingerprint(releases) {
723
+ const retainedIds = collectFrontierReleaseIds(releases);
724
+ const retainedReleases = releases;
725
+ return {
726
+ envelope: {
727
+ fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
728
+ releaseIndexes: retainedReleases.map((release, index) => retainedIds.has(release.id) ? index : -1).filter((index) => index >= 0),
729
+ rollbackReleaseIndexes: retainedReleases.map((_, index) => index),
730
+ schemaVersion: 1,
731
+ strategy: "FINGERPRINT"
732
+ },
733
+ retainedReleases
734
+ };
735
+ }
736
+ async function sha256(value) {
737
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
738
+ return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
739
+ }
740
+ function canonicalStringify(value) {
741
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
742
+ if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(",")}]`;
743
+ const record = value;
744
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`).join(",")}}`;
745
+ }
746
+ async function compileReleaseCatalog(input) {
747
+ const releases = validateReleases(input.releases, input.strategy);
748
+ const targetCohorts = new Set(releases.flatMap((release) => release.targetCohorts));
749
+ if (targetCohorts.size > 512) throw new ReleaseCatalogCompilationError("DISTINCT_TARGET_COHORT_LIMIT", `Scope exceeds 512 distinct target cohorts`);
750
+ const { envelope, retainedReleases } = input.strategy === "APP_VERSION" ? compileAppVersion(releases) : compileFingerprint(releases);
751
+ const payload = {
752
+ ...envelope,
753
+ releaseDescriptors: retainedReleases.map(releaseDescriptor)
754
+ };
755
+ const canonicalPayload = canonicalStringify(payload);
756
+ const byteSize = new TextEncoder().encode(canonicalPayload).byteLength;
757
+ if (byteSize > 262144) throw new ReleaseCatalogCompilationError("CATALOG_OVERSIZE", `Compiled Release catalog is ${byteSize} bytes; maximum is ${MAX_COMPILED_CATALOG_BYTES}`);
758
+ return {
759
+ byteSize,
760
+ canonicalPayload,
761
+ catalogHash: await sha256(canonicalPayload),
762
+ diagnostics: {
763
+ byteSize,
764
+ descriptorCount: payload.releaseDescriptors.length,
765
+ distinctTargetCohortCount: targetCohorts.size,
766
+ releaseCount: releases.length,
767
+ segmentCount: payload.strategy === "APP_VERSION" ? payload.segments.length : 1
768
+ },
769
+ payload
770
+ };
771
+ }
772
+ function versionInSegment(version, segment) {
773
+ if (segment.lower !== null) {
774
+ const compared = compareBounds(version, segment.lower.version);
775
+ if (compared < 0 || compared === 0 && !segment.lower.inclusive) return false;
776
+ }
777
+ if (segment.upper !== null) {
778
+ const compared = compareBounds(version, segment.upper.version);
779
+ if (compared > 0 || compared === 0 && !segment.upper.inclusive) return false;
780
+ }
781
+ return true;
782
+ }
783
+ function canonicalizeAppVersion(appVersion) {
784
+ const version = coerce(appVersion);
785
+ return version ? normalizeFull(version) : null;
786
+ }
787
+ function projectCompiledCatalog(catalog, appVersion) {
788
+ let indexes;
789
+ if (catalog.strategy === "FINGERPRINT") indexes = catalog.releaseIndexes;
790
+ else {
791
+ const canonicalVersion = appVersion && canonicalizeAppVersion(appVersion);
792
+ if (!canonicalVersion) return [];
793
+ indexes = catalog.segments.find((segment) => versionInSegment(canonicalVersion, segment))?.releaseIndexes ?? [];
794
+ }
795
+ return indexes.map((index) => catalog.releaseDescriptors[index]);
796
+ }
797
+ function projectCompiledRollbackCatalog(catalog, appVersion) {
798
+ let indexes;
799
+ if (catalog.strategy === "FINGERPRINT") indexes = catalog.rollbackReleaseIndexes ?? catalog.releaseIndexes;
800
+ else {
801
+ const canonicalVersion = appVersion && canonicalizeAppVersion(appVersion);
802
+ if (!canonicalVersion) return [];
803
+ const segment = catalog.segments.find((candidate) => versionInSegment(canonicalVersion, candidate));
804
+ indexes = segment?.rollbackReleaseIndexes ?? segment?.releaseIndexes ?? [];
805
+ }
806
+ return indexes.map((index) => catalog.releaseDescriptors[index]);
807
+ }
808
+ var ReleaseCatalogMutationError = class extends Error {
809
+ code;
810
+ name = "ReleaseCatalogMutationError";
811
+ constructor(code, message) {
812
+ super(message);
813
+ this.code = code;
814
+ }
815
+ };
816
+ /** A stored catalog payload, checked against its strategy's shape. */
817
+ var parseCompiledCatalog = (payload, strategy) => {
818
+ const parsed = JSON.parse(payload);
819
+ if (typeof parsed !== "object" || parsed === null || Reflect.get(parsed, "schemaVersion") !== 1 || Reflect.get(parsed, "strategy") !== strategy || Reflect.get(parsed, "fallbackPolicy") !== "BUILTIN_IF_ACTIVE_INELIGIBLE" || !Array.isArray(Reflect.get(parsed, "releaseDescriptors"))) throw new Error("Stored Release catalog payload is invalid");
820
+ if (strategy === "APP_VERSION" && !Array.isArray(Reflect.get(parsed, "segments"))) throw new Error("Stored app-version Release catalog is invalid");
821
+ if (strategy === "FINGERPRINT" && !Array.isArray(Reflect.get(parsed, "releaseIndexes"))) throw new Error("Stored fingerprint Release catalog is invalid");
822
+ return parsed;
823
+ };
824
+ /** The scope key an update check reads. */
825
+ var releaseCatalogScopeKeyOf = (input) => input.strategy === "APP_VERSION" ? createReleaseCatalogScopeKey({
826
+ channelKey: input.channelKey,
827
+ platform: input.platform,
828
+ strategy: "APP_VERSION"
829
+ }) : createReleaseCatalogScopeKey({
830
+ channelKey: input.channelKey,
831
+ fingerprintHash: input.fingerprintHash,
832
+ platform: input.platform,
833
+ strategy: "FINGERPRINT"
834
+ });
835
+ /** Projects a stored catalog row for one update check; null when the row is not this scope's. */
836
+ var projectReleaseCatalogRow = (row, input, scopeKey = releaseCatalogScopeKeyOf(input)) => {
837
+ if (row === null || row.platform !== input.platform || row.strategy !== input.strategy || row.channel_key !== input.channelKey || row.scope_key !== scopeKey || row.fingerprint_hash !== (input.strategy === "FINGERPRINT" ? input.fingerprintHash : null)) return null;
838
+ const compiled = parseCompiledCatalog(row.payload, input.strategy);
839
+ const appVersion = input.strategy === "APP_VERSION" ? input.appVersion : void 0;
840
+ return {
841
+ catalogId: row.catalog_id,
842
+ catalogHash: row.catalog_hash,
843
+ fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
844
+ generation: row.generation,
845
+ releases: projectCompiledCatalog(compiled, appVersion),
846
+ rollbackReleases: projectCompiledRollbackCatalog(compiled, appVersion),
847
+ schemaVersion: 1,
848
+ scopeKey
849
+ };
850
+ };
851
+ /** The scope and range a base-candidate key names; null for any other string. */
852
+ var parseBaseCandidateKey = (key) => {
853
+ try {
854
+ const parsed = JSON.parse(key);
855
+ if (!Array.isArray(parsed) || typeof parsed[0] !== "string") return null;
856
+ const [scopeKey, text] = parsed;
857
+ const { strategy } = parseReleaseCatalogScopeKey(scopeKey);
858
+ if (strategy === "FINGERPRINT") return parsed.length === 1 ? {
859
+ scopeKey,
860
+ range: null
861
+ } : null;
862
+ const range = parsed.length === 2 && typeof text === "string" ? normalizeRange(text) : null;
863
+ return range === null ? null : {
864
+ scopeKey,
865
+ range
866
+ };
867
+ } catch {
868
+ return null;
869
+ }
870
+ };
871
+ /** A compiled segment as a range: its version when it holds exactly one. */
872
+ var segmentRange = ({ lower, upper }) => lower?.inclusive && upper?.inclusive && lower.version === upper.version ? lower.version : [lower === null ? "" : `${lower.inclusive ? ">=" : ">"}${lower.version}`, upper === null ? "" : `${upper.inclusive ? "<=" : "<"}${upper.version}`].join(" ").trim() || "*";
873
+ /**
874
+ * Auto-patch bases from a scope's compiled Release Catalog, which holds
875
+ * every enabled bundle release of the scope: those whose range intersects
876
+ * `range` (all of them in a fingerprint scope), newest release first, each
877
+ * bundle once and older than `bundleId`, at most `limit`.
878
+ */
879
+ var baseBundleIdsOf = (row, range, bundleId, limit) => {
880
+ const catalog = parseCompiledCatalog(row.payload, row.strategy);
881
+ const serving = new Set(catalog.strategy === "FINGERPRINT" ? catalog.rollbackReleaseIndexes : catalog.segments.flatMap((segment) => range !== null && rangesIntersect(range, segmentRange(segment)) ? segment.rollbackReleaseIndexes : []));
882
+ const newestFirst = catalog.releaseDescriptors.map((descriptor, index) => ({
883
+ descriptor,
884
+ index
885
+ })).sort((left, right) => left.descriptor.releaseId < right.descriptor.releaseId ? 1 : -1);
886
+ const ids = [];
887
+ for (const { descriptor, index } of newestFirst) {
888
+ if (ids.length >= limit) break;
889
+ const id = descriptor.bundleId;
890
+ if (serving.has(index) && descriptor.kind === "BUNDLE" && id !== null && id < bundleId && !ids.includes(id)) ids.push(id);
891
+ }
892
+ return ids;
893
+ };
894
+ var MINUTE_MS = 6e4;
895
+ var HOUR_MS = 60 * MINUTE_MS;
896
+ var DAY_MS = 24 * HOUR_MS;
897
+ /** A model's retention as its physical table carries it, which each adapter maps natively. */
898
+ var physicalRetention = ({ retention }) => retention === void 0 ? {} : { retention: {
899
+ column: retention.field,
900
+ ms: retention.days * DAY_MS
901
+ } };
902
+ /** Models in a reference or a rooted index, which an expired row's delete would break. */
903
+ var linkedModels = (schema) => Object.entries(schema).flatMap(([name, { fields, indexes }]) => [...Object.values(fields).map(({ references }) => references), ...Object.values(indexes).map(({ root }) => root)].flatMap((link) => link ? [name, link.model] : []));
904
+ /**
905
+ * What is wrong with a model's retention: it counts from an integer field,
906
+ * over whole days, and expired rows are deleted without cascades, counter
907
+ * moves, or root bumps, so the model is in no reference or rooted index.
908
+ */
909
+ var retentionProblems = (schema, model) => {
910
+ const definition = schema[model];
911
+ if (definition.retention === void 0) return [];
912
+ const { field, days } = definition.retention;
913
+ const declared = definition.fields[field] ?? (definition.kind === "table" ? definition.derived[field] : void 0);
914
+ return [
915
+ ...declared?.type !== "integer" || "multi" in declared && declared.multi === true ? [`retention field "${field}" is not an integer field`] : [],
916
+ ...Number.isSafeInteger(days) && days >= 1 ? [] : ["retention days must be a whole number of at least 1"],
917
+ ...linkedModels(schema).includes(model) ? ["a model with retention is in no reference or rooted index"] : []
918
+ ];
919
+ };
920
+ /**
921
+ * Rows a pass deletes from each table: one statement or two a table, so a
922
+ * pass stays well inside D1's 50 queries an invocation.
923
+ */
924
+ var PASS_LIMIT = 500;
925
+ /** The lease row: when the next pass is due, in epoch ms. */
926
+ var LEASE_KEY = "retention.nextPassAt";
927
+ /**
928
+ * The adapter, pruning during its writes with no scheduler: once the lease
929
+ * row in `leaseTable` is due, the write that takes it first runs one bounded
930
+ * pass over the tables with retention, and other writers skip. The next pass is due in an hour, or in a minute while a pass still
931
+ * found a full batch of expired rows in some table. A backend that expires
932
+ * rows natively, or a schema without retention, gets its adapter back.
933
+ */
934
+ var pruneDuringWrites = (adapter, tables, { leaseTable: table, now }) => {
935
+ const expiring = tables.filter(({ retention }) => retention !== void 0);
936
+ if (adapter.prune === void 0 || expiring.length === 0) return adapter;
937
+ const key = [LEASE_KEY];
938
+ let checkAt = 0;
939
+ const lease = async (row, due) => {
940
+ const value = String(due);
941
+ const guard = { v: row === null ? -1 : Number(row._v) };
942
+ const next = {
943
+ key: LEASE_KEY,
944
+ value,
945
+ _v: guard.v + 1
946
+ };
947
+ const op = row === null ? {
948
+ type: "insert",
949
+ table,
950
+ row: next
951
+ } : {
952
+ type: "patch",
953
+ table,
954
+ key,
955
+ set: { value },
956
+ guard,
957
+ previous: row
958
+ };
959
+ return (await adapter.write([op])).ok ? next : null;
960
+ };
961
+ const pass = async () => {
962
+ const at = now();
963
+ if (at < checkAt) return;
964
+ checkAt = at + MINUTE_MS;
965
+ const [row = null] = await adapter.get(table, [key]);
966
+ checkAt = row === null ? at : Number(row.value);
967
+ if (checkAt > at) return;
968
+ const held = await lease(row, at + HOUR_MS);
969
+ if (held === null) return;
970
+ checkAt = at + HOUR_MS;
971
+ const deleted = [];
972
+ for (const t of expiring) deleted.push(await adapter.prune(t, at - t.retention.ms, PASS_LIMIT));
973
+ if (deleted.includes(PASS_LIMIT) && await lease(held, at + MINUTE_MS) !== null) checkAt = at + MINUTE_MS;
974
+ };
975
+ const warn = (error) => console.warn("[hot-updater] Pruning expired rows failed.", error);
976
+ return Object.assign(Object.create(adapter), { async write(ops) {
977
+ await pass().catch(warn);
978
+ return adapter.write(ops);
979
+ } });
980
+ };
981
+ var DatabaseSchemaError = class extends Error {
982
+ name = "DatabaseSchemaError";
983
+ };
984
+ var findPhysicalColumn = (table, name) => {
985
+ const column = table.columns.find((candidate) => candidate.name === name);
986
+ if (column === void 0) throw new DatabaseSchemaError(`${table.name} has no column ${name}.`);
987
+ return column;
988
+ };
989
+ var findPhysicalIndex = (table, name) => {
990
+ const index = table.indexes.find((candidate) => candidate.name === name);
991
+ if (index === void 0) throw new DatabaseSchemaError(`${table.name} has no index ${name}.`);
992
+ return index;
993
+ };
994
+ /**
995
+ * The columns that order an index read and close its cursors: the sort
996
+ * columns, then the key columns that neither `eq` nor `sort` fixes.
997
+ */
998
+ var indexOrderColumns = (table, index) => [...index.sort, ...table.key.filter((column) => !index.sort.includes(column) && !index.eq.includes(column))];
999
+ /** The primary key of a row, in key-column order. */
1000
+ var rowKey = (table, row) => table.key.map((column) => {
1001
+ const value = row[column];
1002
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") throw new DatabaseSchemaError(`${table.name}.${column} is a key column and must be a string, number, or boolean.`);
1003
+ return value;
1004
+ });
1005
+ var REGISTER_COUNT = 1024;
1006
+ var MAX_RANK = 55;
1007
+ var OFFSET = 33;
1008
+ var decode = (summary) => {
1009
+ if (!summary) return new Uint8Array(REGISTER_COUNT);
1010
+ if (summary.length !== REGISTER_COUNT) throw new Error("Invalid distinct sketch");
1011
+ const registers = new Uint8Array(REGISTER_COUNT);
1012
+ for (let index = 0; index < REGISTER_COUNT; index += 1) {
1013
+ const value = summary.charCodeAt(index) - OFFSET;
1014
+ if (!Number.isInteger(value) || value < 0 || value > MAX_RANK) throw new Error("Invalid distinct sketch");
1015
+ registers[index] = value;
1016
+ }
1017
+ return registers;
1018
+ };
1019
+ var encode$1 = (registers) => Array.from(registers, (value) => String.fromCharCode(value + OFFSET)).join("");
1020
+ /** The union of sketches, register by register. */
1021
+ var mergeDistinct = (summaries) => {
1022
+ const merged = new Uint8Array(REGISTER_COUNT);
1023
+ for (const summary of summaries) {
1024
+ const registers = decode(summary);
1025
+ for (let index = 0; index < REGISTER_COUNT; index += 1) merged[index] = Math.max(merged[index], registers[index]);
1026
+ }
1027
+ return encode$1(merged);
1028
+ };
1029
+ var DatabaseValueError = class extends Error {
1030
+ name = "DatabaseValueError";
1031
+ };
1032
+ /** Compares strings in UTF-8 byte order, which is Unicode code point order. */
1033
+ var compareUtf8 = (left, right) => {
1034
+ let leftIndex = 0;
1035
+ let rightIndex = 0;
1036
+ while (leftIndex < left.length && rightIndex < right.length) {
1037
+ const leftPoint = left.codePointAt(leftIndex);
1038
+ const rightPoint = right.codePointAt(rightIndex);
1039
+ if (leftPoint !== rightPoint) return leftPoint < rightPoint ? -1 : 1;
1040
+ leftIndex += leftPoint > 65535 ? 2 : 1;
1041
+ rightIndex += rightPoint > 65535 ? 2 : 1;
1042
+ }
1043
+ const leftDone = leftIndex >= left.length;
1044
+ return leftDone === rightIndex >= right.length ? 0 : leftDone ? -1 : 1;
1045
+ };
1046
+ /** Orders strings by UTF-8 bytes, numbers numerically, and false before true. */
1047
+ var compareKeyValues = (left, right) => {
1048
+ if (typeof left === "string" && typeof right === "string") return compareUtf8(left, right);
1049
+ if (typeof left === "number" && typeof right === "number") return left < right ? -1 : left > right ? 1 : 0;
1050
+ if (typeof left === "boolean" && typeof right === "boolean") return Number(left) - Number(right);
1051
+ throw new DatabaseValueError(`Cannot compare ${typeof left} with ${typeof right}.`);
1052
+ };
1053
+ /** Compares tuples element by element; a shorter equal prefix sorts first. */
1054
+ var compareTuples = (left, right) => {
1055
+ for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
1056
+ const result = compareKeyValues(left[index], right[index]);
1057
+ if (result !== 0) return result;
1058
+ }
1059
+ return left.length - right.length;
1060
+ };
1061
+ /** JSON text with object keys sorted, so a value compares equal after a store reorders map keys. */
1062
+ var canonicalJson = (value) => JSON.stringify(value ?? null, (_, inner) => inner !== null && typeof inner === "object" && !Array.isArray(inner) ? Object.fromEntries(Object.entries(inner).sort(([left], [right]) => compareUtf8(left, right))) : inner);
1063
+ var isKeyValue = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
1064
+ /** Every `eq` tuple a row is indexed under; none when an index field is null. */
1065
+ var indexEntries = (table, index, row) => {
1066
+ if (index.sort.some((column) => !isKeyValue(row[column]))) return [];
1067
+ let entries = [[]];
1068
+ for (const column of index.eq) {
1069
+ const value = row[column];
1070
+ const values = findPhysicalColumn(table, column).multi ? Array.isArray(value) ? value.filter(isKeyValue) : [] : isKeyValue(value) ? [value] : [];
1071
+ if (values.length === 0) return [];
1072
+ entries = entries.flatMap((entry) => [...new Set(values)].map((item) => [...entry, item]));
1073
+ }
1074
+ return entries;
1075
+ };
1076
+ /** The row's position in an index read, following `indexOrderColumns`. */
1077
+ var indexOrderTuple = (table, index, row) => indexOrderColumns(table, index).map((column) => {
1078
+ const value = row[column];
1079
+ if (!isKeyValue(value)) throw new DatabaseValueError(`${table.name}.${column} is not orderable for index ${index.name}.`);
1080
+ return value;
1081
+ });
1082
+ var satisfiesBound = (tuple, bound, side) => {
1083
+ if (bound === void 0) return true;
1084
+ const result = compareTuples(tuple.slice(0, bound.values.length), bound.values);
1085
+ if (result === 0) return bound.inclusive;
1086
+ return side === "lower" ? result > 0 : result < 0;
1087
+ };
1088
+ /** Whether a stored row belongs to the result range of a query. */
1089
+ var matchesQuery = (table, index, request, row) => indexEntries(table, index, row).some((entry) => compareTuples(entry, request.eq) === 0) && satisfiesBound(indexOrderTuple(table, index, row), request.lower, "lower") && satisfiesBound(indexOrderTuple(table, index, row), request.upper, "upper");
1090
+ var DatabaseAdapterContractError = class extends Error {
1091
+ name = "DatabaseAdapterContractError";
1092
+ };
1093
+ var EMPTY = {
1094
+ gets: 0,
1095
+ keys: 0,
1096
+ queries: 0,
1097
+ rows: 0
1098
+ };
1099
+ var createReadMeter = () => {
1100
+ const tables = /* @__PURE__ */ new Map();
1101
+ const add = (table, count) => {
1102
+ const current = tables.get(table) ?? EMPTY;
1103
+ tables.set(table, {
1104
+ gets: current.gets + (count.gets ?? 0),
1105
+ keys: current.keys + (count.keys ?? 0),
1106
+ queries: current.queries + (count.queries ?? 0),
1107
+ rows: current.rows + (count.rows ?? 0)
1108
+ });
1109
+ };
1110
+ return {
1111
+ add,
1112
+ meter: {
1113
+ total: () => [...tables.values()].reduce((sum, count) => ({
1114
+ gets: sum.gets + count.gets,
1115
+ keys: sum.keys + count.keys,
1116
+ queries: sum.queries + count.queries,
1117
+ rows: sum.rows + count.rows
1118
+ }), EMPTY),
1119
+ byTable: (table) => tables.get(table) ?? EMPTY,
1120
+ reset: () => tables.clear()
1121
+ }
1122
+ };
1123
+ };
1124
+ var fail$1 = (message) => {
1125
+ throw new DatabaseAdapterContractError(message);
1126
+ };
1127
+ var sameKey = (left, right) => left.length === right.length && compareTuples(left, right) === 0;
1128
+ var verifyRequest = (table, request) => {
1129
+ const index = findPhysicalIndex(table, request.index);
1130
+ if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 500) fail$1(`query limit must be 1–500.`);
1131
+ if (request.eq.length !== index.eq.length || !request.eq.every(isKeyValue)) fail$1(`query on ${table.name}.${index.name} must bind every eq column.`);
1132
+ return index;
1133
+ };
1134
+ var verifyRows = (table, request, rows) => {
1135
+ const index = findPhysicalIndex(table, request.index);
1136
+ if (rows.length > request.limit) fail$1(`${table.name}.${index.name} returned more than ${request.limit} rows.`);
1137
+ let previous;
1138
+ for (const row of rows) {
1139
+ if (!matchesQuery(table, index, request, row)) fail$1(`${table.name}.${index.name} returned a row outside the request.`);
1140
+ const tuple = indexOrderTuple(table, index, row);
1141
+ if (previous !== void 0) {
1142
+ const order = compareTuples(previous, tuple);
1143
+ if (request.order === "asc" ? order >= 0 : order <= 0) fail$1(`${table.name}.${index.name} returned rows out of order.`);
1144
+ }
1145
+ previous = tuple;
1146
+ }
1147
+ return previous;
1148
+ };
1149
+ var verifyWrite = (adapter, ops) => {
1150
+ if (ops.length === 0) fail$1("write needs at least one op.");
1151
+ const seen = /* @__PURE__ */ new Set();
1152
+ for (const op of ops) {
1153
+ const key = op.type === "insert" ? rowKey(op.table, op.row) : op.key;
1154
+ const id = JSON.stringify([op.table.name, key]);
1155
+ if (seen.has(id)) fail$1(`write touches ${op.table.name} ${id} twice.`);
1156
+ seen.add(id);
1157
+ if (op.type === "insert" && typeof op.row["_v"] !== "number") fail$1(`insert into ${op.table.name} must set _v.`);
1158
+ if (op.type === "patch" && Object.keys(op.set).some((column) => column === "_v" || op.table.key.includes(column))) fail$1(`patch of ${op.table.name} may not set key columns or _v.`);
1159
+ }
1160
+ if (!adapter.fits(ops)) fail$1("write does not fit in one atomic write.");
1161
+ };
1162
+ /** Wraps an adapter to check the contract at its boundary and count reads. */
1163
+ var verifyAdapter = (adapter, options = {}) => {
1164
+ const { add, meter } = createReadMeter();
1165
+ return {
1166
+ ...adapter,
1167
+ id: adapter.id,
1168
+ reads: meter,
1169
+ fits: (ops) => adapter.fits(ops),
1170
+ async get(table, keys) {
1171
+ const rows = await adapter.get(table, keys);
1172
+ add(table.name, {
1173
+ gets: 1,
1174
+ keys: keys.length
1175
+ });
1176
+ if (rows.length !== keys.length) fail$1(`${table.name} get returned ${rows.length} rows for ${keys.length} keys.`);
1177
+ rows.forEach((row, position) => {
1178
+ if (row !== null && !sameKey(rowKey(table, row), keys[position])) fail$1(`${table.name} get returned a row for another key.`);
1179
+ });
1180
+ return rows;
1181
+ },
1182
+ async query(table, request) {
1183
+ const index = verifyRequest(table, request);
1184
+ const rows = await adapter.query(table, request);
1185
+ add(table.name, {
1186
+ queries: 1,
1187
+ rows: rows.length
1188
+ });
1189
+ const last = verifyRows(table, request, rows);
1190
+ if (options.probeShortPages && last !== void 0 && rows.length < request.limit) {
1191
+ const bound = {
1192
+ values: last,
1193
+ inclusive: false
1194
+ };
1195
+ if ((await adapter.query(table, {
1196
+ ...request,
1197
+ limit: 1,
1198
+ ...request.order === "asc" ? { lower: bound } : { upper: bound }
1199
+ })).length > 0) fail$1(`${table.name}.${index.name} returned a short page before the range ended.`);
1200
+ }
1201
+ return rows;
1202
+ },
1203
+ ...adapter.prune && { async prune(table, before, limit) {
1204
+ if (table.retention === void 0) fail$1(`${table.name} has no retention to prune by.`);
1205
+ if (!Number.isSafeInteger(limit) || limit < 1) fail$1("prune limit must be a positive integer.");
1206
+ const deleted = await adapter.prune(table, before, limit);
1207
+ if (!Number.isSafeInteger(deleted) || deleted < 0 || deleted > limit) fail$1(`${table.name} prune reported ${deleted} rows for ${limit}.`);
1208
+ return deleted;
1209
+ } },
1210
+ async write(ops) {
1211
+ verifyWrite(adapter, ops);
1212
+ const result = await adapter.write(ops);
1213
+ if (!result.ok && "failedOp" in result) {
1214
+ if (!Number.isSafeInteger(result.failedOp) || result.failedOp < 0 || result.failedOp >= ops.length) fail$1(`write reported failedOp ${result.failedOp} for ${ops.length} ops.`);
1215
+ }
1216
+ return result;
1217
+ }
1218
+ };
1219
+ };
1220
+ var SHARD_COLUMN = "_shard";
1221
+ var NAME = /^[a-z][a-z0-9_]*$/u;
1222
+ var INDEX_NAME = /^[A-Za-z][A-Za-z0-9_]*$/u;
1223
+ var tableName = (module, model) => module.namespace ? `${module.namespace}_${model}` : model;
1224
+ var integer = (name, defaulted = false) => ({
1225
+ name,
1226
+ type: "integer",
1227
+ nullable: false,
1228
+ ...defaulted ? { default: 0 } : {}
1229
+ });
1230
+ /** A declared or derived field's type, and whether it holds several values. */
1231
+ var fieldType = (model, name) => {
1232
+ const field = model.fields[name] ?? (model.kind === "table" ? model.derived[name] : void 0);
1233
+ return field && {
1234
+ type: field.type,
1235
+ multi: "multi" in field && !!field.multi
1236
+ };
1237
+ };
1238
+ var metricsOf$1 = (model) => [
1239
+ ...model.counters,
1240
+ ...model.gauges,
1241
+ ...model.distinct
1242
+ ];
1243
+ /** Field names, the key, and each field's options. */
1244
+ var fieldProblems = (definition, add) => {
1245
+ const names = [...Object.keys(definition.fields), ...definition.kind === "table" ? Object.keys(definition.derived) : metricsOf$1(definition)];
1246
+ for (const name of new Set(names)) {
1247
+ if (!NAME.test(name)) add(`field "${name}" must match ${NAME.source}`);
1248
+ if (names.indexOf(name) !== names.lastIndexOf(name)) add(`field "${name}" is declared twice`);
1249
+ }
1250
+ const { key } = definition;
1251
+ if (key.length === 0) add("key is empty");
1252
+ if (new Set(key).size !== key.length) add("key repeats a field");
1253
+ for (const name of key) {
1254
+ const field = definition.fields[name];
1255
+ if (field === void 0) add(`key field "${name}" is not a declared field`);
1256
+ else if (field.required === false) add(`key field "${name}" is nullable`);
1257
+ else if (field.type === "json") add(`key field "${name}" is json`);
1258
+ }
1259
+ for (const [name, { type, maxLength, ascii }] of Object.entries(definition.fields)) {
1260
+ if (maxLength !== void 0 && (type !== "string" || !Number.isSafeInteger(maxLength) || maxLength < 1)) add(`field "${name}" has an invalid maxLength`);
1261
+ if (ascii && type !== "string") add(`field "${name}" is ascii but not a string`);
1262
+ }
1263
+ };
1264
+ /** A table's derived fields and references. */
1265
+ var tableProblems = (definition, add, table) => {
1266
+ for (const [name, derived] of Object.entries(definition.derived)) {
1267
+ if (typeof derived.compute !== "function") add(`derived field "${name}" has no compute function`);
1268
+ if (derived.multi && derived.type === "json") add(`derived field "${name}" cannot be multi-valued json`);
1269
+ }
1270
+ for (const [name, { type, references }] of Object.entries(definition.fields)) {
1271
+ if (references === void 0) continue;
1272
+ const target = table(references.model);
1273
+ const targetKey = target?.key.length === 1 ? target.fields[target.key[0]] : void 0;
1274
+ if (targetKey === void 0) add(`field "${name}" references "${references.model}", which is not a single-key table of this module`);
1275
+ else if (targetKey.type !== type) add(`field "${name}" and the key of "${references.model}" differ in type`);
1276
+ if (references.onDelete === "cascade" && !Object.values(definition.indexes).some(({ eq }) => eq.length === 1 && eq[0] === name)) add(`cascade on "${name}" needs an index whose eq is exactly [${name}]`);
1277
+ }
1278
+ };
1279
+ /** An aggregate's identity, metrics, and shards. */
1280
+ var aggregateProblems = (definition, add) => {
1281
+ const identity = Object.keys(definition.fields);
1282
+ if (definition.key.join("\0") !== identity.join("\0")) add(`key must list the identity fields in declaration order: ${identity.join(", ")}`);
1283
+ if (metricsOf$1(definition).length === 0) add("aggregate has no metrics");
1284
+ if (definition.distinct.length > 0 && definition.counters.length + definition.gauges.length > 0) add("distinct sketches need an aggregate of their own");
1285
+ const { shards } = definition;
1286
+ if (!Number.isSafeInteger(shards) || shards < 1 || shards > 64) add(`shards must be 1–64`);
1287
+ };
1288
+ /** Every problem in one model's declaration, its roots and references included. */
1289
+ var problemsOf = (module, model, definition) => {
1290
+ const problems = [];
1291
+ const add = (message) => problems.push(`${module.id}.${model}: ${message}`);
1292
+ const table = (name) => {
1293
+ const target = module.schema[name];
1294
+ return target?.kind === "table" ? target : void 0;
1295
+ };
1296
+ if (!NAME.test(model)) add("model names use lowercase letters, digits, and _");
1297
+ fieldProblems(definition, add);
1298
+ for (const [name, index] of Object.entries(definition.indexes)) {
1299
+ const columns = [...index.eq, ...index.sort];
1300
+ if (!INDEX_NAME.test(name)) add(`index "${name}" must match ${INDEX_NAME.source}`);
1301
+ if (columns.length === 0) add(`index "${name}" has no fields`);
1302
+ if (index.unique && index.sort.length > 0) add(`unique index "${name}" cannot sort`);
1303
+ if (columns.filter((column) => {
1304
+ const type = fieldType(definition, column);
1305
+ if (type === void 0) add(`index "${name}" names undeclared field "${column}"`);
1306
+ else if (type.type === "json") add(`index "${name}" names json field "${column}"`);
1307
+ else if (type.multi && index.sort.includes(column)) add(`index "${name}" sorts by multi-valued field "${column}"`);
1308
+ return type?.type !== "json" && type?.multi;
1309
+ }).length > 1) add(`index "${name}" has more than one multi-valued field`);
1310
+ if (index.root === void 0) continue;
1311
+ const root = table(index.root.model);
1312
+ if (root === void 0) add(`index "${name}" is rooted at unknown table "${index.root.model}"`);
1313
+ else if (root.key.length > index.eq.length || root.key.some((field, position) => fieldType(definition, index.eq[position])?.type !== root.fields[field].type)) add(`index "${name}" eq must start with the key of "${index.root.model}"`);
1314
+ }
1315
+ if (definition.kind === "table") tableProblems(definition, add, table);
1316
+ else aggregateProblems(definition, add);
1317
+ for (const problem of retentionProblems(module.schema, model)) add(problem);
1318
+ return problems;
1319
+ };
1320
+ /** Rejects invalid module schemas at startup, listing every problem. */
1321
+ var validateSchema = (modules) => {
1322
+ const problems = [];
1323
+ const owners = /* @__PURE__ */ new Map();
1324
+ for (const module of modules) for (const [model, definition] of Object.entries(module.schema)) {
1325
+ const name = tableName(module, model);
1326
+ const owner = owners.get(name);
1327
+ if (owner !== void 0) problems.push(`${module.id}.${model}: table "${name}" is also declared by ${owner}`);
1328
+ owners.set(name, `${module.id}.${model}`);
1329
+ problems.push(...problemsOf(module, model, definition));
1330
+ }
1331
+ if (problems.length > 0) throw new DatabaseSchemaError(`Invalid database schema:\n- ${problems.join("\n- ")}`);
1332
+ };
1333
+ var indexesOf = (definition) => [...Object.entries(definition.indexes).map(([name, index]) => ({
1334
+ name,
1335
+ eq: [...index.eq],
1336
+ sort: [...index.sort],
1337
+ ...index.unique ? { unique: true } : {}
1338
+ })), ...Object.entries(definition.fields).flatMap(([name, field]) => field.unique ? [{
1339
+ name,
1340
+ eq: [name],
1341
+ sort: [],
1342
+ unique: true
1343
+ }] : [])];
1344
+ /** Validates the modules, then resolves them into physical tables with engine columns. */
1345
+ var resolveSchema = (modules) => {
1346
+ validateSchema(modules);
1347
+ const references = [];
1348
+ for (const module of modules) for (const [model, definition] of Object.entries(module.schema)) {
1349
+ if (definition.kind !== "table") continue;
1350
+ const table = tableName(module, model);
1351
+ for (const [field, { references: to }] of Object.entries(definition.fields)) {
1352
+ if (to === void 0) continue;
1353
+ const { onDelete } = to;
1354
+ const target = tableName(module, to.model);
1355
+ const counter = onDelete === "none" ? {} : { counter: `_refs_${table}_${field}` };
1356
+ references.push({
1357
+ table,
1358
+ field,
1359
+ target,
1360
+ onDelete,
1361
+ ...counter
1362
+ });
1363
+ }
1364
+ }
1365
+ const models = /* @__PURE__ */ new Map();
1366
+ for (const module of modules) for (const [model, definition] of Object.entries(module.schema)) {
1367
+ const name = tableName(module, model);
1368
+ const referencedBy = references.filter(({ target }) => target === name);
1369
+ const columns = Object.entries(definition.fields).map(([field, { type, required, maxLength, ascii }]) => ({
1370
+ name: field,
1371
+ type,
1372
+ nullable: required === false,
1373
+ ...maxLength === void 0 ? {} : { maxLength },
1374
+ ...ascii ? { ascii } : {}
1375
+ }));
1376
+ if (definition.kind === "table") for (const [field, { type, multi }] of Object.entries(definition.derived)) columns.push({
1377
+ name: field,
1378
+ type,
1379
+ nullable: true,
1380
+ ...multi ? { multi } : {}
1381
+ });
1382
+ else columns.push(integer(SHARD_COLUMN), ...[...definition.counters, ...definition.gauges].map((metric) => integer(metric)), ...definition.distinct.map((metric) => ({
1383
+ name: metric,
1384
+ type: "string",
1385
+ nullable: true
1386
+ })));
1387
+ for (const { counter } of referencedBy) if (counter !== void 0) columns.push(integer(counter, true));
1388
+ columns.push(integer("_v", true));
1389
+ models.set(name, {
1390
+ module: module.id,
1391
+ model,
1392
+ definition,
1393
+ table: {
1394
+ name,
1395
+ columns,
1396
+ key: definition.kind === "aggregate" ? [...definition.key, SHARD_COLUMN] : [...definition.key],
1397
+ indexes: indexesOf(definition),
1398
+ ...physicalRetention(definition)
1399
+ },
1400
+ references: references.filter(({ table }) => table === name),
1401
+ referencedBy,
1402
+ roots: new Map(Object.entries(definition.indexes).flatMap(([index, { root }]) => root === void 0 ? [] : [[index, tableName(module, root.model)]]))
1403
+ });
1404
+ }
1405
+ return {
1406
+ models,
1407
+ tables: [...models.values()].map(({ table }) => table)
1408
+ };
1409
+ };
1410
+ var DatabaseConstraintError = class extends Error {
1411
+ reason;
1412
+ model;
1413
+ name = "DatabaseConstraintError";
1414
+ constructor(reason, model) {
1415
+ super(`${model}: ${reason}`);
1416
+ this.reason = reason;
1417
+ this.model = model;
1418
+ }
1419
+ };
1420
+ /** The retry budget ran out; ingestion answers 503 with Retry-After. */
1421
+ var DatabaseConflictError = class extends Error {
1422
+ name = "DatabaseConflictError";
1423
+ };
1424
+ /** The write may or may not have committed; it is reported, never rerun. */
1425
+ var DatabaseAmbiguousCommitError = class extends Error {
1426
+ name = "DatabaseAmbiguousCommitError";
1427
+ };
1428
+ var DatabaseTransactionError = class extends Error {
1429
+ name = "DatabaseTransactionError";
1430
+ };
1431
+ var DatabaseCursorError = class extends Error {
1432
+ name = "DatabaseCursorError";
1433
+ };
1434
+ var encoder = new TextEncoder();
1435
+ var decoder = new TextDecoder();
1436
+ var toBase64Url = (text) => {
1437
+ let binary = "";
1438
+ for (const byte of encoder.encode(text)) binary += String.fromCharCode(byte);
1439
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
1440
+ };
1441
+ var fromBase64Url = (value) => {
1442
+ const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/"));
1443
+ return decoder.decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
1444
+ };
1445
+ /** A stable 32-bit FNV-1a hash of the text's UTF-8 bytes. */
1446
+ var fnv1a = (text) => {
1447
+ let hash = 2166136261;
1448
+ for (const byte of encoder.encode(text)) hash = Math.imul(hash ^ byte, 16777619) >>> 0;
1449
+ return hash;
1450
+ };
1451
+ /** A hash of the request's identity; a cursor only resumes the same read. */
1452
+ var cursorScope = (parts) => fnv1a(JSON.stringify(parts)).toString(36);
1453
+ var encodeCursor = (scope, after) => toBase64Url(JSON.stringify([scope, after]));
1454
+ /** The order tuple a cursor resumes after; rejects a cursor from another read. */
1455
+ var decodeCursor = (scope, cursor) => {
1456
+ let decoded;
1457
+ try {
1458
+ decoded = JSON.parse(fromBase64Url(cursor));
1459
+ } catch {
1460
+ decoded = void 0;
1461
+ }
1462
+ const [from, after] = Array.isArray(decoded) ? decoded : [];
1463
+ if (typeof from !== "string" || !Array.isArray(after) || !after.every(isKeyValue)) throw new DatabaseCursorError("The cursor is malformed.");
1464
+ if (from !== scope) throw new DatabaseCursorError("The cursor belongs to a read with another model, index, filter, order, or range.");
1465
+ return after;
1466
+ };
1467
+ var definitionOf = (model) => model.definition;
1468
+ /** Every change keyed by one value lands on one shard. */
1469
+ var shardOf = (value, shards) => fnv1a(value) % shards;
1470
+ /** Merges one `tx.aggregate` call into its shard row's pending changes. */
1471
+ var recordAggregate = (changes, model, identity, values, shardBy) => {
1472
+ const { key: fields, counters, gauges, distinct, shards } = definitionOf(model);
1473
+ const fail = (message) => new DatabaseTransactionError(`${model.table.name}: ${message}`);
1474
+ if (Object.keys(identity).length !== fields.length || !fields.every((name) => isKeyValue(identity[name]))) throw fail(`aggregate takes exactly its identity fields: ${fields.join(", ")}.`);
1475
+ if (shardBy === void 0 && shards > 1 && gauges.some((gauge) => gauge in values)) throw fail("gauges of a sharded aggregate need shardBy.");
1476
+ const shard = shardBy === void 0 ? Math.floor(Math.random() * shards) : shardOf(shardBy, shards);
1477
+ const key = [...fields.map((name) => identity[name]), shard];
1478
+ const id = JSON.stringify([model.table.name, key]);
1479
+ const change = changes.get(id) ?? {
1480
+ model,
1481
+ key,
1482
+ deltas: {},
1483
+ sketches: {}
1484
+ };
1485
+ changes.set(id, change);
1486
+ for (const [metric, value] of Object.entries(values)) if (distinct.includes(metric) && typeof value === "string") change.sketches[metric] = mergeDistinct([change.sketches[metric], value]);
1487
+ else if ((counters.includes(metric) || gauges.includes(metric)) && Number.isSafeInteger(value)) change.deltas[metric] = (change.deltas[metric] ?? 0) + Number(value);
1488
+ else throw fail(`${metric} is not a counter, gauge, or sketch of this aggregate.`);
1489
+ };
1490
+ /** A shard row before its first change: every metric zero or empty. */
1491
+ var blank$1 = (model, key) => {
1492
+ const { counters, gauges, distinct } = definitionOf(model);
1493
+ return Object.fromEntries([
1494
+ ...model.table.key.map((column, position) => [column, key[position]]),
1495
+ ...[...counters, ...gauges].map((name) => [name, 0]),
1496
+ ...distinct.map((name) => [name, null]),
1497
+ ["_v", 0]
1498
+ ]);
1499
+ };
1500
+ /**
1501
+ * A gauge computed below zero. Deltas from a row another writer has since
1502
+ * moved do this, so the transaction reruns; it is thrown only when it lasts.
1503
+ */
1504
+ var NegativeGaugeError = class extends DatabaseTransactionError {};
1505
+ /** The guarded write that merges `change` into `current`: insert, patch, or delete at zero. */
1506
+ var rewrite$1 = ({ model, key, deltas, sketches }, current) => {
1507
+ const { table } = model;
1508
+ const { counters, gauges, distinct } = definitionOf(model);
1509
+ const base = current ?? blank$1(model, key);
1510
+ const set = {};
1511
+ for (const [metric, delta] of Object.entries(deltas)) set[metric] = Number(base[metric] ?? 0) + delta;
1512
+ for (const [metric, sketch] of Object.entries(sketches)) set[metric] = mergeDistinct([base[metric], sketch]);
1513
+ if (current && Object.entries(set).every(([metric, value]) => current[metric] === value)) return;
1514
+ const row = {
1515
+ ...base,
1516
+ ...set
1517
+ };
1518
+ if (gauges.some((gauge) => Number(row[gauge]) < 0)) {
1519
+ if (current === null && table.retention) return void 0;
1520
+ throw new NegativeGaugeError(`${table.name}: a gauge went below 0.`);
1521
+ }
1522
+ const zero = distinct.length === 0 && [...counters, ...gauges].every((metric) => Number(row[metric]) === 0);
1523
+ if (current === null) return zero ? void 0 : {
1524
+ type: "insert",
1525
+ table,
1526
+ row
1527
+ };
1528
+ const guard = { v: Number(current["_v"]) };
1529
+ return zero ? {
1530
+ type: "delete",
1531
+ table,
1532
+ key,
1533
+ guard,
1534
+ previous: current
1535
+ } : {
1536
+ type: "patch",
1537
+ table,
1538
+ key,
1539
+ set,
1540
+ guard,
1541
+ previous: current
1542
+ };
1543
+ };
1544
+ /**
1545
+ * Counter-only rows become blind increments that create the row when it is
1546
+ * missing. Rows with gauges or sketches are read in one parallel batch per
1547
+ * table, merged, and written back under a guard, so a conflict resends only
1548
+ * them.
1549
+ */
1550
+ var compileAggregates = async (adapter, changes) => {
1551
+ const ops = [];
1552
+ const merged = /* @__PURE__ */ new Map();
1553
+ for (const change of changes) {
1554
+ const { model, key, deltas, sketches } = change;
1555
+ if (Object.keys(sketches).length > 0 || definitionOf(model).gauges.some((gauge) => (deltas[gauge] ?? 0) !== 0)) merged.set(model, [...merged.get(model) ?? [], change]);
1556
+ else if (Object.values(deltas).some((delta) => delta !== 0)) {
1557
+ const { table } = model;
1558
+ const init = blank$1(model, key);
1559
+ ops.push({
1560
+ type: "increment",
1561
+ table,
1562
+ key,
1563
+ by: deltas,
1564
+ init
1565
+ });
1566
+ }
1567
+ }
1568
+ const groups = [...merged.values()];
1569
+ const current = await Promise.all(groups.map((group) => adapter.get(group[0].model.table, group.map(({ key }) => key))));
1570
+ groups.forEach((group, index) => group.forEach((change, position) => {
1571
+ const op = rewrite$1(change, current[index][position] ?? null);
1572
+ if (op !== void 0) ops.push(op);
1573
+ }));
1574
+ return ops;
1575
+ };
1576
+ var defineTable = (fields, options) => ({
1577
+ kind: "table",
1578
+ fields,
1579
+ key: options.key,
1580
+ derived: options.derived ?? {},
1581
+ indexes: options.indexes ?? {},
1582
+ ...options.retention === void 0 ? {} : { retention: options.retention }
1583
+ });
1584
+ var defineAggregate = (fields, options) => ({
1585
+ kind: "aggregate",
1586
+ fields,
1587
+ key: options.key,
1588
+ counters: options.counters ?? [],
1589
+ gauges: options.gauges ?? [],
1590
+ distinct: options.distinct ?? [],
1591
+ shards: options.shards ?? 1,
1592
+ indexes: options.indexes ?? {},
1593
+ ...options.batched ? { batched: true } : {},
1594
+ ...options.retention === void 0 ? {} : { retention: options.retention }
1595
+ });
1596
+ /** Log rows spread over 8 tables, each its own DynamoDB partition. */
1597
+ var LOG_TABLES = [
1598
+ 0,
1599
+ 1,
1600
+ 2,
1601
+ 3,
1602
+ 4,
1603
+ 5,
1604
+ 6,
1605
+ 7
1606
+ ].map((at) => `aggregate_log_${at}`);
1607
+ var LEASE_TABLE = "aggregate_lease";
1608
+ var LEASE = { id: "compaction" };
1609
+ /** How long a compaction holds the lease before another process may take it. */
1610
+ var LEASE_MS = 3e4;
1611
+ /**
1612
+ * Default windows: log mode compacts every 60 seconds, since reads compact
1613
+ * first; memory mode flushes every 15, since its window is what a crash
1614
+ * loses.
1615
+ */
1616
+ var WINDOW_MS = {
1617
+ log: 6e4,
1618
+ memory: 15e3
1619
+ };
1620
+ /** Log rows a compaction reads from each log table for one group. */
1621
+ var LOG_PAGE = 200;
1622
+ /**
1623
+ * Groups one compaction applies: after a commit, so the request that runs
1624
+ * it waits for one group; before a read; and on flush.
1625
+ */
1626
+ var GROUPS = {
1627
+ commit: 1,
1628
+ read: 8,
1629
+ flush: 256
1630
+ };
1631
+ /** Tries per write after a conflict or a transient error. */
1632
+ var ATTEMPTS = 5;
1633
+ /** Aggregate rows a memory buffer holds before a commit waits for a flush. */
1634
+ var MAX_BUFFERED = 1e4;
1635
+ var log = defineTable({
1636
+ id: {
1637
+ type: "string",
1638
+ maxLength: 32
1639
+ },
1640
+ changes: { type: "string" }
1641
+ }, {
1642
+ key: ["id"],
1643
+ indexes: { oldest: {
1644
+ eq: [],
1645
+ sort: ["id"]
1646
+ } }
1647
+ });
1648
+ /**
1649
+ * Held by `holder` until `until`; the next compaction is due at `next`.
1650
+ * `applied` lists the log rows a compaction has applied and not yet deleted,
1651
+ * as `[log table's index, id]` pairs, so no compaction applies them again.
1652
+ */
1653
+ var lease = defineTable({
1654
+ id: {
1655
+ type: "string",
1656
+ maxLength: 16
1657
+ },
1658
+ holder: {
1659
+ type: "string",
1660
+ maxLength: 32
1661
+ },
1662
+ until: { type: "integer" },
1663
+ next: { type: "integer" },
1664
+ applied: { type: "string" }
1665
+ }, { key: ["id"] });
1666
+ /** The log and lease tables; an engine that batches has them in its schema. */
1667
+ var aggregateBatchingModule = {
1668
+ id: "aggregateBatching",
1669
+ schema: {
1670
+ ...Object.fromEntries(LOG_TABLES.map((table) => [table, log])),
1671
+ [LEASE_TABLE]: lease
1672
+ }
1673
+ };
1674
+ var shapeOf = (model) => model.definition;
1675
+ var isBatched = (model) => model?.definition.kind === "aggregate" && shapeOf(model).batched === true;
1676
+ var valuesOf = ({ deltas, sketches }) => ({
1677
+ ...deltas,
1678
+ ...sketches
1679
+ });
1680
+ /**
1681
+ * Merges `values` into the change for `key`'s identity as `tx.aggregate`
1682
+ * does: deltas add up and sketches merge. One constant `shardBy` keys each
1683
+ * identity to one entry; the write picks the shard.
1684
+ */
1685
+ var merge = (into, model, key, values) => recordAggregate(into, model, Object.fromEntries(shapeOf(model).key.map((field, at) => [field, key[at]])), values, "");
1686
+ var mergeAll = (into, changes) => {
1687
+ for (const change of changes) merge(into, change.model, change.key, valuesOf(change));
1688
+ return into;
1689
+ };
1690
+ /** A row's metrics as values to merge: counters, gauges, and the sketches it has. */
1691
+ var metricsOf = (model, row) => {
1692
+ const { counters, gauges, distinct } = shapeOf(model);
1693
+ return Object.fromEntries([...[...counters, ...gauges].map((name) => [name, Number(row[name] ?? 0)]), ...distinct.flatMap((name) => row[name] === null || row[name] === void 0 ? [] : [[name, row[name]]])]);
1694
+ };
1695
+ /** A shard row before its first change: every metric zero or empty. */
1696
+ var blank = (model, key) => {
1697
+ const { counters, gauges, distinct } = shapeOf(model);
1698
+ return Object.fromEntries([
1699
+ ...model.table.key.map((column, at) => [column, key[at]]),
1700
+ ...[...counters, ...gauges].map((name) => [name, 0]),
1701
+ ...distinct.map((name) => [name, null]),
1702
+ ["_v", 0]
1703
+ ]);
1704
+ };
1705
+ /** Whether a change needs its row: sketches merge, and a gauge may reach zero. */
1706
+ var rewrites = ({ model, deltas, sketches }) => Object.keys(sketches).length > 0 || shapeOf(model).gauges.some((gauge) => (deltas[gauge] ?? 0) !== 0);
1707
+ var increment = ({ model, deltas }, key) => Object.values(deltas).some((delta) => delta !== 0) ? {
1708
+ type: "increment",
1709
+ table: model.table,
1710
+ key,
1711
+ by: deltas,
1712
+ init: blank(model, key)
1713
+ } : void 0;
1714
+ /**
1715
+ * The guarded write that merges `change` into `current`: insert, patch, or
1716
+ * delete at zero. Unlike a transaction's, a gauge may go below zero here: a
1717
+ * shard row holds one writer's net changes, and reads sum every shard.
1718
+ */
1719
+ var rewrite = (change, key, current) => {
1720
+ const { model } = change;
1721
+ const { table } = model;
1722
+ const { counters, gauges, distinct } = shapeOf(model);
1723
+ const base = current ?? blank(model, key);
1724
+ const sum = mergeAll(/* @__PURE__ */ new Map(), [change]);
1725
+ merge(sum, model, key, metricsOf(model, base));
1726
+ const set = valuesOf([...sum.values()][0]);
1727
+ if (current && Object.keys(set).every((name) => current[name] === set[name])) return void 0;
1728
+ const row = {
1729
+ ...base,
1730
+ ...set
1731
+ };
1732
+ const zero = distinct.length === 0 && [...counters, ...gauges].every((name) => Number(row[name]) === 0);
1733
+ if (current === null) return zero ? void 0 : {
1734
+ type: "insert",
1735
+ table,
1736
+ row
1737
+ };
1738
+ const guard = { v: Number(current["_v"]) };
1739
+ return zero ? {
1740
+ type: "delete",
1741
+ table,
1742
+ key,
1743
+ guard,
1744
+ previous: current
1745
+ } : {
1746
+ type: "patch",
1747
+ table,
1748
+ key,
1749
+ set,
1750
+ guard,
1751
+ previous: current
1752
+ };
1753
+ };
1754
+ /**
1755
+ * The write for each change on its shard: a blind increment that creates
1756
+ * the row for counters, and a guarded rewrite for gauges and sketches, whose
1757
+ * rows are read in one batch per table.
1758
+ */
1759
+ var compile = async (adapter, changes, shard) => {
1760
+ const keys = changes.map(({ model, key }) => [...key.slice(0, -1), shard(model)]);
1761
+ const reads = /* @__PURE__ */ new Map();
1762
+ changes.forEach((change, at) => {
1763
+ if (rewrites(change)) reads.set(change.model, [...reads.get(change.model) ?? [], at]);
1764
+ });
1765
+ const current = /* @__PURE__ */ new Map();
1766
+ await Promise.all([...reads].map(async ([model, positions]) => {
1767
+ const rows = await adapter.get(model.table, positions.map((at) => keys[at]));
1768
+ positions.forEach((at, index) => current.set(at, rows[index] ?? null));
1769
+ }));
1770
+ return changes.map((change, at) => current.has(at) ? rewrite(change, keys[at], current.get(at)) : increment(change, keys[at]));
1771
+ };
1772
+ var isOp = (op) => op !== void 0;
1773
+ var sleep = (attempt) => new Promise((resolve) => setTimeout(resolve, Math.min(250, 5 * 2 ** attempt) * Math.random()));
1774
+ /** Runs of one character, where shorter: a sparse sketch packs into a few. */
1775
+ var pack = (text) => {
1776
+ const runs = (text.match(/(.)\1*/gsu) ?? []).flatMap((run) => [run[0], run.length]);
1777
+ return JSON.stringify(runs).length < JSON.stringify(text).length ? runs : text;
1778
+ };
1779
+ var unpack = (value) => Array.isArray(value) ? value.map((item, at) => at % 2 ? "" : item.repeat(value[at + 1])).join("") : value;
1780
+ /** Raw deflate of `text` as base64, where the runtime has `CompressionStream`. */
1781
+ var deflate = async (text) => {
1782
+ if (typeof CompressionStream === "undefined") return void 0;
1783
+ const stream = new Blob([text]).stream().pipeThrough(new CompressionStream("deflate-raw"));
1784
+ let binary = "";
1785
+ for (const byte of new Uint8Array(await new Response(stream).arrayBuffer())) binary += String.fromCharCode(byte);
1786
+ return btoa(binary);
1787
+ };
1788
+ var inflate = (base64) => new Response(new Blob([Uint8Array.from(atob(base64), (char) => char.charCodeAt(0))]).stream().pipeThrough(new DecompressionStream("deflate-raw"))).text();
1789
+ /**
1790
+ * A log row's `changes`: entries of a table, an identity, and values, as
1791
+ * JSON, or `z` and its deflate where shorter, so a row stays under the 1 KB
1792
+ * that DynamoDB bills as one unit.
1793
+ */
1794
+ var encode = async (changes) => {
1795
+ const json = JSON.stringify({
1796
+ v: 1,
1797
+ c: [...changes].map(({ model, key, deltas, sketches }) => {
1798
+ const packed = Object.entries(sketches).map(([name, sketch]) => [name, pack(sketch)]);
1799
+ const values = {
1800
+ ...deltas,
1801
+ ...Object.fromEntries(packed)
1802
+ };
1803
+ return [
1804
+ model.table.name,
1805
+ key.slice(0, -1),
1806
+ values
1807
+ ];
1808
+ })
1809
+ });
1810
+ const zipped = await deflate(json);
1811
+ return zipped !== void 0 && zipped.length < json.length ? `z${zipped}` : json;
1812
+ };
1813
+ /**
1814
+ * Batches changes to aggregates declared `batched`. A transaction records
1815
+ * them without writing their rows; once it commits, "memory" mode keeps
1816
+ * them in this process until its timer flushes them, and "log" mode has
1817
+ * written them as one log row in the same atomic write, which a compaction
1818
+ * later merges into the aggregates and deletes, atomically again, so each
1819
+ * applies exactly once. One lease row keeps compactions one at a time.
1820
+ */
1821
+ var createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "log", windowMs = WINDOW_MS[mode] }, now }) => {
1822
+ if (!schema.models.has(LEASE_TABLE)) throw new DatabaseTransactionError("An engine that batches aggregates needs aggregateBatchingModule in its schema.");
1823
+ const holder = crypto.randomUUID().replaceAll("-", "");
1824
+ const table = (name) => schema.models.get(name).table;
1825
+ const warned = /* @__PURE__ */ new Set();
1826
+ const warn = (what) => (error) => {
1827
+ if (warned.has(what)) return;
1828
+ warned.add(what);
1829
+ console.warn(`[hot-updater] Aggregate ${what} failed.`, error);
1830
+ };
1831
+ /** A log row's changes, or undefined when it names an aggregate or metric this schema lacks. */
1832
+ const decode = async (text) => {
1833
+ const changes = /* @__PURE__ */ new Map();
1834
+ try {
1835
+ const json = text.startsWith("z") ? await inflate(text.slice(1)) : text;
1836
+ const { c } = JSON.parse(json);
1837
+ for (const [name, identity, values] of c) {
1838
+ const model = schema.models.get(name);
1839
+ if (!isBatched(model)) return void 0;
1840
+ const unpacked = Object.entries(values).map(([metric, value]) => [metric, unpack(value)]);
1841
+ merge(changes, model, [...identity, 0], Object.fromEntries(unpacked));
1842
+ }
1843
+ } catch (error) {
1844
+ warn("log decoding")(error);
1845
+ return;
1846
+ }
1847
+ return changes;
1848
+ };
1849
+ /**
1850
+ * Writes changes in atomic chunks within `fits`, and returns the ones it
1851
+ * could not apply. A write whose outcome is unknown is dropped, never
1852
+ * resent, so no change applies twice.
1853
+ */
1854
+ const apply = async (changes, shard) => {
1855
+ let rest = changes;
1856
+ let ops = [];
1857
+ let read = true;
1858
+ for (let failures = 0; rest.length > 0 && failures < ATTEMPTS;) try {
1859
+ if (read) ops = await compile(adapter, rest, shard);
1860
+ let size = rest.length;
1861
+ const chunk = () => ops.slice(0, size).filter(isOp);
1862
+ while (size > 1 && !adapter.fits(chunk())) size = Math.ceil(size / 2);
1863
+ let result = { ok: true };
1864
+ if (chunk().length > 0) result = await adapter.write(chunk()).catch((error) => {
1865
+ warn("flush")(error);
1866
+ return { ok: true };
1867
+ });
1868
+ read = !result.ok && !("retry" in result);
1869
+ if (result.ok) {
1870
+ rest = rest.slice(size);
1871
+ ops = ops.slice(size);
1872
+ failures = 0;
1873
+ } else {
1874
+ failures += 1;
1875
+ if (!read) await sleep(failures);
1876
+ }
1877
+ } catch (error) {
1878
+ warn("flush")(error);
1879
+ return rest;
1880
+ }
1881
+ return rest;
1882
+ };
1883
+ let buffer = /* @__PURE__ */ new Map();
1884
+ let timer;
1885
+ let flushing;
1886
+ const shards = /* @__PURE__ */ new Map();
1887
+ /** Each process writes its own shard, so flushes of two processes rarely meet. */
1888
+ const processShard = (model) => {
1889
+ if (!shards.has(model)) shards.set(model, Math.floor(Math.random() * shapeOf(model).shards));
1890
+ return shards.get(model);
1891
+ };
1892
+ const schedule = () => {
1893
+ if (timer !== void 0 || buffer.size === 0) return;
1894
+ timer = setTimeout(() => {
1895
+ timer = void 0;
1896
+ flushBuffer();
1897
+ }, windowMs);
1898
+ timer.unref?.();
1899
+ };
1900
+ /** Never rejects: what it cannot apply goes back into the buffer. */
1901
+ const flushBuffer = () => flushing ??= (async () => {
1902
+ clearTimeout(timer);
1903
+ timer = void 0;
1904
+ const pending = [...buffer.values()];
1905
+ buffer = /* @__PURE__ */ new Map();
1906
+ mergeAll(buffer, await apply(pending, processShard));
1907
+ })().catch(warn("flush")).finally(() => {
1908
+ flushing = void 0;
1909
+ schedule();
1910
+ });
1911
+ let nextDue = 0;
1912
+ let compacting;
1913
+ /** Log rows this process read, by `[log table's index, id]`, for the deletes after a group. */
1914
+ const known = /* @__PURE__ */ new Map();
1915
+ const idOf = (shard, id) => JSON.stringify([shard, id]);
1916
+ /** The lease row as stored once `set` is written over it. */
1917
+ const leaseOp = (held, set) => {
1918
+ const next = {
1919
+ ...held,
1920
+ ...set,
1921
+ _v: Number(held._v) + 1
1922
+ };
1923
+ return {
1924
+ op: {
1925
+ type: "patch",
1926
+ table: table(LEASE_TABLE),
1927
+ key: [LEASE.id],
1928
+ set,
1929
+ guard: { v: Number(held._v) },
1930
+ previous: held
1931
+ },
1932
+ next
1933
+ };
1934
+ };
1935
+ /** The lease row as stored after this process takes it, or undefined when it is held or not due. */
1936
+ const takeLease = (force) => engine.transaction(async (tx) => {
1937
+ const at = now();
1938
+ const held = await tx.findOne(LEASE_TABLE, LEASE);
1939
+ const fields = {
1940
+ holder,
1941
+ until: at + LEASE_MS
1942
+ };
1943
+ if (held === null) {
1944
+ const row = {
1945
+ ...LEASE,
1946
+ ...fields,
1947
+ next: 0,
1948
+ applied: "[]"
1949
+ };
1950
+ tx.create(LEASE_TABLE, row);
1951
+ return {
1952
+ ...row,
1953
+ _v: 0
1954
+ };
1955
+ }
1956
+ nextDue = Number(held.next);
1957
+ if (held.holder !== holder && Number(held.until) > at || !force && nextDue > at) return void 0;
1958
+ tx.update(LEASE_TABLE, held, fields);
1959
+ return {
1960
+ ...held,
1961
+ ...fields,
1962
+ _v: Number(held._v) + 1
1963
+ };
1964
+ });
1965
+ /**
1966
+ * Writes `ops` once, retrying transient errors; true when it committed.
1967
+ * A conflict leaves its log rows for the next compaction; a write whose
1968
+ * outcome is unknown throws, and the next compaction reads what it did.
1969
+ */
1970
+ const commit = async (ops) => {
1971
+ for (let failures = 0; failures < ATTEMPTS; failures += 1) {
1972
+ const result = await adapter.write(ops);
1973
+ if (result.ok) return true;
1974
+ if (!("retry" in result)) return false;
1975
+ await sleep(failures);
1976
+ }
1977
+ return false;
1978
+ };
1979
+ /** Deletes the log rows `held` lists as applied, in parallel writes; true once none is left. */
1980
+ const deleteApplied = async (held) => {
1981
+ const applied = JSON.parse(String(held.applied));
1982
+ await Promise.all(LOG_TABLES.map(async (name, shard) => {
1983
+ const ids = applied.flatMap(([at, id]) => at === shard && !known.has(idOf(at, id)) ? [id] : []);
1984
+ if (ids.length === 0) return;
1985
+ const rows = await adapter.get(table(name), ids.map((id) => [id]));
1986
+ for (const row of rows) if (row) known.set(idOf(shard, row.id), row);
1987
+ }));
1988
+ const rows = applied.flatMap(([shard, id]) => {
1989
+ const row = known.get(idOf(shard, id));
1990
+ return row === void 0 ? [] : [{
1991
+ shard,
1992
+ row
1993
+ }];
1994
+ });
1995
+ if (adapter.deleteConsumed && await Promise.all(LOG_TABLES.map((name, at) => {
1996
+ const own = rows.flatMap(({ shard, row }) => shard === at ? [row] : []);
1997
+ return own.length > 0 && adapter.deleteConsumed(table(name), own);
1998
+ })).then(() => true, (error) => (warn("log cleanup")(error), false))) {
1999
+ for (const [shard, id] of applied) known.delete(idOf(shard, id));
2000
+ return true;
2001
+ }
2002
+ const deletes = rows.map(({ shard, row }) => {
2003
+ const guard = { v: Number(row["_v"]) };
2004
+ return {
2005
+ type: "delete",
2006
+ table: table(LOG_TABLES[shard]),
2007
+ key: [String(row.id)],
2008
+ guard,
2009
+ previous: row
2010
+ };
2011
+ });
2012
+ let size = deletes.length;
2013
+ while (size > 1 && !adapter.fits(deletes.slice(0, size))) size = Math.ceil(size / 2);
2014
+ const chunks = [];
2015
+ for (let at = 0; at < deletes.length; at += size) chunks.push(deletes.slice(at, at + size));
2016
+ if ((await Promise.all(chunks.map(commit))).includes(false)) return false;
2017
+ for (const [shard, id] of applied) known.delete(idOf(shard, id));
2018
+ return true;
2019
+ };
2020
+ /**
2021
+ * The oldest pending log rows this schema can read, a page from each
2022
+ * table; `more` when a page was full, so more may wait.
2023
+ */
2024
+ const readLogs = async () => {
2025
+ let more = false;
2026
+ return {
2027
+ logs: (await Promise.all(LOG_TABLES.map(async (name, shard) => {
2028
+ const page = await engine.reads.findMany(name, {
2029
+ index: "oldest",
2030
+ limit: LOG_PAGE
2031
+ });
2032
+ more ||= page.next !== void 0;
2033
+ return (await Promise.all(page.rows.map(async (row) => ({
2034
+ shard,
2035
+ row,
2036
+ changes: await decode(String(row.changes))
2037
+ })))).flatMap(({ changes, ...entry }) => changes === void 0 ? [] : [{
2038
+ ...entry,
2039
+ changes
2040
+ }]);
2041
+ }))).flat().sort((left, right) => compareUtf8(String(left.row.id), String(right.row.id))),
2042
+ more
2043
+ };
2044
+ };
2045
+ const rowsFor = async (changes) => (await compile(adapter, changes, () => 0)).filter(isOp);
2046
+ /**
2047
+ * Applies the oldest pending log rows whose merged changes fit in one
2048
+ * atomic write with the lease, which lists them as applied; the rows are
2049
+ * deleted after it. A log row too large for one write applies in parts,
2050
+ * keeping the rest in it. Returns the lease after the write and how many
2051
+ * log rows it applied whole, or undefined when a conflict stopped it.
2052
+ */
2053
+ const applyGroup = async (held, logs) => {
2054
+ for (let count = logs.length; count > 0; count = Math.floor(count / 2)) {
2055
+ const batch = logs.slice(0, count);
2056
+ const changes = mergeAll(/* @__PURE__ */ new Map(), batch.flatMap((entry) => [...entry.changes.values()]));
2057
+ const applied = batch.map(({ shard, row }) => [shard, row.id]);
2058
+ const lease = leaseOp(held, { applied: JSON.stringify(applied) });
2059
+ const ops = [...await rowsFor([...changes.values()]), lease.op];
2060
+ if (!adapter.fits(ops)) continue;
2061
+ for (const { shard, row } of batch) known.set(idOf(shard, row.id), row);
2062
+ return await commit(ops) ? {
2063
+ held: lease.next,
2064
+ count
2065
+ } : void 0;
2066
+ }
2067
+ const [{ shard, row, changes }] = logs;
2068
+ const name = LOG_TABLES[shard];
2069
+ const entries = [...changes.values()];
2070
+ for (let take = entries.length - 1; take > 0; take = Math.floor(take / 2)) {
2071
+ const kept = {
2072
+ type: "patch",
2073
+ table: table(name),
2074
+ key: [String(row.id)],
2075
+ set: { changes: await encode(entries.slice(take)) },
2076
+ guard: { v: Number(row["_v"]) },
2077
+ previous: row
2078
+ };
2079
+ const lease = leaseOp(held, {});
2080
+ const ops = [
2081
+ ...await rowsFor(entries.slice(0, take)),
2082
+ kept,
2083
+ lease.op
2084
+ ];
2085
+ if (adapter.fits(ops)) return await commit(ops) ? {
2086
+ held: lease.next,
2087
+ count: 0
2088
+ } : void 0;
2089
+ }
2090
+ throw new DatabaseTransactionError(`${name}: one aggregate change does not fit in one write.`);
2091
+ };
2092
+ /**
2093
+ * Applies up to `groups` groups under the lease, deleting each group's log
2094
+ * rows before the next; the next compaction is due at once when some are
2095
+ * left. The lease release clears the applied list once its rows are gone.
2096
+ */
2097
+ const run = async (force, groups) => {
2098
+ let held = await takeLease(force);
2099
+ if (held === void 0) return;
2100
+ let pending = true;
2101
+ let clear = false;
2102
+ try {
2103
+ for (let group = 0;; group += 1) {
2104
+ clear = await deleteApplied(held);
2105
+ if (!clear || !pending || group === groups) break;
2106
+ const { logs, more } = await readLogs();
2107
+ if (logs.length === 0) {
2108
+ pending = false;
2109
+ break;
2110
+ }
2111
+ const applied = await applyGroup(held, logs);
2112
+ if (applied === void 0) break;
2113
+ [held, clear] = [applied.held, false];
2114
+ pending = more || applied.count < logs.length;
2115
+ }
2116
+ } finally {
2117
+ nextDue = now() + (pending ? 0 : windowMs);
2118
+ const set = {
2119
+ until: 0,
2120
+ next: nextDue,
2121
+ ...clear ? { applied: "[]" } : {}
2122
+ };
2123
+ await commit([leaseOp(held, set).op]);
2124
+ known.clear();
2125
+ }
2126
+ };
2127
+ /** One compaction at a time in this process; never rejects. */
2128
+ const compact = (force, groups) => compacting ??= run(force, groups).catch(warn("compaction")).finally(() => {
2129
+ compacting = void 0;
2130
+ });
2131
+ let checking;
2132
+ /** Whether any log row waits; reads at once share one check. */
2133
+ const hasPending = () => checking ??= Promise.all(LOG_TABLES.map((name) => engine.reads.findMany(name, {
2134
+ index: "oldest",
2135
+ limit: 1
2136
+ }))).then((pages) => pages.some((page) => page.rows.length > 0)).finally(() => {
2137
+ checking = void 0;
2138
+ });
2139
+ /** Takes a committed transaction's changes; never rejects, since the transaction already committed. */
2140
+ const committed = async (changes) => {
2141
+ if (mode === "log") {
2142
+ if (now() >= nextDue) await compact(false, GROUPS.commit);
2143
+ return;
2144
+ }
2145
+ mergeAll(buffer, changes.values());
2146
+ schedule();
2147
+ if (buffer.size >= MAX_BUFFERED) await flushBuffer();
2148
+ };
2149
+ const flush = async () => {
2150
+ await flushBuffer();
2151
+ if (mode === "log" && await hasPending()) await compact(true, GROUPS.flush);
2152
+ };
2153
+ return {
2154
+ async transaction(fn) {
2155
+ let changes = /* @__PURE__ */ new Map();
2156
+ const result = await engine.transaction(async (tx) => {
2157
+ const attempt = /* @__PURE__ */ new Map();
2158
+ changes = attempt;
2159
+ let open = true;
2160
+ let failure;
2161
+ const aggregate = (name, identity, values, options) => {
2162
+ const model = schema.models.get(name);
2163
+ if (!isBatched(model)) return tx.aggregate(name, identity, values, options);
2164
+ try {
2165
+ if (!open) throw new DatabaseTransactionError("The transaction has ended.");
2166
+ recordAggregate(attempt, model, identity, values, options?.shardBy);
2167
+ } catch (error) {
2168
+ failure ??= { error };
2169
+ throw error;
2170
+ }
2171
+ };
2172
+ try {
2173
+ const value = await fn({
2174
+ ...tx,
2175
+ aggregate
2176
+ });
2177
+ if (failure) throw failure.error;
2178
+ if (mode === "log" && attempt.size > 0) {
2179
+ const id = `${now().toString(36).padStart(9, "0")}${holder.slice(0, 8)}${crypto.randomUUID().slice(0, 8)}`;
2180
+ const shard = Math.floor(Math.random() * LOG_TABLES.length);
2181
+ const merged = mergeAll(/* @__PURE__ */ new Map(), attempt.values());
2182
+ tx.create(LOG_TABLES[shard], {
2183
+ id,
2184
+ changes: await encode(merged.values())
2185
+ });
2186
+ }
2187
+ return value;
2188
+ } finally {
2189
+ open = false;
2190
+ }
2191
+ });
2192
+ if (changes.size > 0) await committed(changes);
2193
+ return result;
2194
+ },
2195
+ async beforeRead(name) {
2196
+ if (!isBatched(schema.models.get(name))) return;
2197
+ if (buffer.size > 0 || flushing !== void 0) await flushBuffer();
2198
+ if (mode === "log") {
2199
+ if (await hasPending().catch((error) => {
2200
+ warn("read")(error);
2201
+ return false;
2202
+ })) await compact(true, GROUPS.read);
2203
+ }
2204
+ },
2205
+ flush,
2206
+ async dispose() {
2207
+ clearTimeout(timer);
2208
+ timer = void 0;
2209
+ await flush();
2210
+ }
2211
+ };
2212
+ };
2213
+ var DatabaseQueryError = class extends Error {
2214
+ name = "DatabaseQueryError";
2215
+ };
2216
+ var sameFields = (left, right) => left.length === right.length && left.every((field) => right.includes(field));
2217
+ var keyValue = (value, field) => {
2218
+ if (isKeyValue(value)) return value;
2219
+ throw new DatabaseQueryError(`${field} needs a string, number, or boolean.`);
2220
+ };
2221
+ var bound = (exclusive, inclusive, columns) => {
2222
+ if (exclusive !== void 0 && inclusive !== void 0) throw new DatabaseQueryError(`range on ${columns[0]} sets both bounds of one side.`);
2223
+ const value = exclusive ?? inclusive;
2224
+ if (value === void 0) return void 0;
2225
+ const values = Array.isArray(value) ? value : [value];
2226
+ if (values.length === 0 || values.length > columns.length) throw new DatabaseQueryError(`range on ${columns[0]} takes 1–${columns.length} values of ${columns.join(", ")}.`);
2227
+ return {
2228
+ values: values.map((item, position) => keyValue(item, columns[position])),
2229
+ inclusive: inclusive !== void 0
2230
+ };
2231
+ };
2232
+ var createEngineReads = ({ adapter, schema, maxPageSize: requested = 500 }) => {
2233
+ const maxPageSize = Math.min(requested, 500);
2234
+ let count = {
2235
+ calls: 0,
2236
+ rows: 0
2237
+ };
2238
+ const returned = (rows) => {
2239
+ const size = Array.isArray(rows) ? rows.length : rows === null ? 0 : 1;
2240
+ count = {
2241
+ calls: count.calls + 1,
2242
+ rows: count.rows + size
2243
+ };
2244
+ return rows;
2245
+ };
2246
+ /** A model of `kind` by physical table name, for reads and transactions. */
2247
+ const modelOf = (name, kind = "table") => {
2248
+ const model = schema.models.get(name);
2249
+ if (model?.definition.kind === kind) return model;
2250
+ throw new DatabaseQueryError(model === void 0 ? `Unknown model ${name}.` : kind === "table" ? `${name} is an aggregate; read it with findAggregates and change it with aggregate.` : `${name} is a table; read it with findMany and change it with create, update, or delete.`);
2251
+ };
2252
+ const plan = ({ table }, input) => {
2253
+ const index = table.indexes.find(({ name }) => name === input.index);
2254
+ if (index === void 0) throw new DatabaseQueryError(`${table.name} has no index ${input.index}.`);
2255
+ const where = input.where ?? {};
2256
+ if (!sameFields(Object.keys(where), index.eq)) throw new DatabaseQueryError(`${table.name}.${index.name} reads need where on exactly: ${index.eq.join(", ") || "(none)"}.`);
2257
+ const { limit, order = "asc", range } = input;
2258
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > maxPageSize) throw new DatabaseQueryError(`limit must be 1–${maxPageSize}.`);
2259
+ const columns = indexOrderColumns(table, index);
2260
+ const eq = index.eq.map((field) => keyValue(where[field], `${table.name}.${field}`));
2261
+ const scope = cursorScope([
2262
+ table.name,
2263
+ index.name,
2264
+ eq,
2265
+ order,
2266
+ range
2267
+ ]);
2268
+ let lower = bound(range?.gt, range?.gte, columns);
2269
+ let upper = bound(range?.lt, range?.lte, columns);
2270
+ if (input.cursor !== void 0) {
2271
+ const values = decodeCursor(scope, input.cursor);
2272
+ if (order === "asc") lower = {
2273
+ values,
2274
+ inclusive: false
2275
+ };
2276
+ else upper = {
2277
+ values,
2278
+ inclusive: false
2279
+ };
2280
+ }
2281
+ return {
2282
+ request: {
2283
+ index: index.name,
2284
+ eq,
2285
+ order,
2286
+ limit,
2287
+ ...lower === void 0 ? {} : { lower },
2288
+ ...upper === void 0 ? {} : { upper }
2289
+ },
2290
+ index,
2291
+ scope
2292
+ };
2293
+ };
2294
+ /** Reads rows by key in one batch; the results follow the keys' order. */
2295
+ const findByKeys = async (name, lookups) => {
2296
+ const { table } = modelOf(name, "table");
2297
+ const keys = lookups.map((lookup) => {
2298
+ if (!sameFields(Object.keys(lookup), table.key)) throw new DatabaseQueryError(`findByKeys on ${name} takes its key: ${table.key.join(", ")}.`);
2299
+ return table.key.map((field) => keyValue(lookup[field], `${name}.${field}`));
2300
+ });
2301
+ const rows = keys.length === 0 ? [] : await adapter.get(table, keys);
2302
+ returned(rows.filter((row) => row !== null));
2303
+ return rows.map((row) => row ?? null);
2304
+ };
2305
+ return {
2306
+ maxPageSize,
2307
+ modelOf,
2308
+ reads: {
2309
+ total: () => count,
2310
+ reset: () => {
2311
+ count = {
2312
+ calls: 0,
2313
+ rows: 0
2314
+ };
2315
+ }
2316
+ },
2317
+ findByKeys,
2318
+ async findOne(name, lookup) {
2319
+ const { table } = modelOf(name, "table");
2320
+ const fields = Object.keys(lookup);
2321
+ if (sameFields(fields, table.key)) return (await findByKeys(name, [lookup]))[0];
2322
+ const index = table.indexes.find(({ unique, eq }) => unique && sameFields(fields, eq));
2323
+ if (index === void 0) throw new DatabaseQueryError(`findOne on ${name} needs its key or a unique field, not ${fields.join(", ")}.`);
2324
+ const [row] = await adapter.query(table, {
2325
+ index: index.name,
2326
+ eq: index.eq.map((field) => keyValue(lookup[field], `${name}.${field}`)),
2327
+ order: "asc",
2328
+ limit: 1
2329
+ });
2330
+ return returned(row ?? null);
2331
+ },
2332
+ async findMany(name, input) {
2333
+ const model = modelOf(name, "table");
2334
+ const { request, index, scope } = plan(model, input);
2335
+ const rows = returned(await adapter.query(model.table, request));
2336
+ const last = rows.at(-1);
2337
+ return rows.length === input.limit && last !== void 0 ? {
2338
+ rows,
2339
+ next: encodeCursor(scope, indexOrderTuple(model.table, index, last))
2340
+ } : { rows };
2341
+ },
2342
+ async findAggregates(name, input) {
2343
+ const model = modelOf(name, "aggregate");
2344
+ const { table, definition } = model;
2345
+ if (definition.kind !== "aggregate") throw new DatabaseQueryError(name);
2346
+ const { request, index, scope } = plan(model, input);
2347
+ const rows = await adapter.query(table, request);
2348
+ const logicalKey = (row) => definition.key.map((column) => keyValue(row[column], column));
2349
+ const groups = /* @__PURE__ */ new Map();
2350
+ for (const row of rows) {
2351
+ const key = JSON.stringify(logicalKey(row));
2352
+ groups.set(key, [...groups.get(key) ?? [], row]);
2353
+ }
2354
+ const full = rows.length === input.limit;
2355
+ const last = [...groups.values()].at(-1);
2356
+ if (full && last !== void 0) {
2357
+ const seen = new Set(last.map((row) => row[SHARD_COLUMN]));
2358
+ const missing = Array.from({ length: definition.shards }, (_, shard) => shard).filter((shard) => !seen.has(shard));
2359
+ if (missing.length > 0) {
2360
+ const key = logicalKey(last[0]);
2361
+ const rest = await adapter.get(table, missing.map((shard) => [...key, shard]));
2362
+ last.push(...rest.filter((row) => row !== null));
2363
+ }
2364
+ }
2365
+ const merged = [...groups.values()].map((shards) => {
2366
+ const row = Object.fromEntries(definition.key.map((field) => [field, shards[0][field]]));
2367
+ for (const metric of [...definition.counters, ...definition.gauges]) row[metric] = shards.reduce((sum, shard) => sum + Number(shard[metric] ?? 0), 0);
2368
+ for (const metric of definition.distinct) row[metric] = mergeDistinct(shards.map((shard) => shard[metric]));
2369
+ return row;
2370
+ });
2371
+ returned(merged);
2372
+ if (!full || last === void 0) return { rows: merged };
2373
+ return {
2374
+ rows: merged,
2375
+ next: encodeCursor(scope, indexOrderTuple(table, index, last[0]).slice(0, -1))
2376
+ };
2377
+ }
2378
+ };
2379
+ };
2380
+ /** A row this attempt read changed while it ran; the attempt reruns. */
2381
+ var StaleReadError = class extends Error {};
2382
+ var idOf = (table, key) => JSON.stringify([table, key]);
2383
+ var versionOf = (row) => Number(row["_v"]);
2384
+ var isKeyed = (value) => value !== null && value !== void 0;
2385
+ var misuse = (table, message) => new DatabaseTransactionError(`${table}: ${message}`);
2386
+ var once = (table) => misuse(table, "a transaction writes one key once.");
2387
+ var hooks = globalThis.process?.getBuiltinModule?.("node:async_hooks");
2388
+ /** Where the runtime tracks async context, `db.*` inside a transaction throws. */
2389
+ var transactionScope = hooks && new hooks.AsyncLocalStorage();
2390
+ var assertOutsideTransaction = () => {
2391
+ if (transactionScope?.getStore() !== void 0) throw new DatabaseTransactionError("Inside transaction(), read and write through its handle; db and core are not part of it.");
2392
+ };
2393
+ var createTransactions = ({ adapter, schema, reads, retry = {} }) => {
2394
+ const { attempts = 8, baseDelayMs = 5, maxDelayMs = 250, onRetry } = retry;
2395
+ const { modelOf } = reads;
2396
+ /**
2397
+ * A key-value index copy has no `_v`. A transaction guards what it reads,
2398
+ * so it reads such a row whole, and reruns if it changed since the copy.
2399
+ */
2400
+ const complete = async ({ table }, rows) => {
2401
+ const copies = rows.filter((row) => !("_v" in row));
2402
+ if (copies.length === 0) return [...rows];
2403
+ const current = await adapter.get(table, copies.map((copy) => rowKey(table, copy)));
2404
+ return rows.map((row) => {
2405
+ const at = copies.indexOf(row);
2406
+ if (at < 0) return row;
2407
+ const whole = current[at];
2408
+ if (!whole || Object.keys(row).some((column) => canonicalJson(row[column]) !== canonicalJson(whole[column]))) throw new StaleReadError();
2409
+ return whole;
2410
+ });
2411
+ };
2412
+ const attempt = async (fn) => {
2413
+ const read = /* @__PURE__ */ new Map();
2414
+ /** Rows of rooted ranges: their root guards them, so they need no check of their own. */
2415
+ const ranged = /* @__PURE__ */ new Map();
2416
+ const writes = /* @__PURE__ */ new Map();
2417
+ /** Deleted keys, children before their parents. */
2418
+ const deleted = [];
2419
+ const aggregates = /* @__PURE__ */ new Map();
2420
+ let open = true;
2421
+ let failure;
2422
+ /** A failed call fails the attempt even when `fn` catches it. */
2423
+ const step = (method) => (...args) => {
2424
+ if (!open) throw new DatabaseTransactionError("The transaction has ended.");
2425
+ const fail = (error) => {
2426
+ failure ??= { error };
2427
+ throw error;
2428
+ };
2429
+ try {
2430
+ const result = method(...args);
2431
+ return result instanceof Promise ? result.catch(fail) : result;
2432
+ } catch (error) {
2433
+ return fail(error);
2434
+ }
2435
+ };
2436
+ const remember = (model, key, row) => {
2437
+ const id = idOf(model.table.name, key);
2438
+ const known = read.get(id);
2439
+ if (known !== void 0 && (known && row ? versionOf(known) !== versionOf(row) : known !== row)) throw new StaleReadError();
2440
+ read.set(id, row);
2441
+ return row;
2442
+ };
2443
+ const readRow = ({ table }, row) => {
2444
+ const id = idOf(table.name, rowKey(table, row));
2445
+ const known = read.get(id) ?? ranged.get(id);
2446
+ if (known && versionOf(known) === versionOf(row)) return known;
2447
+ throw misuse(table.name, "update and delete take a row this transaction read.");
2448
+ };
2449
+ const checkFields = ({ table, definition: { fields } }, values, creating) => {
2450
+ for (const field of Object.keys(values)) if (!(field in fields) || !creating && table.key.includes(field)) throw misuse(table.name, `${field} cannot be ${creating ? "set" : "changed"}.`);
2451
+ for (const [field, { required }] of Object.entries(fields)) if (required !== false && (creating || field in values) && !isKeyed(values[field])) throw misuse(table.name, `${field} is required.`);
2452
+ };
2453
+ /** Adds counter deltas to a key's pending op; an empty `by` still bumps `_v`. */
2454
+ const bump = (model, key, by) => {
2455
+ const id = idOf(model.table.name, key);
2456
+ const pending = writes.get(id);
2457
+ if (pending === void 0) writes.set(id, {
2458
+ model,
2459
+ key,
2460
+ type: "increment",
2461
+ row: { ...by }
2462
+ });
2463
+ else if (pending.type !== "delete") for (const [column, delta] of Object.entries(by)) pending.row[column] = Number(pending.row[column] ?? pending.read?.[column] ?? 0) + delta;
2464
+ else if (Object.values(by).some((delta) => delta > 0)) throw new DatabaseConstraintError("not_found", model.table.name);
2465
+ };
2466
+ /** Moves parent counters when a reference changes and bumps every rooted parent. */
2467
+ const touchParents = (model, before, after) => {
2468
+ for (const { field, counter, target } of model.references) {
2469
+ const [from, to] = [before?.[field], after?.[field]];
2470
+ if (counter === void 0 || from === to) continue;
2471
+ if (isKeyed(from)) bump(modelOf(target), [from], { [counter]: -1 });
2472
+ if (isKeyed(to)) bump(modelOf(target), [to], { [counter]: 1 });
2473
+ }
2474
+ for (const [index, root] of model.roots) {
2475
+ const parent = modelOf(root);
2476
+ const { eq } = model.table.indexes.find(({ name }) => name === index);
2477
+ for (const row of [before, after]) {
2478
+ const key = eq.slice(0, parent.table.key.length).map((field) => row?.[field]);
2479
+ if (row && key.every(isKeyed)) bump(parent, key, {});
2480
+ }
2481
+ }
2482
+ };
2483
+ const derive = ({ definition }, row) => {
2484
+ if (definition.kind === "table") for (const [name, derived] of Object.entries(definition.derived)) row[name] = derived.compute(row);
2485
+ return row;
2486
+ };
2487
+ /** Deletes a row after its cascaded children, or refuses a restricted delete. */
2488
+ const remove = async (model, known) => {
2489
+ const { table } = model;
2490
+ const key = rowKey(table, known);
2491
+ const id = idOf(table.name, key);
2492
+ const pending = writes.get(id);
2493
+ if (pending?.type === "delete") return;
2494
+ if (pending && pending.type !== "increment") throw once(table.name);
2495
+ writes.set(id, {
2496
+ model,
2497
+ key,
2498
+ type: "delete",
2499
+ row: {},
2500
+ read: known
2501
+ });
2502
+ touchParents(model, known, null);
2503
+ for (const reference of model.referencedBy) {
2504
+ const { counter, field, onDelete, table: name } = reference;
2505
+ if (counter === void 0) continue;
2506
+ const stored = Number(known[counter] ?? 0);
2507
+ const added = Number(pending?.row[counter] ?? 0);
2508
+ if (added > 0) throw new DatabaseConstraintError("referenced", table.name);
2509
+ if (stored + added <= 0) continue;
2510
+ if (onDelete === "restrict") {
2511
+ const [current] = await adapter.get(table, [key]);
2512
+ if (current && versionOf(current) === versionOf(known)) throw new DatabaseConstraintError("referenced", table.name);
2513
+ throw new StaleReadError();
2514
+ }
2515
+ const child = modelOf(name);
2516
+ const index = child.table.indexes.find(({ eq }) => eq.length === 1 && eq[0] === field);
2517
+ let cursor;
2518
+ let seen = 0;
2519
+ do {
2520
+ const page = await reads.findMany(name, {
2521
+ index: index.name,
2522
+ where: { [field]: key[0] },
2523
+ limit: Math.min(stored - seen, reads.maxPageSize),
2524
+ ...cursor === void 0 ? {} : { cursor }
2525
+ });
2526
+ for (const row of await complete(child, page.rows)) {
2527
+ remember(child, rowKey(child.table, row), row);
2528
+ await remove(child, row);
2529
+ }
2530
+ seen += page.rows.length;
2531
+ cursor = page.next;
2532
+ } while (cursor !== void 0 && seen < stored);
2533
+ }
2534
+ deleted.push(id);
2535
+ };
2536
+ const tx = {
2537
+ findOne: step(async (name, lookup) => {
2538
+ const model = modelOf(name);
2539
+ const { table } = model;
2540
+ const found = await reads.findOne(name, lookup);
2541
+ if (found === null && !table.key.every((field) => field in lookup)) return null;
2542
+ const [row = null] = found ? await complete(model, [found]) : [];
2543
+ return remember(model, row ? rowKey(table, row) : table.key.map((field) => lookup[field]), row);
2544
+ }),
2545
+ findMany: step(async (name, input) => {
2546
+ const model = modelOf(name);
2547
+ const root = model.roots.get(input.index);
2548
+ if (root === void 0) throw new DatabaseQueryError(`${name}.${input.index} is not rooted; a transaction reads a range only under a guarded parent.`);
2549
+ const parent = modelOf(root);
2550
+ const { eq } = model.table.indexes.find(({ name: index }) => index === input.index);
2551
+ const where = input.where ?? {};
2552
+ const lookup = Object.fromEntries(parent.table.key.map((field, at) => [field, where[eq[at]]]));
2553
+ remember(parent, Object.values(lookup), await reads.findOne(root, lookup));
2554
+ const found = await reads.findMany(name, input);
2555
+ const rows = await complete(model, found.rows);
2556
+ for (const row of rows) ranged.set(idOf(name, rowKey(model.table, row)), row);
2557
+ return {
2558
+ ...found,
2559
+ rows
2560
+ };
2561
+ }),
2562
+ create: step((name, values) => {
2563
+ const model = modelOf(name);
2564
+ checkFields(model, values, true);
2565
+ const defaults = model.table.columns.flatMap((column) => column.nullable || column.default !== void 0 ? [[column.name, column.default ?? null]] : []);
2566
+ const row = derive(model, {
2567
+ ...Object.fromEntries(defaults),
2568
+ ...values
2569
+ });
2570
+ const key = rowKey(model.table, row);
2571
+ const id = idOf(name, key);
2572
+ const pending = writes.get(id);
2573
+ if (pending && pending.type !== "increment") throw once(name);
2574
+ for (const [column, delta] of Object.entries(pending?.row ?? {})) row[column] = Number(row[column]) + Number(delta);
2575
+ writes.set(id, {
2576
+ model,
2577
+ key,
2578
+ type: "insert",
2579
+ row
2580
+ });
2581
+ touchParents(model, null, row);
2582
+ }),
2583
+ update: step((name, row, set) => {
2584
+ const model = modelOf(name);
2585
+ const known = readRow(model, row);
2586
+ checkFields(model, set, false);
2587
+ const key = rowKey(model.table, known);
2588
+ const id = idOf(name, key);
2589
+ const pending = writes.get(id);
2590
+ if (pending?.type === "insert" || pending?.type === "delete") throw once(name);
2591
+ const before = pending?.type === "patch" ? {
2592
+ ...known,
2593
+ ...pending.row
2594
+ } : known;
2595
+ const next = derive(model, {
2596
+ ...before,
2597
+ ...set
2598
+ });
2599
+ const changed = Object.entries(next).filter(([column, value]) => column !== "_v" && JSON.stringify(value) !== JSON.stringify(known[column]));
2600
+ const patch = Object.fromEntries(changed);
2601
+ writes.set(id, {
2602
+ model,
2603
+ key,
2604
+ type: "patch",
2605
+ row: patch,
2606
+ read: known
2607
+ });
2608
+ if (pending?.type === "increment") bump(model, key, pending.row);
2609
+ touchParents(model, before, next);
2610
+ }),
2611
+ delete: step(async (name, row) => {
2612
+ const model = modelOf(name);
2613
+ await remove(model, readRow(model, row));
2614
+ }),
2615
+ aggregate: step((name, identity, values, options) => {
2616
+ recordAggregate(aggregates, modelOf(name, "aggregate"), identity, values, options?.shardBy);
2617
+ })
2618
+ };
2619
+ try {
2620
+ const result = await (transactionScope ? transactionScope.run(tx, () => fn(tx)) : fn(tx));
2621
+ if (failure) throw failure.error;
2622
+ return {
2623
+ result,
2624
+ read,
2625
+ ops: compile(writes, read, deleted),
2626
+ aggregates: [...aggregates.values()]
2627
+ };
2628
+ } catch (error) {
2629
+ throw failure?.error instanceof StaleReadError ? failure.error : error;
2630
+ } finally {
2631
+ open = false;
2632
+ }
2633
+ };
2634
+ /** Inserts first, then patches and increments, then checks, then deletes, children before parents. */
2635
+ const compile = (writes, read, deleted) => {
2636
+ const toOp = (id) => {
2637
+ const { model, key, type, row, read: previous } = writes.get(id);
2638
+ const { table } = model;
2639
+ if (type === "insert") return {
2640
+ type,
2641
+ table,
2642
+ row
2643
+ };
2644
+ if (type === "increment") {
2645
+ const known = read.get(id);
2646
+ const by = row;
2647
+ return known ? {
2648
+ type,
2649
+ table,
2650
+ key,
2651
+ by,
2652
+ guard: { v: versionOf(known) }
2653
+ } : {
2654
+ type,
2655
+ table,
2656
+ key,
2657
+ by
2658
+ };
2659
+ }
2660
+ const guard = { v: versionOf(previous) };
2661
+ return type === "patch" ? {
2662
+ type,
2663
+ table,
2664
+ key,
2665
+ set: row,
2666
+ guard,
2667
+ previous
2668
+ } : {
2669
+ type,
2670
+ table,
2671
+ key,
2672
+ guard,
2673
+ previous
2674
+ };
2675
+ };
2676
+ const checks = [...read].flatMap(([id, row]) => {
2677
+ if (row === null || writes.has(id)) return [];
2678
+ const { table } = schema.models.get(JSON.parse(id)[0]);
2679
+ const guard = { v: versionOf(row) };
2680
+ return [{
2681
+ type: "check",
2682
+ table,
2683
+ key: rowKey(table, row),
2684
+ guard
2685
+ }];
2686
+ });
2687
+ const ofType = (...types) => [...writes.keys()].filter((id) => types.includes(writes.get(id).type));
2688
+ return [
2689
+ ...[...ofType("insert"), ...ofType("patch", "increment")].map(toOp),
2690
+ ...checks,
2691
+ ...deleted.map(toOp)
2692
+ ];
2693
+ };
2694
+ /** Only a constraint the current state confirms is reported; anything else reruns. */
2695
+ const classify = async (op, read) => {
2696
+ if (op.type === "check" || op.type === "delete") return void 0;
2697
+ if (op.type === "increment" && op.guard) return void 0;
2698
+ const { table } = op;
2699
+ const key = op.type === "insert" ? rowKey(table, op.row) : op.key;
2700
+ const [current] = await adapter.get(table, [key]);
2701
+ if (op.type === "increment") return current ? void 0 : "not_found";
2702
+ if (op.type === "insert" && current) {
2703
+ const known = read.get(idOf(table.name, key));
2704
+ return known === null || known !== void 0 && versionOf(known) !== versionOf(current) ? void 0 : "exists";
2705
+ }
2706
+ if (op.type === "patch" && current?.["_v"] !== op.guard.v) return;
2707
+ const row = op.type === "insert" ? op.row : {
2708
+ ...current,
2709
+ ...op.set
2710
+ };
2711
+ for (const index of table.indexes) {
2712
+ if (!index.unique) continue;
2713
+ for (const eq of indexEntries(table, index, row)) if ((await adapter.query(table, {
2714
+ index: index.name,
2715
+ eq,
2716
+ order: "asc",
2717
+ limit: 2
2718
+ })).some((other) => compareTuples(rowKey(table, other), key))) return "unique";
2719
+ }
2720
+ };
2721
+ return { async transaction(fn) {
2722
+ assertOutsideTransaction();
2723
+ let run;
2724
+ let negative;
2725
+ for (let round = 0; round < attempts; round += 1) {
2726
+ const delay = round && Math.min(maxDelayMs, baseDelayMs * 2 ** (round - 1));
2727
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay / 2 + Math.random() * (delay / 2)));
2728
+ run ??= await attempt(fn).catch((error) => {
2729
+ if (error instanceof StaleReadError) return void 0;
2730
+ throw error;
2731
+ });
2732
+ if (run === void 0) {
2733
+ onRetry?.("rerun", round + 1);
2734
+ continue;
2735
+ }
2736
+ let aggregates;
2737
+ try {
2738
+ aggregates = await compileAggregates(adapter, run.aggregates);
2739
+ } catch (error) {
2740
+ if (!(error instanceof NegativeGaugeError)) throw error;
2741
+ negative = error;
2742
+ onRetry?.("rerun", round + 1);
2743
+ run = void 0;
2744
+ continue;
2745
+ }
2746
+ const ops = [...run.ops, ...aggregates];
2747
+ if (ops.length <= 1 && ops.every(({ type }) => type === "check")) return run.result;
2748
+ if (!adapter.fits(ops)) throw new DatabaseConstraintError("too_large", ops[0].table.name);
2749
+ let outcome;
2750
+ try {
2751
+ outcome = await adapter.write(ops);
2752
+ } catch (cause) {
2753
+ throw new DatabaseAmbiguousCommitError("The commit outcome is unknown.", { cause });
2754
+ }
2755
+ if (outcome.ok) return run.result;
2756
+ const failed = "failedOp" in outcome ? outcome.failedOp : void 0;
2757
+ if (failed === void 0 || failed >= run.ops.length) {
2758
+ onRetry?.("resend", round + 1);
2759
+ continue;
2760
+ }
2761
+ const reason = await classify(ops[failed], run.read);
2762
+ if (reason) throw new DatabaseConstraintError(reason, ops[failed].table.name);
2763
+ onRetry?.("rerun", round + 1);
2764
+ run = void 0;
2765
+ }
2766
+ throw negative ?? new DatabaseConflictError(`The transaction conflicted ${attempts} times.`);
2767
+ } };
2768
+ };
2769
+ /** Storage semantics over one adapter, addressed by physical table name. */
2770
+ var createEngine = ({ adapter: given, schema, maxPageSize, verify, retry }) => {
2771
+ const verified = verify ? verifyAdapter(given) : void 0;
2772
+ const adapter = verified ?? given;
2773
+ const reads = createEngineReads({
2774
+ adapter,
2775
+ schema,
2776
+ maxPageSize
2777
+ });
2778
+ const { transaction } = createTransactions({
2779
+ adapter,
2780
+ schema,
2781
+ reads,
2782
+ retry
2783
+ });
2784
+ return {
2785
+ reads,
2786
+ transaction,
2787
+ async measureReads(read) {
2788
+ if (verified === void 0) throw new Error("measureReads needs an engine created with verify: true.");
2789
+ verified.reads.reset();
2790
+ reads.reads.reset();
2791
+ return {
2792
+ result: await read(),
2793
+ adapter: verified.reads.total(),
2794
+ engine: reads.reads.total()
2795
+ };
2796
+ }
2797
+ };
2798
+ };
2799
+ /** The engine plus `database(module)`, which hands a module its typed handle. */
2800
+ var createDatabaseEngine = ({ batching, now = Date.now, ...options }) => {
2801
+ const engine = createEngine(options);
2802
+ const { reads } = engine;
2803
+ const batches = batching && createAggregateBatches({
2804
+ engine,
2805
+ adapter: options.adapter,
2806
+ schema: options.schema,
2807
+ batching,
2808
+ now
2809
+ });
2810
+ const transaction = batches?.transaction ?? engine.transaction;
2811
+ const outside = (read) => (...args) => {
2812
+ assertOutsideTransaction();
2813
+ return read(...args);
2814
+ };
2815
+ /** No read outside a transaction returns `_v`, which a key-value index copy lacks. */
2816
+ const bare = (row) => row && Object.fromEntries(Object.entries(row).filter(([column]) => column !== "_v"));
2817
+ const barePage = async (page) => {
2818
+ const { rows, next } = await page;
2819
+ return {
2820
+ rows: rows.map(bare),
2821
+ ...next === void 0 ? {} : { next }
2822
+ };
2823
+ };
2824
+ return {
2825
+ ...engine,
2826
+ flush: async () => {
2827
+ await batches?.flush();
2828
+ },
2829
+ dispose: async () => {
2830
+ await batches?.dispose();
2831
+ },
2832
+ database(module) {
2833
+ const name = (model) => module.namespace ? `${module.namespace}_${model}` : model;
2834
+ return {
2835
+ findOne: outside(async (model, lookup) => bare(await reads.findOne(name(model), lookup))),
2836
+ findByKeys: outside(async (model, keys) => (await reads.findByKeys(name(model), keys)).map(bare)),
2837
+ findMany: outside((model, input) => barePage(reads.findMany(name(model), input))),
2838
+ findAggregates: outside(async (model, input) => {
2839
+ await batches?.beforeRead(name(model));
2840
+ return await barePage(reads.findAggregates(name(model), input));
2841
+ }),
2842
+ transaction: (fn) => transaction((tx) => fn({
2843
+ findOne: (model, lookup) => tx.findOne(name(model), lookup),
2844
+ findMany: (model, input) => tx.findMany(name(model), input),
2845
+ create: (model, row) => tx.create(name(model), row),
2846
+ update: (model, row, set) => tx.update(name(model), row, set),
2847
+ delete: (model, row) => tx.delete(name(model), row),
2848
+ aggregate: (model, identity, changes, options) => tx.aggregate(name(model), identity, changes, options)
2849
+ }))
2850
+ };
2851
+ }
2852
+ };
2853
+ };
2854
+ var HBC_ASSET_PATH_RE = /\.bundle$/;
2855
+ var asByteSize = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
2856
+ var resolveUniqueHbcAssetPath = (manifest) => {
2857
+ const candidates = Object.keys(manifest.assets).sort((left, right) => left.localeCompare(right)).filter((candidate) => HBC_ASSET_PATH_RE.test(candidate));
2858
+ return candidates.length === 1 ? candidates[0] : null;
2859
+ };
2860
+ var isBundleManifest = (value) => {
2861
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2862
+ const manifest = value;
2863
+ if (typeof manifest.bundleId !== "string") return false;
2864
+ if (!manifest.assets || typeof manifest.assets !== "object") return false;
2865
+ return Object.values(manifest.assets).every((asset) => {
2866
+ if (!asset || typeof asset !== "object" || Array.isArray(asset)) return false;
2867
+ const manifestAsset = asset;
2868
+ return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
2869
+ });
2870
+ };
2871
+ async function fetchBundleManifest(storageUri, readStorageText) {
2872
+ const storageText = await readStorageText(storageUri);
2873
+ if (storageText === null) return null;
2874
+ let payload;
2875
+ try {
2876
+ payload = JSON.parse(storageText);
2877
+ } catch {
2878
+ return null;
2879
+ }
2880
+ if (!isBundleManifest(payload)) return null;
2881
+ return payload;
2882
+ }
2883
+ function resolveHbcPatchPlan({ currentBundle, targetBundle, targetManifest }) {
2884
+ const matchingPatch = currentBundle ? getBundlePatch(targetBundle, currentBundle.id) : null;
2885
+ const patchAssetPath = resolveUniqueHbcAssetPath(targetManifest);
2886
+ if (!currentBundle || !matchingPatch || !patchAssetPath || !matchingPatch.patchStorageUri || !matchingPatch.patchFileHash || !matchingPatch.baseFileHash) return null;
2887
+ return {
2888
+ assetPath: patchAssetPath,
2889
+ patch: {
2890
+ baseBundleId: matchingPatch.baseBundleId,
2891
+ baseFileHash: matchingPatch.baseFileHash,
2892
+ byteSize: asByteSize(matchingPatch.byteSize),
2893
+ patchFileHash: matchingPatch.patchFileHash,
2894
+ storageUri: matchingPatch.patchStorageUri
2895
+ }
2896
+ };
2897
+ }
2898
+ function createManifestArtifactPlan({ assetBaseStorageUri, currentBundle, targetBundle, targetManifest }) {
2899
+ const patchCandidate = resolveHbcPatchPlan({
2900
+ currentBundle,
2901
+ targetBundle,
2902
+ targetManifest
2903
+ });
2904
+ return { assets: Object.entries(targetManifest.assets).map(([assetPath, asset]) => {
2905
+ const downloadByteSize = asByteSize(asset.downloadByteSize);
2906
+ const downloadPath = getManifestAssetDownloadPath(assetPath);
2907
+ const isTransformedDownload = downloadPath !== assetPath;
2908
+ const hasValidDownloadFileHash = isContentAddressedAssetFileHash(asset.downloadFileHash);
2909
+ const file = {
2910
+ byteSize: downloadByteSize !== null && (hasValidDownloadFileHash || !isTransformedDownload && asset.downloadFileHash === void 0) ? downloadByteSize : null,
2911
+ storageUri: resolveManifestAssetStorageUri({
2912
+ assetBaseStorageUri,
2913
+ assetPath: downloadPath,
2914
+ downloadFileHash: asset.downloadFileHash,
2915
+ fileHash: asset.fileHash
2916
+ })
2917
+ };
2918
+ if (isTransformedDownload) file.compression = "br";
2919
+ const patch = patchCandidate?.assetPath === assetPath ? patchCandidate.patch : null;
2920
+ return {
2921
+ assetPath,
2922
+ file,
2923
+ fileHash: asset.fileHash,
2924
+ patch
2925
+ };
2926
+ }) };
2927
+ }
2928
+ async function resolveAssets(plan, resolveFileUrl) {
2929
+ const patchAsset = plan.assets.find((asset) => asset.patch !== null);
2930
+ let resolvedPatch = null;
2931
+ if (patchAsset?.patch) {
2932
+ const patchUrl = await resolveFileUrl(patchAsset.patch.storageUri);
2933
+ if (patchUrl) resolvedPatch = {
2934
+ assetPath: patchAsset.assetPath,
2935
+ patch: {
2936
+ algorithm: "bsdiff",
2937
+ baseBundleId: patchAsset.patch.baseBundleId,
2938
+ baseFileHash: patchAsset.patch.baseFileHash,
2939
+ patchFileHash: patchAsset.patch.patchFileHash,
2940
+ patchUrl,
2941
+ ...patchAsset.patch.byteSize === null ? {} : { byteSize: patchAsset.patch.byteSize }
2942
+ }
2943
+ };
2944
+ }
2945
+ const entries = await Promise.all(plan.assets.map(async (asset) => {
2946
+ let patch = resolvedPatch?.assetPath === asset.assetPath ? resolvedPatch.patch : null;
2947
+ let fileUrl = null;
2948
+ try {
2949
+ fileUrl = await resolveFileUrl(asset.file.storageUri);
2950
+ } catch (error) {
2951
+ if (!patch) throw error;
2952
+ }
2953
+ if (fileUrl && patch && asset.file.byteSize !== null && asset.patch !== null && asset.patch.byteSize !== null && asset.patch.byteSize >= asset.file.byteSize) patch = null;
2954
+ if (!fileUrl) return null;
2955
+ const changedAsset = {
2956
+ file: { url: fileUrl },
2957
+ fileHash: asset.fileHash
2958
+ };
2959
+ if (asset.file.compression) changedAsset.file.compression = asset.file.compression;
2960
+ if (patch) changedAsset.patch = patch;
2961
+ return { asset: [asset.assetPath, changedAsset] };
2962
+ }));
2963
+ if (entries.some((entry) => entry === null)) return null;
2964
+ const resolvedEntries = entries.filter((entry) => entry !== null);
2965
+ return { assets: Object.fromEntries(resolvedEntries.map((entry) => entry.asset)) };
2966
+ }
2967
+ async function resolveManifestArtifacts({ currentBundle, resolveFileUrl, readStorageText, targetBundle }) {
2968
+ const manifestStorageUri = targetBundle ? getManifestStorageUri(targetBundle) : null;
2969
+ const manifestFileHash = targetBundle ? getManifestFileHash(targetBundle) : null;
2970
+ const assetBaseStorageUri = targetBundle ? getAssetBaseStorageUri(targetBundle) : null;
2971
+ if (!targetBundle || !manifestStorageUri || !manifestFileHash || !assetBaseStorageUri) return null;
2972
+ const targetManifest = await fetchBundleManifest(manifestStorageUri, readStorageText);
2973
+ if (!targetManifest) return null;
2974
+ const plan = createManifestArtifactPlan({
2975
+ assetBaseStorageUri,
2976
+ currentBundle,
2977
+ targetBundle,
2978
+ targetManifest
2979
+ });
2980
+ const manifestUrl = await resolveFileUrl(manifestStorageUri);
2981
+ if (!manifestUrl) return null;
2982
+ const resolved = await resolveAssets(plan, resolveFileUrl);
2983
+ if (!resolved) return null;
2984
+ let archiveUrl = null;
2985
+ const archive = targetManifest.archive;
2986
+ if (archive && isContentAddressedAssetFileHash(archive.downloadFileHash) && (asByteSize(archive.downloadByteSize) ?? 0) > 0 && (asByteSize(archive.tarByteSize) ?? 0) > 0) try {
2987
+ archiveUrl = await resolveFileUrl(getBundleArchiveStorageUri({
2988
+ manifestStorageUri,
2989
+ bundleId: targetBundle.id
2990
+ }));
2991
+ } catch {}
2992
+ return {
2993
+ artifactProtocolVersion: 1,
2994
+ assets: resolved.assets,
2995
+ manifestFileHash,
2996
+ manifestUrl,
2997
+ ...archiveUrl ? { archiveUrl } : {}
2998
+ };
2999
+ }
3000
+ var PAGE$2 = 500;
3001
+ var pick = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
3002
+ var toBundleRow = pick([
3003
+ "id",
3004
+ "platform",
3005
+ "git_commit_hash",
3006
+ "metadata",
3007
+ "manifest_storage_uri",
3008
+ "manifest_file_hash",
3009
+ "asset_base_storage_uri"
3010
+ ]);
3011
+ var toPatchRow = pick([
3012
+ "id",
3013
+ "bundle_id",
3014
+ "base_bundle_id",
3015
+ "base_file_hash",
3016
+ "patch_file_hash",
3017
+ "patch_storage_uri",
3018
+ "byte_size",
3019
+ "order_index"
3020
+ ]);
3021
+ var toReleaseRow = pick([
3022
+ "id",
3023
+ "revision",
3024
+ "scope_key",
3025
+ "channel_id",
3026
+ "platform",
3027
+ "kind",
3028
+ "bundle_id",
3029
+ "strategy",
3030
+ "target_app_version",
3031
+ "fingerprint_hash",
3032
+ "enabled",
3033
+ "should_force_update",
3034
+ "message",
3035
+ "rollout_cohort_count",
3036
+ "target_cohorts",
3037
+ "operation",
3038
+ "source_release_id",
3039
+ "created_at_ms",
3040
+ "updated_at_ms"
3041
+ ]);
3042
+ var toCatalogRow = pick([
3043
+ "scope_key",
3044
+ "catalog_id",
3045
+ "strategy",
3046
+ "channel_id",
3047
+ "channel_key",
3048
+ "platform",
3049
+ "fingerprint_hash",
3050
+ "generation",
3051
+ "payload",
3052
+ "catalog_hash",
3053
+ "byte_size",
3054
+ "is_tombstone",
3055
+ "updated_at_ms"
3056
+ ]);
3057
+ var toChannelRow = pick(["id", "name"]);
3058
+ /** Every page of one read, in order. */
3059
+ var drain = async (read) => {
3060
+ const rows = [];
3061
+ let cursor;
3062
+ do {
3063
+ const page = await read(cursor === void 0 ? {} : { cursor });
3064
+ rows.push(...page.rows);
3065
+ cursor = page.next;
3066
+ } while (cursor !== void 0);
3067
+ return rows;
3068
+ };
3069
+ /** Core's reads, each through one declared index or key. */
3070
+ var createCoreReads = (db, storage) => {
3071
+ /** A bundle row's patches, exactly as many as its reference counter holds. */
3072
+ const detail = async (row) => {
3073
+ const count = row._refs_bundle_patches_bundle_id ?? 0;
3074
+ const patches = [];
3075
+ let cursor;
3076
+ while (patches.length < count) {
3077
+ const page = await db.findMany("bundle_patches", {
3078
+ index: "byBundle",
3079
+ where: { bundle_id: row.id },
3080
+ limit: Math.min(count - patches.length, PAGE$2),
3081
+ ...cursor === void 0 ? {} : { cursor }
3082
+ });
3083
+ patches.push(...page.rows.map(toPatchRow));
3084
+ if (page.next === void 0) break;
3085
+ cursor = page.next;
3086
+ }
3087
+ return {
3088
+ bundle: toBundleRow(row),
3089
+ patches,
3090
+ childCount: row._refs_bundle_patches_base_bundle_id ?? 0
3091
+ };
3092
+ };
3093
+ const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
3094
+ return {
3095
+ async getReleaseCatalog(input) {
3096
+ const scopeKey = releaseCatalogScopeKeyOf(input);
3097
+ const row = await db.findOne("release_catalogs", { scope_key: scopeKey });
3098
+ return projectReleaseCatalogRow(row === null ? null : toCatalogRow(row), input, scopeKey);
3099
+ },
3100
+ async getArtifactInfo(targetBundleId, currentBundleId, artifactProtocolVersion) {
3101
+ const { readStorageText } = storage;
3102
+ if (artifactProtocolVersion !== 1 || readStorageText === void 0) return null;
3103
+ const withCurrent = currentBundleId !== NIL_UUID;
3104
+ const [[target, current], patch] = await Promise.all([db.findByKeys("bundles", withCurrent ? [{ id: targetBundleId }, { id: currentBundleId }] : [{ id: targetBundleId }]), withCurrent ? db.findOne("bundle_patches", {
3105
+ bundle_id: targetBundleId,
3106
+ base_bundle_id: currentBundleId
3107
+ }) : null]);
3108
+ if (target === null || target === void 0) return null;
3109
+ return resolveManifestArtifacts({
3110
+ currentBundle: current ? rowToBundle(toBundleRow(current)) : null,
3111
+ readStorageText,
3112
+ resolveFileUrl: storage.resolveFileUrl,
3113
+ targetBundle: rowToBundle(toBundleRow(target), patch === null ? [] : [toPatchRow(patch)])
3114
+ });
3115
+ },
3116
+ async getBundle(id) {
3117
+ const row = await db.findOne("bundles", { id });
3118
+ return row === null ? null : detail(row);
3119
+ },
3120
+ async countBundleChildren(baseBundleIds) {
3121
+ const ids = [...new Set(baseBundleIds)];
3122
+ if (ids.length === 0) return {};
3123
+ const rows = await db.findByKeys("bundles", ids.map((id) => ({ id })));
3124
+ return Object.fromEntries(ids.map((id, position) => [id, rows[position]?._refs_bundle_patches_base_bundle_id ?? 0]));
3125
+ },
3126
+ async listPatchesFromBase(baseBundleId, input) {
3127
+ return (await db.findMany("bundle_patches", {
3128
+ index: "byBase",
3129
+ where: { base_bundle_id: baseBundleId },
3130
+ order: input.order ?? "asc",
3131
+ limit: input.limit,
3132
+ ...after(input)
3133
+ })).rows.map(toPatchRow);
3134
+ },
3135
+ async listBundles(input) {
3136
+ const page = input.platform === void 0 ? await db.findMany("bundles", {
3137
+ index: "all",
3138
+ where: {},
3139
+ order: input.order ?? "asc",
3140
+ limit: input.limit,
3141
+ ...after(input)
3142
+ }) : await db.findMany("bundles", {
3143
+ index: "byPlatform",
3144
+ where: { platform: input.platform },
3145
+ order: input.order ?? "asc",
3146
+ limit: input.limit,
3147
+ ...after(input)
3148
+ });
3149
+ return Promise.all(page.rows.map(detail));
3150
+ },
3151
+ async countBundles(platform) {
3152
+ const { rows } = await db.findAggregates("bundle_totals", {
3153
+ index: "byPlatform",
3154
+ where: { platform_key: platform ?? "*" },
3155
+ limit: 1
3156
+ });
3157
+ return rows[0]?.bundles ?? 0;
3158
+ },
3159
+ async findBaseBundleIds(candidateKey, bundleId, limit) {
3160
+ const query = parseBaseCandidateKey(candidateKey);
3161
+ if (query === null) return [];
3162
+ const row = await db.findOne("release_catalogs", { scope_key: query.scopeKey });
3163
+ return row === null ? [] : baseBundleIdsOf(toCatalogRow(row), query.range, bundleId, limit);
3164
+ },
3165
+ async getRelease(id) {
3166
+ const row = await db.findOne("releases", { id });
3167
+ return row === null ? null : toReleaseRow(row);
3168
+ },
3169
+ async listReleases(input) {
3170
+ const { filter } = input;
3171
+ const options = {
3172
+ order: input.order ?? "asc",
3173
+ limit: input.limit,
3174
+ ...after(input)
3175
+ };
3176
+ return (filter.kind === "all" ? await db.findMany("releases", {
3177
+ index: "all",
3178
+ where: {},
3179
+ ...options
3180
+ }) : filter.kind === "bundle" ? await db.findMany("releases", {
3181
+ index: "byBundle",
3182
+ where: { bundle_id: filter.bundleId },
3183
+ ...options
3184
+ }) : filter.kind === "scope" ? filter.enabled === void 0 ? await db.findMany("releases", {
3185
+ index: "byScope",
3186
+ where: { scope_key: filter.scopeKey },
3187
+ ...options
3188
+ }) : await db.findMany("releases", {
3189
+ index: "byScopeEnabled",
3190
+ where: {
3191
+ scope_key: filter.scopeKey,
3192
+ enabled: filter.enabled
3193
+ },
3194
+ ...options
3195
+ }) : filter.enabled === void 0 ? await db.findMany("releases", {
3196
+ index: "byChannelPlatform",
3197
+ where: {
3198
+ channel_id: filter.channelId,
3199
+ platform: filter.platform
3200
+ },
3201
+ ...options
3202
+ }) : await db.findMany("releases", {
3203
+ index: "byChannelPlatformEnabled",
3204
+ where: {
3205
+ channel_id: filter.channelId,
3206
+ platform: filter.platform,
3207
+ enabled: filter.enabled
3208
+ },
3209
+ ...options
3210
+ })).rows.map(toReleaseRow);
3211
+ },
3212
+ async latestReleaseId(scopeKey) {
3213
+ const { rows } = await db.findMany("releases", {
3214
+ index: "byScope",
3215
+ where: { scope_key: scopeKey },
3216
+ order: "desc",
3217
+ limit: 1
3218
+ });
3219
+ return rows[0]?.id ?? null;
3220
+ },
3221
+ async getReleaseCatalogRow(scopeKey) {
3222
+ const row = await db.findOne("release_catalogs", { scope_key: scopeKey });
3223
+ return row === null ? null : toCatalogRow(row);
3224
+ },
3225
+ async listReleaseCatalogs(input) {
3226
+ return (await db.findMany("release_catalogs", {
3227
+ index: "all",
3228
+ where: {},
3229
+ order: input.order ?? "asc",
3230
+ limit: input.limit,
3231
+ ...after(input)
3232
+ })).rows.map(toCatalogRow);
3233
+ },
3234
+ async listChannels() {
3235
+ return (await drain((page) => db.findMany("channels", {
3236
+ index: "all",
3237
+ where: {},
3238
+ limit: PAGE$2,
3239
+ ...page
3240
+ }))).map(toChannelRow);
3241
+ },
3242
+ async findChannelByName(name) {
3243
+ const row = await db.findOne("channels", { name });
3244
+ return row === null ? null : toChannelRow(row);
3245
+ }
3246
+ };
3247
+ };
3248
+ var PAGE$1 = 500;
3249
+ /** Moves one bundle into or out of its platform's total and the overall total. */
3250
+ var countBundle = (tx, platform, delta) => {
3251
+ for (const key of [platform, "*"]) tx.aggregate("bundle_totals", { platform_key: key }, { bundles: delta });
3252
+ };
3253
+ var insertBundle = (tx, row) => {
3254
+ tx.create("bundles", row);
3255
+ countBundle(tx, row.platform, 1);
3256
+ };
3257
+ var updateBundle = (tx, current, set) => {
3258
+ tx.update("bundles", current, set);
3259
+ if (set.platform !== void 0 && set.platform !== current.platform) {
3260
+ countBundle(tx, current.platform, -1);
3261
+ countBundle(tx, set.platform, 1);
3262
+ }
3263
+ };
3264
+ /** Deletes a bundle and, by cascade, every patch to or from it; a release on it refuses. */
3265
+ var deleteBundle = async (tx, current) => {
3266
+ await tx.delete("bundles", current);
3267
+ countBundle(tx, current.platform, -1);
3268
+ };
3269
+ /**
3270
+ * Replaces a bundle's patches with `rows`: kept patches are updated where
3271
+ * they changed, others deleted or created, so no key is written twice.
3272
+ */
3273
+ var replaceBundlePatches = async (tx, bundle, rows) => {
3274
+ const wanted = new Map(rows.map((row) => [row.id, row]));
3275
+ const count = bundle._refs_bundle_patches_bundle_id ?? 0;
3276
+ let cursor;
3277
+ for (let seen = 0; seen < count;) {
3278
+ const page = await tx.findMany("bundle_patches", {
3279
+ index: "byBundle",
3280
+ where: { bundle_id: bundle.id },
3281
+ limit: Math.min(count - seen, PAGE$1),
3282
+ ...cursor === void 0 ? {} : { cursor }
3283
+ });
3284
+ for (const stored of page.rows) {
3285
+ const next = wanted.get(stored.id);
3286
+ wanted.delete(stored.id);
3287
+ if (next === void 0) {
3288
+ await tx.delete("bundle_patches", stored);
3289
+ continue;
3290
+ }
3291
+ const set = Object.fromEntries(Object.entries(next).filter(([field, value]) => field !== "id" && stored[field] !== value));
3292
+ if (Object.keys(set).length > 0) tx.update("bundle_patches", stored, set);
3293
+ }
3294
+ seen += page.rows.length;
3295
+ if (page.next === void 0) break;
3296
+ cursor = page.next;
3297
+ }
3298
+ for (const row of wanted.values()) tx.create("bundle_patches", row);
3299
+ };
3300
+ /** A channel by name, created when missing; a concurrent creator's row is returned instead. */
3301
+ var insertChannel = async (db, row) => {
3302
+ for (let attempt = 1;; attempt += 1) try {
3303
+ return await db.transaction(async (tx) => {
3304
+ const existing = await tx.findOne("channels", { name: row.name });
3305
+ if (existing !== null) return {
3306
+ row: toChannelRow(existing),
3307
+ inserted: false
3308
+ };
3309
+ tx.create("channels", row);
3310
+ return {
3311
+ row,
3312
+ inserted: true
3313
+ };
3314
+ });
3315
+ } catch (error) {
3316
+ if (attempt < 3 && error instanceof DatabaseConstraintError && (error.reason === "unique" || error.reason === "exists")) continue;
3317
+ throw error;
3318
+ }
3319
+ };
3320
+ /** Deletes a channel no release uses: 1 read + 1 write. */
3321
+ var deleteChannel = (db, id) => db.transaction(async (tx) => {
3322
+ const row = await tx.findOne("channels", { id });
3323
+ if (row === null) return {
3324
+ deleted: false,
3325
+ reason: "not_found"
3326
+ };
3327
+ if ((row._refs_releases_channel_id ?? 0) > 0) return {
3328
+ deleted: false,
3329
+ reason: "not_empty"
3330
+ };
3331
+ await tx.delete("channels", row);
3332
+ return { deleted: true };
3333
+ });
3334
+ var PAGE = 500;
3335
+ var fail = (code, message) => {
3336
+ throw new ReleaseCatalogMutationError(code, message);
3337
+ };
3338
+ var checkScope = (scope) => {
3339
+ const channelKey = encodeChannelKey(scope.channelName);
3340
+ const expected = scope.strategy === "APP_VERSION" ? createReleaseCatalogScopeKey({
3341
+ channelKey,
3342
+ platform: scope.platform,
3343
+ strategy: "APP_VERSION"
3344
+ }) : createReleaseCatalogScopeKey({
3345
+ channelKey,
3346
+ fingerprintHash: scope.fingerprintHash ?? "",
3347
+ platform: scope.platform,
3348
+ strategy: "FINGERPRINT"
3349
+ });
3350
+ if (scope.strategy === "FINGERPRINT" !== (scope.fingerprintHash !== null) || scope.scopeKey !== expected) fail("INVALID_SCOPE", "Release catalog scope metadata is inconsistent");
3351
+ };
3352
+ /** A scope's enabled releases, all of them, under the catalog's guard. */
3353
+ var enabledReleases = async (tx, scopeKey) => {
3354
+ const rows = [];
3355
+ let cursor;
3356
+ do {
3357
+ const page = await tx.findMany("releases", {
3358
+ index: "byScopeEnabled",
3359
+ where: {
3360
+ scope_key: scopeKey,
3361
+ enabled: true
3362
+ },
3363
+ limit: PAGE,
3364
+ ...cursor === void 0 ? {} : { cursor }
3365
+ });
3366
+ rows.push(...page.rows.map(toReleaseRow));
3367
+ cursor = page.next;
3368
+ } while (cursor !== void 0);
3369
+ return rows;
3370
+ };
3371
+ /** The scope's newest release id: `byScope` descending, limit 1. */
3372
+ var latestReleaseId = async (tx, scopeKey) => {
3373
+ const { rows } = await tx.findMany("releases", {
3374
+ index: "byScope",
3375
+ where: { scope_key: scopeKey },
3376
+ order: "desc",
3377
+ limit: 1
3378
+ });
3379
+ return rows[0]?.id ?? null;
3380
+ };
3381
+ /** Compiles `enabled` and writes it as the scope's next catalog generation. */
3382
+ var writeCatalog = async (tx, scope, stored, enabled, updatedAtMs) => {
3383
+ const compilation = await compileReleaseCatalog({
3384
+ strategy: scope.strategy,
3385
+ releases: enabled.map(releaseRowToRelease)
3386
+ });
3387
+ const generation = (stored.generation ?? 0) + 1;
3388
+ if (!Number.isSafeInteger(generation)) fail("CATALOG_GENERATION_EXHAUSTED", `Catalog "${scope.scopeKey}" exhausted its generation counter`);
3389
+ const catalog = {
3390
+ scope_key: scope.scopeKey,
3391
+ catalog_id: stored.catalog?.catalog_id ?? createUUIDv7(),
3392
+ strategy: scope.strategy,
3393
+ channel_id: scope.channelId,
3394
+ channel_key: encodeChannelKey(scope.channelName),
3395
+ platform: scope.platform,
3396
+ fingerprint_hash: scope.fingerprintHash,
3397
+ generation,
3398
+ payload: compilation.canonicalPayload,
3399
+ catalog_hash: compilation.catalogHash,
3400
+ byte_size: compilation.byteSize,
3401
+ is_tombstone: compilation.payload.releaseDescriptors.length === 0,
3402
+ updated_at_ms: updatedAtMs
3403
+ };
3404
+ const { scope_key: _scopeKey, ...set } = catalog;
3405
+ if (stored.row === null) tx.create("release_catalogs", catalog);
3406
+ else tx.update("release_catalogs", stored.row, set);
3407
+ return {
3408
+ catalog,
3409
+ compilation
3410
+ };
3411
+ };
3412
+ /** The scope's catalog row, and its compiled catalog and generation if it has one. */
3413
+ var readCatalog = async (tx, scopeKey) => {
3414
+ const row = await tx.findOne("release_catalogs", { scope_key: scopeKey });
3415
+ return {
3416
+ row,
3417
+ generation: row?.generation ?? null,
3418
+ catalog: row === null ? null : toCatalogRow(row)
3419
+ };
3420
+ };
3421
+ /** A scope with releases and no compiled catalog lost its history. */
3422
+ var assertHistory = (stored, hasReleases) => {
3423
+ if (stored.catalog === null && hasReleases) fail("CATALOG_IDENTITY_MISSING", "Catalog history is missing. Restore its catalog row from backup before rebuilding or deploying.");
3424
+ };
3425
+ /** One release change and its scope's next catalog, inside `tx`. */
3426
+ var changeRelease = async (tx, { scope, change, bundle, updatedAtMs }) => {
3427
+ checkScope(scope);
3428
+ const stored = await readCatalog(tx, scope.scopeKey);
3429
+ const enabled = await enabledReleases(tx, scope.scopeKey);
3430
+ if (bundle !== void 0) {
3431
+ insertBundle(tx, bundle.row);
3432
+ for (const patch of bundle.patches) tx.create("bundle_patches", patch);
3433
+ }
3434
+ let before = null;
3435
+ let release;
3436
+ if (change.operation === "insert") {
3437
+ const latest = await latestReleaseId(tx, scope.scopeKey);
3438
+ assertHistory(stored, latest !== null);
3439
+ const floor = [latest, bundle && isUUIDv7(bundle.row.id) ? bundle.row.id : null].filter((id) => id !== null).sort().at(-1);
3440
+ const id = change.row.id ?? createUUIDv7After(floor ?? null);
3441
+ if (latest !== null && id <= latest) fail("NON_MONOTONIC_RELEASE_ID", `Release "${id}" must sort after "${latest}"`);
3442
+ release = {
3443
+ ...change.row,
3444
+ id
3445
+ };
3446
+ if (release.scope_key !== scope.scopeKey || release.channel_id !== scope.channelId || release.platform !== scope.platform || release.strategy !== scope.strategy || release.fingerprint_hash !== scope.fingerprintHash || release.revision !== 1) fail("INVALID_SCOPE", "Inserted Release does not match its catalog scope");
3447
+ tx.create("releases", release);
3448
+ } else {
3449
+ const row = await tx.findOne("releases", { id: change.id });
3450
+ if (row === null || row.scope_key !== scope.scopeKey) return fail("RELEASE_NOT_FOUND", `Release "${change.id}" was not found in scope "${scope.scopeKey}"`);
3451
+ assertHistory(stored, true);
3452
+ before = toReleaseRow(row);
3453
+ if (change.operation === "delete") {
3454
+ await tx.delete("releases", row);
3455
+ release = null;
3456
+ } else {
3457
+ const set = {
3458
+ ...change.update,
3459
+ revision: before.revision + 1
3460
+ };
3461
+ tx.update("releases", row, set);
3462
+ release = {
3463
+ ...before,
3464
+ ...set
3465
+ };
3466
+ }
3467
+ }
3468
+ const changedId = before?.id ?? release?.id;
3469
+ const { catalog, compilation } = await writeCatalog(tx, scope, stored, [...enabled.filter(({ id }) => id !== changedId), ...release?.enabled ? [release] : []].sort((left, right) => left.id < right.id ? -1 : 1), updatedAtMs);
3470
+ return {
3471
+ catalog,
3472
+ release,
3473
+ diagnostics: compilation.diagnostics
3474
+ };
3475
+ };
3476
+ /**
3477
+ * Release changes rooted at their catalogs: each reads its scope's catalog
3478
+ * and enabled releases, applies the change, and writes the next catalog
3479
+ * generation, all in one transaction. A concurrent change in a scope bumps
3480
+ * its catalog row, so this transaction reruns instead of publishing a stale
3481
+ * catalog.
3482
+ */
3483
+ var changeReleases = (db, inputs) => {
3484
+ if (new Set(inputs.map(({ scope }) => scope.scopeKey)).size !== inputs.length) fail("INVALID_SCOPE", "A batch cannot change one catalog scope twice");
3485
+ return db.transaction(async (tx) => {
3486
+ const results = [];
3487
+ for (const input of inputs) results.push(await changeRelease(tx, input));
3488
+ return results;
3489
+ });
3490
+ };
3491
+ /** Recompiles a scope's catalog from its enabled releases inside `tx`; an unchanged catalog is not rewritten. */
3492
+ var rebuildScope = async (tx, scope, updatedAtMs) => {
3493
+ checkScope(scope);
3494
+ const stored = await readCatalog(tx, scope.scopeKey);
3495
+ assertHistory(stored, await latestReleaseId(tx, scope.scopeKey) !== null);
3496
+ const enabled = await enabledReleases(tx, scope.scopeKey);
3497
+ const compilation = await compileReleaseCatalog({
3498
+ strategy: scope.strategy,
3499
+ releases: enabled.map(releaseRowToRelease)
3500
+ });
3501
+ const current = stored.catalog;
3502
+ if (current !== null && current.strategy === scope.strategy && current.channel_id === scope.channelId && current.channel_key === encodeChannelKey(scope.channelName) && current.platform === scope.platform && current.fingerprint_hash === scope.fingerprintHash && current.catalog_hash === compilation.catalogHash) return {
3503
+ catalog: current,
3504
+ current,
3505
+ changed: false,
3506
+ diagnostics: compilation.diagnostics
3507
+ };
3508
+ const written = await writeCatalog(tx, scope, stored, enabled, updatedAtMs);
3509
+ return {
3510
+ catalog: written.catalog,
3511
+ current,
3512
+ changed: true,
3513
+ diagnostics: written.compilation.diagnostics
3514
+ };
3515
+ };
3516
+ /** Carries a preflight's result out of the transaction it stops, so nothing is written. */
3517
+ var Preflight = class extends Error {
3518
+ result;
3519
+ constructor(result) {
3520
+ super("preflight");
3521
+ this.result = result;
3522
+ }
3523
+ };
3524
+ var dryRun = async (db, read) => {
3525
+ try {
3526
+ await db.transaction(async (tx) => {
3527
+ throw new Preflight(await read(tx));
3528
+ });
3529
+ } catch (error) {
3530
+ if (error instanceof Preflight) return error.result;
3531
+ throw error;
3532
+ }
3533
+ throw new Error("A preflight transaction returned without its result.");
3534
+ };
3535
+ /** A channel's catalog scope for a platform, and a fingerprint when it has one. */
3536
+ var scopeOf = (channel, platform, fingerprintHash) => {
3537
+ const channelKey = encodeChannelKey(channel.name);
3538
+ return fingerprintHash === null ? {
3539
+ channelId: channel.id,
3540
+ channelName: channel.name,
3541
+ fingerprintHash,
3542
+ platform,
3543
+ scopeKey: createReleaseCatalogScopeKey({
3544
+ channelKey,
3545
+ platform,
3546
+ strategy: "APP_VERSION"
3547
+ }),
3548
+ strategy: "APP_VERSION"
3549
+ } : {
3550
+ channelId: channel.id,
3551
+ channelName: channel.name,
3552
+ fingerprintHash,
3553
+ platform,
3554
+ scopeKey: createReleaseCatalogScopeKey({
3555
+ channelKey,
3556
+ fingerprintHash,
3557
+ platform,
3558
+ strategy: "FINGERPRINT"
3559
+ }),
3560
+ strategy: "FINGERPRINT"
3561
+ };
3562
+ };
3563
+ /** A release and its scope, checked against the revision the caller saw. */
3564
+ var loadTarget = async (tx, { releaseId, expectedRevision }) => {
3565
+ const row = await tx.findOne("releases", { id: releaseId });
3566
+ if (row === null) throw new ReleaseManagementError("RELEASE_NOT_FOUND", `Release "${releaseId}" was not found.`);
3567
+ const release = toReleaseRow(row);
3568
+ if (expectedRevision !== void 0 && expectedRevision !== release.revision) throw new ReleaseManagementError("VERSION_CONFLICT", `Release "${release.id}" is revision ${release.revision}; expected ${expectedRevision}.`);
3569
+ const catalog = await tx.findOne("release_catalogs", { scope_key: release.scope_key });
3570
+ if (catalog === null) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${release.id}" has no catalog projection.`);
3571
+ return {
3572
+ release,
3573
+ scope: {
3574
+ channelId: release.channel_id,
3575
+ channelName: decodeChannelKey(catalog.channel_key),
3576
+ fingerprintHash: release.fingerprint_hash,
3577
+ platform: release.platform,
3578
+ scopeKey: release.scope_key,
3579
+ strategy: release.strategy
3580
+ }
3581
+ };
3582
+ };
3583
+ var policyUpdate = (release, patch, updatedAtMs) => {
3584
+ if (patch.fingerprintHash !== void 0 && patch.fingerprintHash !== release.fingerprint_hash) throw new ReleaseManagementError("SCOPE_MOVE_UNSUPPORTED", "Changing a fingerprint target requires an atomic Release scope move.");
3585
+ return {
3586
+ ...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
3587
+ ...patch.message === void 0 ? {} : { message: patch.message },
3588
+ ...patch.rolloutCohortCount === void 0 ? {} : { rollout_cohort_count: patch.rolloutCohortCount },
3589
+ ...patch.shouldForceUpdate === void 0 ? {} : { should_force_update: patch.shouldForceUpdate },
3590
+ ...patch.targetAppVersion === void 0 ? {} : { target_app_version: patch.targetAppVersion },
3591
+ ...patch.targetCohorts === void 0 ? {} : { target_cohorts: [...patch.targetCohorts] },
3592
+ updated_at_ms: updatedAtMs
3593
+ };
3594
+ };
3595
+ var mutationResult = ({ catalog, release }) => ({
3596
+ attempts: 1,
3597
+ catalog,
3598
+ release
3599
+ });
3600
+ /** The scope a catalog row names, for a rebuild. */
3601
+ var catalogScope = async (tx, scopeKey) => {
3602
+ const row = await tx.findOne("release_catalogs", { scope_key: scopeKey });
3603
+ if (row === null) throw new Error(`Release catalog "${scopeKey}" was not found.`);
3604
+ return {
3605
+ channelId: row.channel_id,
3606
+ channelName: decodeChannelKey(row.channel_key),
3607
+ fingerprintHash: row.fingerprint_hash,
3608
+ platform: row.platform,
3609
+ scopeKey: row.scope_key,
3610
+ strategy: row.strategy
3611
+ };
3612
+ };
3613
+ /**
3614
+ * Core's typed writes: each runs in one engine transaction rooted at the
3615
+ * catalogs it changes, so a concurrent change reruns it instead of
3616
+ * publishing a stale catalog.
3617
+ */
3618
+ var createCoreOperations = (db, { now = Date.now } = {}) => {
3619
+ const ensureChannel = async (name) => (await insertChannel(db, {
3620
+ id: `channel:${encodeChannelKey(name)}`,
3621
+ name
3622
+ })).row;
3623
+ /**
3624
+ * Runs a write that ensured its channels first. A channel deleted between
3625
+ * that and the write refuses the write, so it runs again and ensures it anew.
3626
+ */
3627
+ const withChannels = async (write) => {
3628
+ for (let attempt = 1;; attempt += 1) try {
3629
+ return await write();
3630
+ } catch (error) {
3631
+ if (attempt < 3 && error instanceof DatabaseConstraintError && error.reason === "not_found" && error.model === "channels") continue;
3632
+ throw error;
3633
+ }
3634
+ };
3635
+ const deployOnce = async (deployments) => {
3636
+ const channels = /* @__PURE__ */ new Map();
3637
+ for (const { release } of deployments) if (!channels.has(release.channel)) channels.set(release.channel, await ensureChannel(release.channel));
3638
+ const platforms = /* @__PURE__ */ new Map();
3639
+ for (const deployment of deployments) {
3640
+ if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
3641
+ const row = await db.findOne("bundles", { id: deployment.bundleId });
3642
+ if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
3643
+ platforms.set(deployment.bundleId, toBundleRow(row).platform);
3644
+ }
3645
+ const updatedAtMs = now();
3646
+ return (await changeReleases(db, deployments.map((deployment) => {
3647
+ const { release: policy } = deployment;
3648
+ const stored = "bundleId" in deployment;
3649
+ const bundleId = stored ? deployment.bundleId : deployment.bundle.id;
3650
+ const platform = stored ? platforms.get(deployment.bundleId) : deployment.bundle.platform;
3651
+ const scope = scopeOf(channels.get(policy.channel), platform, policy.fingerprintHash);
3652
+ return {
3653
+ scope,
3654
+ ...stored ? {} : { bundle: {
3655
+ row: bundleToRow(deployment.bundle),
3656
+ patches: bundleToPatchRows(deployment.bundle)
3657
+ } },
3658
+ change: {
3659
+ operation: "insert",
3660
+ row: {
3661
+ bundle_id: bundleId,
3662
+ channel_id: scope.channelId,
3663
+ created_at_ms: updatedAtMs,
3664
+ enabled: policy.enabled,
3665
+ fingerprint_hash: policy.fingerprintHash,
3666
+ kind: "BUNDLE",
3667
+ message: policy.message,
3668
+ operation: "DEPLOY",
3669
+ platform,
3670
+ revision: 1,
3671
+ rollout_cohort_count: policy.rolloutCohortCount ?? 1e3,
3672
+ scope_key: scope.scopeKey,
3673
+ should_force_update: policy.shouldForceUpdate,
3674
+ source_release_id: null,
3675
+ strategy: scope.strategy,
3676
+ target_app_version: policy.targetAppVersion,
3677
+ target_cohorts: policy.targetCohorts ?? [],
3678
+ updated_at_ms: updatedAtMs
3679
+ }
3680
+ },
3681
+ updatedAtMs
3682
+ };
3683
+ }))).map(mutationResult);
3684
+ };
3685
+ return {
3686
+ deploy: (deployments) => withChannels(() => deployOnce(deployments)),
3687
+ updateReleasePolicy: (input) => {
3688
+ const updatedAtMs = now();
3689
+ return db.transaction(async (tx) => {
3690
+ const { release, scope } = await loadTarget(tx, input);
3691
+ return mutationResult(await changeRelease(tx, {
3692
+ scope,
3693
+ change: {
3694
+ operation: "update",
3695
+ id: release.id,
3696
+ update: policyUpdate(release, input.patch, updatedAtMs)
3697
+ },
3698
+ updatedAtMs
3699
+ }));
3700
+ });
3701
+ },
3702
+ preflightReleasePolicy: (input) => {
3703
+ const updatedAtMs = now();
3704
+ return dryRun(db, async (tx) => {
3705
+ const { release, scope } = await loadTarget(tx, input);
3706
+ const stored = await readCatalog(tx, scope.scopeKey);
3707
+ const changed = await changeRelease(tx, {
3708
+ scope,
3709
+ change: {
3710
+ operation: "update",
3711
+ id: release.id,
3712
+ update: policyUpdate(release, input.patch, updatedAtMs)
3713
+ },
3714
+ updatedAtMs
3715
+ });
3716
+ return {
3717
+ catalog: changed.catalog,
3718
+ currentCatalog: stored.catalog,
3719
+ diagnostics: changed.diagnostics,
3720
+ expectedReleaseRevision: release.revision,
3721
+ release: changed.release
3722
+ };
3723
+ });
3724
+ },
3725
+ deleteRelease: (input) => db.transaction(async (tx) => {
3726
+ const { release, scope } = await loadTarget(tx, input);
3727
+ if (release.enabled) throw new ReleaseManagementError("ENABLED_RELEASE", `Disable Release "${release.id}" before hard deletion.`);
3728
+ return mutationResult(await changeRelease(tx, {
3729
+ scope,
3730
+ change: {
3731
+ operation: "delete",
3732
+ id: release.id
3733
+ },
3734
+ updatedAtMs: now()
3735
+ }));
3736
+ }),
3737
+ promoteRelease: (input) => withChannels(async () => {
3738
+ const checkSource = (source) => {
3739
+ if (source.kind !== "BUNDLE" || source.bundle_id === null) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Only a Bundle Release can be promoted.");
3740
+ };
3741
+ checkSource((await db.transaction((tx) => loadTarget(tx, input))).release);
3742
+ const targetChannel = input.targetChannel.trim();
3743
+ if (targetChannel.length === 0) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Promotion requires a target channel.");
3744
+ const channel = await ensureChannel(targetChannel);
3745
+ const updatedAtMs = now();
3746
+ return db.transaction(async (tx) => {
3747
+ const { release: source, scope: sourceScope } = await loadTarget(tx, input);
3748
+ checkSource(source);
3749
+ const targetScope = scopeOf(channel, source.platform, source.fingerprint_hash);
3750
+ if (targetScope.scopeKey === sourceScope.scopeKey) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Source and target Release scopes are the same.");
3751
+ const { rows } = await tx.findMany("releases", {
3752
+ index: "byScope",
3753
+ where: { scope_key: targetScope.scopeKey },
3754
+ order: "desc",
3755
+ limit: 1
3756
+ });
3757
+ const floor = [
3758
+ rows[0]?.id ?? null,
3759
+ source.id,
3760
+ source.bundle_id
3761
+ ].filter((id) => id !== null && isUUIDv7(id)).sort().at(-1);
3762
+ const moved = input.action === "move" ? await changeRelease(tx, {
3763
+ scope: sourceScope,
3764
+ change: {
3765
+ operation: "update",
3766
+ id: source.id,
3767
+ update: {
3768
+ enabled: false,
3769
+ updated_at_ms: updatedAtMs
3770
+ }
3771
+ },
3772
+ updatedAtMs
3773
+ }) : null;
3774
+ const target = await changeRelease(tx, {
3775
+ scope: targetScope,
3776
+ change: {
3777
+ operation: "insert",
3778
+ row: {
3779
+ ...source,
3780
+ id: createUUIDv7After(floor ?? null, updatedAtMs),
3781
+ revision: 1,
3782
+ scope_key: targetScope.scopeKey,
3783
+ channel_id: targetScope.channelId,
3784
+ enabled: true,
3785
+ rollout_cohort_count: 1e3,
3786
+ target_cohorts: [],
3787
+ operation: "PROMOTE",
3788
+ source_release_id: source.id,
3789
+ created_at_ms: updatedAtMs,
3790
+ updated_at_ms: updatedAtMs
3791
+ }
3792
+ },
3793
+ updatedAtMs
3794
+ });
3795
+ return {
3796
+ source: moved === null ? null : mutationResult(moved),
3797
+ target: mutationResult(target)
3798
+ };
3799
+ });
3800
+ }),
3801
+ rebuildReleaseCatalog: (scopeKey) => {
3802
+ const updatedAtMs = now();
3803
+ return db.transaction(async (tx) => {
3804
+ const { current: _current, ...rebuilt } = await rebuildScope(tx, await catalogScope(tx, scopeKey), updatedAtMs);
3805
+ return {
3806
+ attempts: 1,
3807
+ ...rebuilt
3808
+ };
3809
+ });
3810
+ },
3811
+ preflightReleaseCatalogRebuild: (scopeKey) => {
3812
+ const updatedAtMs = now();
3813
+ return dryRun(db, async (tx) => {
3814
+ const rebuilt = await rebuildScope(tx, await catalogScope(tx, scopeKey), updatedAtMs);
3815
+ return {
3816
+ changed: rebuilt.changed,
3817
+ currentCatalog: rebuilt.current,
3818
+ diagnostics: rebuilt.diagnostics,
3819
+ projectedCatalog: rebuilt.catalog
3820
+ };
3821
+ });
3822
+ },
3823
+ ensureChannel,
3824
+ deleteChannel: (id) => deleteChannel(db, id),
3825
+ updateBundle: (id, update) => db.transaction(async (tx) => {
3826
+ const current = await tx.findOne("bundles", { id });
3827
+ if (current === null) throw new DatabaseBundleNotFoundError(id);
3828
+ const { patches, ...fields } = update;
3829
+ const next = {
3830
+ ...rowToBundle(toBundleRow(current)),
3831
+ ...fields,
3832
+ id
3833
+ };
3834
+ const { id: _id, ...set } = bundleToRow(next);
3835
+ updateBundle(tx, current, set);
3836
+ if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
3837
+ ...next,
3838
+ patches
3839
+ }));
3840
+ }),
3841
+ deleteBundles: async (ids) => {
3842
+ try {
3843
+ await db.transaction(async (tx) => {
3844
+ for (const id of new Set(ids)) {
3845
+ const current = await tx.findOne("bundles", { id });
3846
+ if (current !== null) await deleteBundle(tx, current);
3847
+ }
3848
+ });
3849
+ } catch (error) {
3850
+ if (error instanceof DatabaseConstraintError && error.reason === "referenced") throw new DatabaseRowReferencedError();
3851
+ throw error;
3852
+ }
3853
+ }
3854
+ };
3855
+ };
3856
+ /** A channel's id is `channel:` and its name's channel key, so ids share channel_key's bound. */
3857
+ var CHANNEL_ID = {
3858
+ type: "string",
3859
+ maxLength: 1408,
3860
+ ascii: true
3861
+ };
3862
+ /** Core's module: always installed, never a plugin. */
3863
+ var coreModule = {
3864
+ id: "core",
3865
+ schema: {
3866
+ bundles: defineTable({
3867
+ id: {
3868
+ type: "string",
3869
+ maxLength: 36
3870
+ },
3871
+ platform: {
3872
+ type: "string",
3873
+ maxLength: 16
3874
+ },
3875
+ git_commit_hash: {
3876
+ type: "string",
3877
+ maxLength: 255,
3878
+ required: false
3879
+ },
3880
+ metadata: { type: "json" },
3881
+ manifest_storage_uri: { type: "string" },
3882
+ manifest_file_hash: { type: "string" },
3883
+ asset_base_storage_uri: { type: "string" }
3884
+ }, {
3885
+ key: ["id"],
3886
+ indexes: {
3887
+ byPlatform: {
3888
+ eq: ["platform"],
3889
+ sort: ["id"]
3890
+ },
3891
+ all: {
3892
+ eq: [],
3893
+ sort: ["id"]
3894
+ }
3895
+ }
3896
+ }),
3897
+ bundle_patches: defineTable({
3898
+ id: {
3899
+ type: "string",
3900
+ maxLength: 255
3901
+ },
3902
+ bundle_id: {
3903
+ type: "string",
3904
+ maxLength: 36,
3905
+ references: {
3906
+ model: "bundles",
3907
+ onDelete: "cascade"
3908
+ }
3909
+ },
3910
+ base_bundle_id: {
3911
+ type: "string",
3912
+ maxLength: 36,
3913
+ references: {
3914
+ model: "bundles",
3915
+ onDelete: "cascade"
3916
+ }
3917
+ },
3918
+ base_file_hash: { type: "string" },
3919
+ patch_file_hash: { type: "string" },
3920
+ patch_storage_uri: { type: "string" },
3921
+ byte_size: { type: "integer" },
3922
+ order_index: { type: "integer" }
3923
+ }, {
3924
+ key: ["id"],
3925
+ indexes: {
3926
+ pair: {
3927
+ eq: ["bundle_id", "base_bundle_id"],
3928
+ sort: [],
3929
+ unique: true
3930
+ },
3931
+ byBundle: {
3932
+ eq: ["bundle_id"],
3933
+ sort: ["order_index"],
3934
+ root: { model: "bundles" }
3935
+ },
3936
+ byBase: {
3937
+ eq: ["base_bundle_id"],
3938
+ sort: ["bundle_id"]
3939
+ }
3940
+ }
3941
+ }),
3942
+ releases: defineTable({
3943
+ id: {
3944
+ type: "string",
3945
+ maxLength: 36
3946
+ },
3947
+ revision: { type: "integer" },
3948
+ scope_key: {
3949
+ type: "string",
3950
+ maxLength: 2048,
3951
+ ascii: true
3952
+ },
3953
+ channel_id: {
3954
+ ...CHANNEL_ID,
3955
+ references: {
3956
+ model: "channels",
3957
+ onDelete: "restrict"
3958
+ }
3959
+ },
3960
+ platform: {
3961
+ type: "string",
3962
+ maxLength: 16
3963
+ },
3964
+ kind: {
3965
+ type: "string",
3966
+ maxLength: 16
3967
+ },
3968
+ bundle_id: {
3969
+ type: "string",
3970
+ maxLength: 36,
3971
+ required: false,
3972
+ references: {
3973
+ model: "bundles",
3974
+ onDelete: "restrict"
3975
+ }
3976
+ },
3977
+ strategy: {
3978
+ type: "string",
3979
+ maxLength: 16
3980
+ },
3981
+ target_app_version: {
3982
+ type: "string",
3983
+ required: false
3984
+ },
3985
+ fingerprint_hash: {
3986
+ type: "string",
3987
+ maxLength: 255,
3988
+ required: false
3989
+ },
3990
+ enabled: { type: "boolean" },
3991
+ should_force_update: { type: "boolean" },
3992
+ message: {
3993
+ type: "string",
3994
+ required: false
3995
+ },
3996
+ rollout_cohort_count: { type: "integer" },
3997
+ target_cohorts: { type: "json" },
3998
+ operation: {
3999
+ type: "string",
4000
+ maxLength: 16
4001
+ },
4002
+ source_release_id: {
4003
+ type: "string",
4004
+ maxLength: 36,
4005
+ required: false,
4006
+ references: {
4007
+ model: "releases",
4008
+ onDelete: "none"
4009
+ }
4010
+ },
4011
+ created_at_ms: { type: "integer" },
4012
+ updated_at_ms: { type: "integer" }
4013
+ }, {
4014
+ key: ["id"],
4015
+ indexes: {
4016
+ byScope: {
4017
+ eq: ["scope_key"],
4018
+ sort: ["id"],
4019
+ root: { model: "release_catalogs" }
4020
+ },
4021
+ byScopeEnabled: {
4022
+ eq: ["scope_key", "enabled"],
4023
+ sort: ["id"],
4024
+ root: { model: "release_catalogs" }
4025
+ },
4026
+ byChannelPlatform: {
4027
+ eq: ["channel_id", "platform"],
4028
+ sort: ["id"]
4029
+ },
4030
+ byChannelPlatformEnabled: {
4031
+ eq: [
4032
+ "channel_id",
4033
+ "platform",
4034
+ "enabled"
4035
+ ],
4036
+ sort: ["id"]
4037
+ },
4038
+ byBundle: {
4039
+ eq: ["bundle_id"],
4040
+ sort: ["id"]
4041
+ },
4042
+ all: {
4043
+ eq: [],
4044
+ sort: ["id"]
4045
+ }
4046
+ }
4047
+ }),
4048
+ release_catalogs: defineTable({
4049
+ scope_key: {
4050
+ type: "string",
4051
+ maxLength: 2048,
4052
+ ascii: true
4053
+ },
4054
+ catalog_id: {
4055
+ type: "string",
4056
+ maxLength: 255
4057
+ },
4058
+ strategy: {
4059
+ type: "string",
4060
+ maxLength: 16
4061
+ },
4062
+ channel_id: {
4063
+ ...CHANNEL_ID,
4064
+ references: {
4065
+ model: "channels",
4066
+ onDelete: "none"
4067
+ }
4068
+ },
4069
+ channel_key: {
4070
+ type: "string",
4071
+ maxLength: 1400,
4072
+ ascii: true
4073
+ },
4074
+ platform: {
4075
+ type: "string",
4076
+ maxLength: 16
4077
+ },
4078
+ fingerprint_hash: {
4079
+ type: "string",
4080
+ maxLength: 255,
4081
+ required: false
4082
+ },
4083
+ generation: { type: "integer" },
4084
+ payload: { type: "string" },
4085
+ catalog_hash: {
4086
+ type: "string",
4087
+ maxLength: 71
4088
+ },
4089
+ byte_size: { type: "integer" },
4090
+ is_tombstone: { type: "boolean" },
4091
+ updated_at_ms: { type: "integer" }
4092
+ }, {
4093
+ key: ["scope_key"],
4094
+ indexes: { all: {
4095
+ eq: [],
4096
+ sort: ["scope_key"]
4097
+ } }
4098
+ }),
4099
+ channels: defineTable({
4100
+ id: CHANNEL_ID,
4101
+ name: {
4102
+ type: "string",
4103
+ maxLength: 255,
4104
+ unique: true
4105
+ }
4106
+ }, {
4107
+ key: ["id"],
4108
+ indexes: { all: {
4109
+ eq: [],
4110
+ sort: ["name"]
4111
+ } }
4112
+ }),
4113
+ bundle_totals: defineAggregate({ platform_key: {
4114
+ type: "string",
4115
+ maxLength: 16
4116
+ } }, {
4117
+ key: ["platform_key"],
4118
+ counters: ["bundles"],
4119
+ indexes: { byPlatform: {
4120
+ eq: ["platform_key"],
4121
+ sort: []
4122
+ } }
4123
+ })
4124
+ }
4125
+ };
4126
+ /**
4127
+ * The update check answers from one Release Catalog row, so a transaction
4128
+ * changes what the cacheable client routes answer only when it writes one.
4129
+ * The purge runs once, after the attempt that commits.
4130
+ */
4131
+ var purgingCachedRoutes = (db, onCachedRoutesChange) => {
4132
+ if (onCachedRoutesChange === void 0) return db;
4133
+ return {
4134
+ ...db,
4135
+ async transaction(fn) {
4136
+ let changed = false;
4137
+ const result = await db.transaction((tx) => {
4138
+ changed = false;
4139
+ return fn({
4140
+ ...tx,
4141
+ create: (model, row) => {
4142
+ changed ||= model === "release_catalogs";
4143
+ tx.create(model, row);
4144
+ },
4145
+ update: (model, row, set) => {
4146
+ changed ||= model === "release_catalogs";
4147
+ tx.update(model, row, set);
4148
+ },
4149
+ delete: (model, row) => {
4150
+ changed ||= model === "release_catalogs";
4151
+ return tx.delete(model, row);
4152
+ }
4153
+ });
4154
+ });
4155
+ if (changed) await onCachedRoutesChange();
4156
+ return result;
4157
+ }
4158
+ };
4159
+ };
4160
+ /** Core's reads and typed writes over one database handle. */
4161
+ var createCoreApi = (db, storage, options = {}) => {
4162
+ const reads = createCoreReads(db, storage);
4163
+ return {
4164
+ ...reads,
4165
+ ...createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
4166
+ ready: async () => {
4167
+ await reads.listReleaseCatalogs({ limit: 1 });
4168
+ }
4169
+ };
4170
+ };
4171
+ /**
4172
+ * Core's API in process, on a database's storage adapter: how the CLI reads
4173
+ * and writes a database that runs on the storage engine.
4174
+ */
4175
+ var createInProcessCoreApi = (adapter, options = {}) => createCoreApi(createDatabaseEngine({
4176
+ adapter,
4177
+ schema: resolveSchema([coreModule])
4178
+ }).database(coreModule), { resolveFileUrl: async () => null }, options);
4179
+ var isRecord = (value) => typeof value === "object" && value !== null;
4180
+ var isAdapter = (value) => isRecord(value) && [
4181
+ "fits",
4182
+ "get",
4183
+ "query",
4184
+ "write"
4185
+ ].every((method) => typeof value[method] === "function");
4186
+ /** The storage adapter a configured database runs on, if it is on the engine. */
4187
+ var engineAdapterOf = (database) => isRecord(database) && isAdapter(database.adapter) ? database.adapter : isAdapter(database) ? database : void 0;
4188
+ var OFF_ENGINE_DATABASE = "This database does not run on Hot Updater's storage engine. Upgrade its provider package to 1.0.";
4189
+ /**
4190
+ * Core's API for a configured database, as the CLI uses it: a self-hosted
4191
+ * server's admin API when the database is `standaloneRepository`, or the
4192
+ * database's own storage adapter, in process.
4193
+ */
4194
+ var createDatabaseCoreApi = (database, options = {}) => {
4195
+ if (isRecord(database) && isRecord(database.core)) return database.core;
4196
+ const adapter = engineAdapterOf(database);
4197
+ if (adapter === void 0) throw new Error(OFF_ENGINE_DATABASE);
4198
+ const engineDatabase = database;
4199
+ return createInProcessCoreApi(adapter, {
4200
+ ...options,
4201
+ ...typeof engineDatabase.onCachedRoutesChange === "function" ? { onCachedRoutesChange: () => engineDatabase.onCachedRoutesChange() } : {}
4202
+ });
4203
+ };
4204
+ //#endregion
4205
+ export { createCoreReads as a, engineAdapterOf as c, validateSchema as d, createCoreApi as i, pruneDuringWrites as l, aggregateBatchingModule as n, createDatabaseCoreApi as o, coreModule as r, createDatabaseEngine as s, OFF_ENGINE_DATABASE as t, resolveSchema as u };