@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/ConsoleFeatureUnavailable-B9VYGA3N.js +1 -0
- package/.output/public/assets/HashValueDisplay-D880zntR.js +1 -0
- package/.output/public/assets/InsightsInfo-By_sPjTE.js +1 -0
- package/.output/public/assets/InsightsPageHeader-9_kx_LL7.js +1 -0
- package/.output/public/assets/NotFoundPage-Dys8C0cW.js +1 -0
- package/.output/public/assets/api-CtarlV0l.js +1 -0
- package/.output/public/assets/api-keys-DFF_QqtN.js +1 -0
- package/.output/public/assets/api-keys-evfsQ4sN.js +1 -0
- package/.output/public/assets/arrow-left-BP9D-ta_.js +1 -0
- package/.output/public/assets/arrow-up-right-CDP9Tevw.js +1 -0
- package/.output/public/assets/card-DzCPSRsh.js +1 -0
- package/.output/public/assets/console-features-api-yv7rm3DX.js +1 -0
- package/.output/public/assets/dialog-BDvCy_QO.js +1 -0
- package/.output/public/assets/dist-Clb6cQ9l.js +1 -0
- package/.output/public/assets/empty-C5x9mn_s.js +1 -0
- package/.output/public/assets/field-Bou7shoO.js +1 -0
- package/.output/public/assets/index-0eg3l8aC.js +10 -0
- package/.output/public/assets/input-group-DVtWFUUF.js +1 -0
- package/.output/public/assets/insights-2MbdiSNv.js +1 -0
- package/.output/public/assets/insights-C6tmCZHk.js +2 -0
- package/.output/public/assets/insights-CadS_aIA.js +42 -0
- package/.output/public/assets/insights-recovery-rpc-wvdyM4LD.js +1 -0
- package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
- package/.output/public/assets/insights-usage-rpc-qUkNtrWr.js +1 -0
- package/.output/public/assets/insights_.distribution-BhBs6PNA.js +1 -0
- package/.output/public/assets/insights_.distribution-CItZw3-8.js +1 -0
- package/.output/public/assets/insights_.distribution-CJUH_wAS.js +2 -0
- package/.output/public/assets/installations-CH2H7JQt.js +2 -0
- package/.output/public/assets/installations-DMeMuwsW.js +1 -0
- package/.output/public/assets/installations-DnvxI9Dt.js +1 -0
- package/.output/public/assets/loader-circle-Ca7C9YY1.js +1 -0
- package/.output/public/assets/mutation-x9L28_CD.js +1 -0
- package/.output/public/assets/preload-helper-BN-UkjOE.js +1 -0
- package/.output/public/assets/refresh-cw-CVGqvy8Z.js +1 -0
- package/.output/public/assets/routes-BhK8DuZA.js +1 -0
- package/.output/public/assets/routes-veKtDW39.js +2 -0
- package/.output/public/assets/sidebar-B45aMqRm.js +3 -0
- package/.output/public/assets/styles-Dn0pK1EX.css +2 -0
- package/.output/public/assets/table-CwLnN6KX.js +1 -0
- package/.output/public/assets/useCompositeListItem-CF7vsfcA.js +1 -0
- package/.output/public/assets/useMutation-CN1ygYEr.js +1 -0
- package/.output/public/assets/useOpenInteractionType-AnAMWYIY.js +1 -0
- package/.output/public/assets/valueToPercent-C-t8FT0y.js +1 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-CUQLz7hs.mjs} +52 -40
- package/.output/server/_libs/@base-ui/react+[...].mjs +7044 -3622
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +49 -3
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +4 -4
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +207 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +818 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/es-toolkit.mjs +1159 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +46 -32
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14350 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__react-query.mjs +8 -3
- package/.output/server/_libs/verkit.mjs +99 -1
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/ConsoleFeatureUnavailable-DoxET2AT.mjs +58 -0
- package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-BplboHEb.mjs} +3 -3
- package/.output/server/_ssr/InsightsInfo-o2YPd5Qc.mjs +37 -0
- package/.output/server/_ssr/InsightsPageHeader-NExCJL1I.mjs +154 -0
- package/.output/server/_ssr/NotFoundPage-Cd-A8ock.mjs +37 -0
- package/.output/server/_ssr/api-B1Ro0uys.mjs +202 -0
- package/.output/server/_ssr/api-C0G9MTMI.mjs +4205 -0
- package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-CT3HG7KT.mjs} +49 -9
- package/.output/server/_ssr/api-keys-Dvvy1sem.mjs +5 -0
- package/.output/server/_ssr/api-keys-rpc-b1qTTFfG.mjs +52 -0
- package/.output/server/_ssr/api-rpc-Cbns2MGx.mjs +338 -0
- package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-DChS7m9y.mjs} +6 -6
- package/.output/server/_ssr/auth.server-Ccqz2iVv.mjs +37 -0
- package/.output/server/_ssr/bundleActivity-D17Pb_h5.mjs +19 -0
- package/.output/server/_ssr/card-DMbcvlPz.mjs +49 -0
- package/.output/server/_ssr/config-DMEJdhoW.mjs +13 -0
- package/.output/server/_ssr/config.server-DyypN6cQ.mjs +60 -0
- package/.output/server/_ssr/console-access-KpPv5lin.mjs +17 -0
- package/.output/server/_ssr/console-features-CsbyHYfK.mjs +55 -0
- package/.output/server/_ssr/console-features-api-B-KVuo4p.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-Djyo5740.mjs +19 -0
- package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-B_I3Ll4M.mjs} +28 -2
- package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-DuNm5mmo.mjs} +2 -2
- package/.output/server/_ssr/databaseRows-CXxf_VIB.mjs +132 -0
- package/.output/server/_ssr/{deleteBundle-BTGwBgzx.mjs → deleteBundle-Dcc21Lbd.mjs} +9 -19
- package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-cOmLTUV2.mjs} +2 -2
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-B-r4u3Lg.mjs} +28 -5
- package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-CexOFoZ4.mjs} +6 -7
- package/.output/server/_ssr/empty-BHoY_5jt.mjs +49 -0
- package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-mjhcNEXD.mjs} +63 -51
- package/.output/server/_ssr/getBundleChildren-DbDXe9wB.mjs +22 -0
- package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-96H30HWC.mjs} +2 -2
- package/.output/server/_ssr/insights-D25VDVaz.mjs +5 -0
- package/.output/server/_ssr/insights-D359GJFL.mjs +14 -0
- package/.output/server/_ssr/insights-DgFv1NsO.mjs +1238 -0
- package/.output/server/_ssr/insights-api-ChAKrbhG.mjs +105 -0
- package/.output/server/_ssr/insights-failures-WQ-ttjOx.mjs +44 -0
- package/.output/server/_ssr/insights-failures-rpc-BFsqlltN.mjs +21 -0
- package/.output/server/_ssr/insights-recovery-DkL6bxMW.mjs +28 -0
- package/.output/server/_ssr/insights-recovery-rpc-2P5m8F62.mjs +46 -0
- package/.output/server/_ssr/insights-recovery-rpc-Tj8vNC19.mjs +20 -0
- package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-DrkVmV9o.mjs} +22 -14
- package/.output/server/_ssr/insights-search-BsalnNw-.mjs +25 -0
- package/.output/server/_ssr/insights-usage-1nVTQrLy.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-Ct_4xHhZ.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-DaYIRtFK.mjs +41 -0
- package/.output/server/_ssr/insightsRecovery-CpGucWN1.mjs +45 -0
- package/.output/server/_ssr/insightsUsage-JYpKZAF7.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-Bcu6uYs_.mjs +5 -0
- package/.output/server/_ssr/insights_.distribution-D3Csuyjw.mjs +14 -0
- package/.output/server/_ssr/insights_.distribution-jQZEF07J.mjs +335 -0
- package/.output/server/_ssr/installations-B8Vh54lH.mjs +69 -0
- package/.output/server/_ssr/installations-BI5kelHv.mjs +5 -0
- package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-Cxikllmx.mjs} +517 -54
- package/.output/server/_ssr/listReleases-DqU9t1l3.mjs +79 -0
- package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
- package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-CRkm-wHD.mjs} +117 -124
- package/.output/server/_ssr/routes-B3IJo_jL.mjs +292 -0
- package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-CNAi0Mmv.mjs} +176 -455
- package/.output/server/_ssr/runtime.server-NucbYjc7.mjs +1372 -0
- package/.output/server/_ssr/select-BkPAJFL7.mjs +107 -0
- package/.output/server/_ssr/{server-D_C2rMzf.mjs → server-DKJYsaCP.mjs} +6 -32
- package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-Cw1VuC7d.mjs} +117 -63
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/table-q0P9wz7x.mjs +53 -0
- package/.output/server/_ssr/updateFailures-DMU9f0pK.mjs +24 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-BzSO-fr-.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-Bb0gKDbb.mjs +124 -0
- package/.output/server/index.mjs +273 -168
- package/README.md +71 -65
- package/lib/index.d.mts +13 -11
- package/lib/vite-internal.mjs +6 -3
- package/package.json +8 -8
- package/src/components/AppSidebar.tsx +91 -71
- package/src/components/ConsoleAccessPage.tsx +5 -1
- package/src/components/ConsoleFeatureUnavailable.tsx +82 -0
- package/src/components/NotFoundPage.tsx +7 -7
- package/src/components/features/FeatureSlots.tsx +99 -0
- package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
- package/src/components/features/bundles/BundleMetadata.tsx +7 -4
- package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
- package/src/components/features/insights/AppUsage.tsx +370 -0
- package/src/components/features/insights/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +117 -37
- package/src/components/features/insights/EventHistoryCard.tsx +112 -26
- package/src/components/features/insights/EventHistoryList.tsx +2 -2
- package/src/components/features/insights/InsightsControls.tsx +265 -102
- package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
- package/src/components/features/insights/InsightsInfo.tsx +48 -0
- package/src/components/features/insights/InsightsOverview.tsx +176 -173
- package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
- package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
- package/src/components/features/insights/InstallationHistoryCard.tsx +52 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +188 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
- package/src/components/features/insights/UpdateFailures.tsx +319 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -1
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/empty.tsx +101 -0
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/sidebar.tsx +22 -29
- package/src/components/ui/tabs.tsx +81 -0
- package/src/index.ts +12 -17
- package/src/lib/api-keys-api.ts +0 -28
- package/src/lib/api-keys-rpc.ts +27 -38
- package/src/lib/api-rpc.ts +197 -221
- package/src/lib/api.ts +11 -13
- package/src/lib/bundle-activity.ts +31 -0
- package/src/lib/console-access.ts +20 -0
- package/src/lib/console-features-api.ts +68 -0
- package/src/lib/console-features-rpc.ts +13 -0
- package/src/lib/console-features.ts +91 -0
- package/src/lib/insights-api.ts +37 -1
- package/src/lib/insights-failures-rpc.ts +21 -0
- package/src/lib/insights-failures.ts +129 -0
- package/src/lib/insights-recovery-rpc.ts +51 -0
- package/src/lib/insights-recovery.ts +68 -0
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +58 -0
- package/src/lib/insights-usage-rpc.ts +29 -0
- package/src/lib/insights-usage.ts +74 -0
- package/src/lib/insights-view.ts +38 -2
- package/src/lib/scroll-restoration.ts +21 -0
- package/src/lib/server/adminInsights.ts +124 -0
- package/src/lib/server/auth.server.ts +13 -5
- package/src/lib/server/bundleActivity.ts +37 -0
- package/src/lib/server/config.server.ts +16 -32
- package/src/lib/server/console-feature-apis.ts +59 -0
- package/src/lib/server/console-runtime.server.ts +6 -40
- package/src/lib/server/deleteBundle.ts +16 -25
- package/src/lib/server/downloadBundle.ts +7 -10
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsRecovery.ts +55 -0
- package/src/lib/server/insightsUsage.ts +63 -0
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseReachability.ts +4 -3
- package/src/lib/server/runtime.server.ts +132 -31
- package/src/lib/server/updateFailures.ts +31 -0
- package/src/routeTree.gen.ts +21 -21
- package/src/router.tsx +3 -2
- package/src/routes/-installations-search.ts +32 -8
- package/src/routes/-releases-search.ts +76 -29
- package/src/routes/__root.tsx +10 -4
- package/src/routes/api/bundles/$bundleId/download.ts +2 -5
- package/src/routes/api-keys.tsx +5 -2
- package/src/routes/index.tsx +167 -104
- package/src/routes/insights.tsx +100 -59
- package/src/routes/insights_.distribution.tsx +398 -0
- package/src/routes/installations.tsx +98 -41
- package/src/vite-internal.ts +6 -3
- package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
- package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
- package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
- package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
- package/.output/public/assets/card-Dhr_eFYj.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
- package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
- package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
- package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
- package/.output/public/assets/dist-C_NydL0e.js +0 -1
- package/.output/public/assets/index-DdAz-CXv.js +0 -10
- package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
- package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
- package/.output/public/assets/installations-CE33wgbY.js +0 -2
- package/.output/public/assets/installations-CStZja8x.js +0 -1
- package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
- package/.output/public/assets/link-CI8idAUi.js +0 -1
- package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
- package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
- package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
- package/.output/public/assets/routes-DGVXdzpw.js +0 -2
- package/.output/public/assets/routes-DTlwOr8I.js +0 -1
- package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
- package/.output/public/assets/signing-CMf4aqDO.js +0 -1
- package/.output/public/assets/styles-BCvbtKsm.css +0 -2
- package/.output/public/assets/table-2vcV89QB.js +0 -1
- package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
- package/.output/public/assets/x-DCd0j8c6.js +0 -1
- package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
- package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
- package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
- package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
- package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
- package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +0 -45
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
- package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
- package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
- package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
- package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
- package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
- package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
- package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
- package/src/components/features/signing/BundleSigningPage.tsx +0 -139
- package/src/lib/bundle-signing-api.ts +0 -14
- package/src/lib/bundle-signing-rpc.ts +0 -16
- package/src/lib/server/bundle-signing.server.ts +0 -23
- 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 };
|