@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
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
HotUpdaterCoreApi,
|
|
3
3
|
StoragePluginWith,
|
|
4
4
|
} from "@hot-updater/plugin-core";
|
|
5
5
|
|
|
6
6
|
interface DownloadBundleDependencies {
|
|
7
|
-
readonly
|
|
7
|
+
readonly core: Pick<HotUpdaterCoreApi, "getBundle">;
|
|
8
8
|
readonly storagePlugin?: StoragePluginWith<"get">;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const downloadBundle = async (
|
|
12
12
|
bundleId: string,
|
|
13
|
-
{
|
|
13
|
+
{ core, storagePlugin }: DownloadBundleDependencies,
|
|
14
14
|
): Promise<Response> => {
|
|
15
|
-
const
|
|
16
|
-
if (!
|
|
15
|
+
const detail = await core.getBundle(bundleId);
|
|
16
|
+
if (!detail) return new Response("Bundle not found", { status: 404 });
|
|
17
17
|
|
|
18
|
-
const storageUri = bundle.
|
|
19
|
-
if (!storageUri) {
|
|
20
|
-
return new Response("Bundle has no storage URI", { status: 404 });
|
|
21
|
-
}
|
|
18
|
+
const storageUri = detail.bundle.manifest_storage_uri;
|
|
22
19
|
|
|
23
20
|
const protocol = new URL(storageUri).protocol.replace(":", "");
|
|
24
21
|
if (storagePlugin?.protocol === protocol) {
|
|
@@ -28,7 +25,7 @@ export const downloadBundle = async (
|
|
|
28
25
|
|
|
29
26
|
const headers = new Headers(response.headers);
|
|
30
27
|
headers.set("cache-control", "private, no-store");
|
|
31
|
-
headers.set("content-disposition",
|
|
28
|
+
headers.set("content-disposition", 'attachment; filename="manifest.json"');
|
|
32
29
|
return new Response(response.body, {
|
|
33
30
|
headers,
|
|
34
31
|
status: response.status,
|
|
@@ -1,130 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type CursorPaginationInfo = {
|
|
15
|
-
hasNextPage: boolean;
|
|
16
|
-
nextCursor?: string | null;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
async function collectBundleChildrenByBaseIds(
|
|
20
|
-
baseBundleIds: string[],
|
|
21
|
-
deps: GetBundleChildrenDeps,
|
|
22
|
-
): Promise<Record<string, Bundle[]>> {
|
|
23
|
-
const uniqueBaseBundleIds = [...new Set(baseBundleIds.filter(Boolean))];
|
|
24
|
-
const bundlesByBaseId = Object.fromEntries(
|
|
25
|
-
uniqueBaseBundleIds.map((bundleId) => [bundleId, [] as Bundle[]]),
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
const baseBundles = (
|
|
29
|
-
await Promise.all(
|
|
30
|
-
uniqueBaseBundleIds.map((bundleId) =>
|
|
31
|
-
deps.databaseClient.getBundleById(bundleId),
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
).filter((bundle): bundle is Bundle => Boolean(bundle));
|
|
35
|
-
|
|
36
|
-
const groupMap = new Map<Bundle["platform"], Set<string>>();
|
|
37
|
-
|
|
38
|
-
for (const baseBundle of baseBundles) {
|
|
39
|
-
const existingGroup = groupMap.get(baseBundle.platform);
|
|
40
|
-
|
|
41
|
-
if (existingGroup) {
|
|
42
|
-
existingGroup.add(baseBundle.id);
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
groupMap.set(baseBundle.platform, new Set([baseBundle.id]));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
for (const [platform, bundleIds] of groupMap) {
|
|
50
|
-
const seenBundleIds = new Set<string>();
|
|
51
|
-
const seenCursors = new Set<string>();
|
|
52
|
-
let after: string | undefined;
|
|
53
|
-
|
|
54
|
-
while (true) {
|
|
55
|
-
const page = await deps.databaseClient.getBundles({
|
|
56
|
-
where: {
|
|
57
|
-
platform,
|
|
58
|
-
},
|
|
59
|
-
limit: CHILDREN_QUERY_LIMIT,
|
|
60
|
-
cursor: after ? { after } : undefined,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
for (const bundle of page.data) {
|
|
64
|
-
const parentBundleIds = getBundlePatches(bundle).map(
|
|
65
|
-
(patch) => patch.baseBundleId,
|
|
66
|
-
);
|
|
67
|
-
const matchedParentBundleIds = parentBundleIds.filter((bundleId) =>
|
|
68
|
-
bundleIds.has(bundleId),
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
matchedParentBundleIds.length === 0 ||
|
|
73
|
-
seenBundleIds.has(bundle.id)
|
|
74
|
-
) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
seenBundleIds.add(bundle.id);
|
|
79
|
-
for (const parentBundleId of matchedParentBundleIds) {
|
|
80
|
-
bundlesByBaseId[parentBundleId]?.push(bundle);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const pagination = page.pagination as CursorPaginationInfo;
|
|
85
|
-
const nextCursor = pagination.nextCursor ?? undefined;
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
!pagination.hasNextPage ||
|
|
89
|
-
!nextCursor ||
|
|
90
|
-
seenCursors.has(nextCursor)
|
|
91
|
-
) {
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
seenCursors.add(nextCursor);
|
|
96
|
-
after = nextCursor;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return bundlesByBaseId;
|
|
101
|
-
}
|
|
102
|
-
|
|
1
|
+
import {
|
|
2
|
+
type Bundle,
|
|
3
|
+
type HotUpdaterCoreApi,
|
|
4
|
+
rowToBundle,
|
|
5
|
+
} from "@hot-updater/plugin-core";
|
|
6
|
+
|
|
7
|
+
/** The most children the lineage panel lists; the count comes from the base bundle's counter. */
|
|
8
|
+
export const BUNDLE_CHILDREN_LIMIT = 100;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The bundles with a patch from this base, newest first: one read of the
|
|
12
|
+
* patches index, then each child bundle.
|
|
13
|
+
*/
|
|
103
14
|
export async function getBundleChildren(
|
|
104
|
-
|
|
105
|
-
|
|
15
|
+
core: Pick<HotUpdaterCoreApi, "getBundle" | "listPatchesFromBase">,
|
|
16
|
+
baseBundleId: string,
|
|
106
17
|
): Promise<Bundle[]> {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
18
|
+
const patches = await core.listPatchesFromBase(baseBundleId, {
|
|
19
|
+
order: "desc",
|
|
20
|
+
limit: BUNDLE_CHILDREN_LIMIT,
|
|
21
|
+
});
|
|
22
|
+
const details = await Promise.all(
|
|
23
|
+
[...new Set(patches.map(({ bundle_id }) => bundle_id))].map((id) =>
|
|
24
|
+
core.getBundle(id),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
return details.flatMap((detail) =>
|
|
28
|
+
detail === null ? [] : [rowToBundle(detail.bundle, detail.patches)],
|
|
110
29
|
);
|
|
111
|
-
|
|
112
|
-
return childrenByBaseId[baseBundleId] ?? [];
|
|
113
30
|
}
|
|
114
31
|
|
|
32
|
+
/** How many bundles patch from each base: each base bundle's reference counter, in one batch read. */
|
|
115
33
|
export async function getBundleChildCounts(
|
|
116
|
-
|
|
117
|
-
|
|
34
|
+
core: Pick<HotUpdaterCoreApi, "countBundleChildren">,
|
|
35
|
+
baseBundleIds: readonly string[],
|
|
118
36
|
): Promise<Record<string, number>> {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
deps,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
return Object.fromEntries(
|
|
125
|
-
Object.entries(childrenByBaseId).map(([bundleId, bundles]) => [
|
|
126
|
-
bundleId,
|
|
127
|
-
bundles.length,
|
|
128
|
-
]),
|
|
129
|
-
);
|
|
37
|
+
const ids = [...new Set(baseBundleIds.filter(Boolean))];
|
|
38
|
+
return ids.length === 0 ? {} : core.countBundleChildren(ids);
|
|
130
39
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { InsightsModel } from "@hot-updater/server/plugins/insights";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
insightsPeriodEnd,
|
|
5
|
+
readRecoveryInput,
|
|
6
|
+
recoveryWindows,
|
|
7
|
+
type RecoveryInput,
|
|
8
|
+
type RecoveryReport,
|
|
9
|
+
} from "../insights-recovery";
|
|
10
|
+
|
|
11
|
+
export async function getRecoveryReport(
|
|
12
|
+
model: InsightsModel,
|
|
13
|
+
input: RecoveryInput,
|
|
14
|
+
now = Date.now(),
|
|
15
|
+
): Promise<RecoveryReport> {
|
|
16
|
+
readRecoveryInput(input);
|
|
17
|
+
const end = insightsPeriodEnd(now);
|
|
18
|
+
const start = Math.max(0, end - recoveryWindows[input.window].durationMs);
|
|
19
|
+
const result = await model.getReleaseActivity(
|
|
20
|
+
input.releaseId === undefined
|
|
21
|
+
? {
|
|
22
|
+
scope: { platform: input.platform, channel: input.channel },
|
|
23
|
+
timeRange: { start, end },
|
|
24
|
+
}
|
|
25
|
+
: {
|
|
26
|
+
releases: [
|
|
27
|
+
{
|
|
28
|
+
releaseId: input.releaseId,
|
|
29
|
+
platform: input.platform,
|
|
30
|
+
channel: input.channel,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
timeRange: { start, end },
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
const metrics = result.data[0]?.metrics;
|
|
37
|
+
// The launches counter counts each installation once per UTC day it
|
|
38
|
+
// launched, so it reads as active days, and per day as daily active
|
|
39
|
+
// installations.
|
|
40
|
+
return {
|
|
41
|
+
downloads: metrics?.downloads ?? 0,
|
|
42
|
+
activeInstallations: metrics?.uniqueUsers ?? 0,
|
|
43
|
+
activeDays: metrics?.launches ?? 0,
|
|
44
|
+
failedLaunches: metrics?.failedLaunches ?? 0,
|
|
45
|
+
points: (metrics?.series ?? []).map((point) => ({
|
|
46
|
+
startMs: point.startMs,
|
|
47
|
+
dailyActiveInstallations: point.launches,
|
|
48
|
+
failedLaunches: point.failedLaunches,
|
|
49
|
+
})),
|
|
50
|
+
startMs: start,
|
|
51
|
+
endMs: end,
|
|
52
|
+
measuredAtMs: result.measuredAtMs,
|
|
53
|
+
coverage: result.coverage,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { InsightsModel } from "@hot-updater/server/plugins/insights";
|
|
2
|
+
|
|
3
|
+
import { insightsPeriodEnd, recoveryWindows } from "../insights-recovery";
|
|
4
|
+
import {
|
|
5
|
+
readAppUsageInput,
|
|
6
|
+
type AppUsageInput,
|
|
7
|
+
type AppUsageReport,
|
|
8
|
+
type UsageWindow,
|
|
9
|
+
} from "../insights-usage";
|
|
10
|
+
|
|
11
|
+
const DAY_MS = 86_400_000;
|
|
12
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What a period reads, ending with the current hour. The 12-month period is
|
|
16
|
+
* 52 weeks of whole UTC days, the last one today, so it reads the daily
|
|
17
|
+
* counts: the hourly ones are kept 90 days.
|
|
18
|
+
*/
|
|
19
|
+
const usageRange = (window: UsageWindow, now: number) => {
|
|
20
|
+
const end = insightsPeriodEnd(now);
|
|
21
|
+
if (window === "12m") {
|
|
22
|
+
const lastDay = Math.ceil(end / DAY_MS) * DAY_MS;
|
|
23
|
+
return {
|
|
24
|
+
start: Math.max(0, lastDay - 52 * WEEK_MS),
|
|
25
|
+
end,
|
|
26
|
+
intervalMs: WEEK_MS,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const { durationMs, intervalMs } = recoveryWindows[window];
|
|
30
|
+
return { start: Math.max(0, end - durationMs), end, intervalMs };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export async function getAppUsageReport(
|
|
34
|
+
model: InsightsModel,
|
|
35
|
+
input: AppUsageInput,
|
|
36
|
+
now = Date.now(),
|
|
37
|
+
): Promise<AppUsageReport> {
|
|
38
|
+
readAppUsageInput(input);
|
|
39
|
+
const { start, end, intervalMs } = usageRange(input.window, now);
|
|
40
|
+
const report = await model.getAppUsage({
|
|
41
|
+
channel: input.channel,
|
|
42
|
+
platform: input.platform,
|
|
43
|
+
...(input.appVersion === undefined ? {} : { appVersion: input.appVersion }),
|
|
44
|
+
timeRange: { start, end },
|
|
45
|
+
intervalMs,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
bundleDistribution: report.bundleDistribution,
|
|
49
|
+
activeInstallations: report.activeInstallations,
|
|
50
|
+
sinceMs:
|
|
51
|
+
report.coverage.sinceMs === null
|
|
52
|
+
? start
|
|
53
|
+
: Math.max(start, report.coverage.sinceMs),
|
|
54
|
+
distributionSinceMs: start - (start % DAY_MS),
|
|
55
|
+
beforeReceivedAtMs: end,
|
|
56
|
+
intervalMs,
|
|
57
|
+
truncated: report.coverage.kind === "partial",
|
|
58
|
+
appVersions: report.appVersions,
|
|
59
|
+
versions: report.versions,
|
|
60
|
+
platforms: report.platforms,
|
|
61
|
+
points: report.points,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -1,61 +1,126 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
HotUpdaterCoreApi,
|
|
3
|
+
ReleaseFilter,
|
|
4
|
+
ReleaseRow,
|
|
5
|
+
} from "@hot-updater/plugin-core";
|
|
2
6
|
|
|
3
7
|
export interface ReleaseListInput {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
platform?: "ios" | "android";
|
|
12
|
-
targetAppVersion?: string;
|
|
8
|
+
/** One of the filter sets the release indexes serve. */
|
|
9
|
+
readonly filter: ReleaseFilter;
|
|
10
|
+
/** Releases older than this id: the next page. */
|
|
11
|
+
readonly beforeReleaseId?: string;
|
|
12
|
+
/** Releases newer than this id: the previous page. */
|
|
13
|
+
readonly afterReleaseId?: string;
|
|
14
|
+
readonly limit: number;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface ReleaseListResult {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/** Newest first. */
|
|
19
|
+
readonly data: readonly ReleaseRow[];
|
|
20
|
+
/** `beforeReleaseId` of the next page, when there may be older releases. */
|
|
21
|
+
readonly next?: string;
|
|
22
|
+
/** `afterReleaseId` of the previous page, when there may be newer releases. */
|
|
23
|
+
readonly previous?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
27
|
+
typeof value === "object" && value !== null;
|
|
28
|
+
|
|
29
|
+
const text = (value: unknown, name: string): string => {
|
|
30
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
31
|
+
throw new Error(`Release filter ${name} is required.`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const enabledOf = (value: unknown) => {
|
|
37
|
+
if (value === undefined) return {};
|
|
38
|
+
if (typeof value !== "boolean") {
|
|
39
|
+
throw new Error("Release filter enabled must be true or false.");
|
|
40
|
+
}
|
|
41
|
+
return { enabled: value };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A release list's filter: none, a channel with its platform (and enabled),
|
|
46
|
+
* a bundle, or a scope (and enabled). Those are the sets the release indexes
|
|
47
|
+
* serve, so any other combination is refused.
|
|
48
|
+
*/
|
|
49
|
+
export const readReleaseFilter = (value: unknown): ReleaseFilter => {
|
|
50
|
+
if (value === undefined) return { kind: "all" };
|
|
51
|
+
if (!isRecord(value)) throw new Error("Invalid release filter.");
|
|
52
|
+
switch (value.kind) {
|
|
53
|
+
case "all":
|
|
54
|
+
return { kind: "all" };
|
|
55
|
+
case "channelPlatform": {
|
|
56
|
+
if (value.platform !== "ios" && value.platform !== "android") {
|
|
57
|
+
throw new Error("Release filter platform must be ios or android.");
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
kind: "channelPlatform",
|
|
61
|
+
channelId: text(value.channelId, "channelId"),
|
|
62
|
+
platform: value.platform,
|
|
63
|
+
...enabledOf(value.enabled),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case "bundle":
|
|
67
|
+
return { kind: "bundle", bundleId: text(value.bundleId, "bundleId") };
|
|
68
|
+
case "scope":
|
|
69
|
+
return {
|
|
70
|
+
kind: "scope",
|
|
71
|
+
scopeKey: text(value.scopeKey, "scopeKey"),
|
|
72
|
+
...enabledOf(value.enabled),
|
|
73
|
+
};
|
|
74
|
+
default:
|
|
75
|
+
throw new Error("Invalid release filter.");
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One page of releases, newest first, through the index the filter names:
|
|
81
|
+
* each page reads only its own rows. `next` is set for a full page, so the
|
|
82
|
+
* last page may lead to an empty one.
|
|
83
|
+
*/
|
|
24
84
|
export async function listReleases(
|
|
25
|
-
|
|
85
|
+
core: Pick<HotUpdaterCoreApi, "listReleases">,
|
|
26
86
|
input: ReleaseListInput,
|
|
27
87
|
): Promise<ReleaseListResult> {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
88
|
+
const { filter, limit } = input;
|
|
89
|
+
if (input.afterReleaseId !== undefined) {
|
|
90
|
+
// Newer releases, read oldest first from the cursor, then turned around.
|
|
91
|
+
const data = (
|
|
92
|
+
await core.listReleases({
|
|
93
|
+
filter,
|
|
94
|
+
order: "asc",
|
|
95
|
+
after: input.afterReleaseId,
|
|
96
|
+
limit,
|
|
97
|
+
})
|
|
98
|
+
).reverse();
|
|
99
|
+
const first = data[0]?.id;
|
|
100
|
+
const last = data.at(-1)?.id;
|
|
101
|
+
return {
|
|
102
|
+
data,
|
|
103
|
+
...(last === undefined ? {} : { next: last }),
|
|
104
|
+
...(first !== undefined && data.length === limit
|
|
105
|
+
? { previous: first }
|
|
106
|
+
: {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const data = await core.listReleases({
|
|
110
|
+
filter,
|
|
111
|
+
order: "desc",
|
|
112
|
+
limit,
|
|
33
113
|
...(input.beforeReleaseId === undefined
|
|
34
114
|
? {}
|
|
35
|
-
: {
|
|
36
|
-
...(input.bundleId === undefined ? {} : { bundleId: input.bundleId }),
|
|
37
|
-
...(input.channelId === undefined ? {} : { channelId: input.channelId }),
|
|
38
|
-
...(input.enabled === undefined ? {} : { enabled: input.enabled }),
|
|
39
|
-
...(input.platform === undefined ? {} : { platform: input.platform }),
|
|
40
|
-
...(input.targetAppVersion === undefined
|
|
41
|
-
? {}
|
|
42
|
-
: { targetAppVersion: input.targetAppVersion }),
|
|
43
|
-
limit: input.limit + 1,
|
|
115
|
+
: { after: input.beforeReleaseId }),
|
|
44
116
|
});
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const data = isPreviousRequest
|
|
48
|
-
? hasExtraRow
|
|
49
|
-
? rows.slice(1)
|
|
50
|
-
: rows
|
|
51
|
-
: rows.slice(0, input.limit);
|
|
52
|
-
|
|
117
|
+
const first = data[0]?.id;
|
|
118
|
+
const last = data.at(-1)?.id;
|
|
53
119
|
return {
|
|
54
120
|
data,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
},
|
|
121
|
+
...(last !== undefined && data.length === limit ? { next: last } : {}),
|
|
122
|
+
...(first !== undefined && input.beforeReleaseId !== undefined
|
|
123
|
+
? { previous: first }
|
|
124
|
+
: {}),
|
|
60
125
|
};
|
|
61
126
|
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "@hot-updater/core";
|
|
5
5
|
import type {
|
|
6
6
|
CompiledReleaseCatalog,
|
|
7
|
-
|
|
7
|
+
HotUpdaterCoreApi,
|
|
8
8
|
ReleaseRow,
|
|
9
9
|
} from "@hot-updater/plugin-core";
|
|
10
10
|
|
|
@@ -63,15 +63,16 @@ export function collectCurrentlyReachableReleaseIds(
|
|
|
63
63
|
return reachableReleaseIds;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Marks each release its scope's compiled catalog no longer serves: one catalog read per scope. */
|
|
66
67
|
export async function addReleaseReachability(
|
|
67
|
-
|
|
68
|
+
core: Pick<HotUpdaterCoreApi, "getReleaseCatalogRow">,
|
|
68
69
|
releases: readonly ReleaseRow[],
|
|
69
70
|
): Promise<readonly ReleaseReachabilityRow[]> {
|
|
70
71
|
const scopeKeys = [...new Set(releases.map(({ scope_key }) => scope_key))];
|
|
71
72
|
const reachableByScope = new Map(
|
|
72
73
|
await Promise.all(
|
|
73
74
|
scopeKeys.map(async (scopeKey) => {
|
|
74
|
-
const row = await
|
|
75
|
+
const row = await core.getReleaseCatalogRow(scopeKey);
|
|
75
76
|
const reachableReleaseIds =
|
|
76
77
|
row === null
|
|
77
78
|
? new Set<string>()
|
|
@@ -1,36 +1,137 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
DatabasePlugin,
|
|
1
|
+
import {
|
|
2
|
+
type ConfiguredDatabase,
|
|
3
|
+
isRemoteDatabase,
|
|
5
4
|
} from "@hot-updater/plugin-core";
|
|
5
|
+
import { createDatabasePluginApis } from "@hot-updater/server/db";
|
|
6
|
+
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
type ConsoleFeature,
|
|
9
|
+
ConsoleFeatureUnavailableError,
|
|
10
|
+
type ConsoleFeatures,
|
|
11
|
+
consoleFeatures,
|
|
12
|
+
resolveConsoleFeatures,
|
|
13
|
+
} from "../console-features";
|
|
14
|
+
import {
|
|
15
|
+
type ConsoleFeatureApis,
|
|
16
|
+
consoleFeatureApis,
|
|
17
|
+
type FetchAdmin,
|
|
18
|
+
} from "./console-feature-apis";
|
|
19
|
+
|
|
20
|
+
export type { ConsoleFeatureApis } from "./console-feature-apis";
|
|
21
|
+
|
|
22
|
+
export interface ConsoleRuntime {
|
|
23
|
+
/** Whether the console reaches a self-hosted server through its admin API. */
|
|
24
|
+
readonly remote: boolean;
|
|
25
|
+
/** The features on here, resolved once from the plugins the server runs. */
|
|
26
|
+
features(): Promise<ConsoleFeatures>;
|
|
27
|
+
/** What serves the features the console can serve here; {@link requireFeature} hands it out. */
|
|
28
|
+
readonly apis: Partial<ConsoleFeatureApis>;
|
|
15
29
|
}
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
apiKeys === null ||
|
|
28
|
-
typeof Reflect.get(apiKeys, "create") !== "function" ||
|
|
29
|
-
typeof Reflect.get(apiKeys, "findByHash") !== "function" ||
|
|
30
|
-
typeof Reflect.get(apiKeys, "list") !== "function" ||
|
|
31
|
-
typeof Reflect.get(apiKeys, "revoke") !== "function"
|
|
32
|
-
) {
|
|
33
|
-
return null;
|
|
31
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
32
|
+
typeof value === "object" && value !== null;
|
|
33
|
+
|
|
34
|
+
/** The ids of the plugins a self-hosted server lists on its admin `/version`. */
|
|
35
|
+
const readServerPlugins = async (
|
|
36
|
+
fetchAdmin: FetchAdmin,
|
|
37
|
+
): Promise<readonly string[]> => {
|
|
38
|
+
const response = await fetchAdmin("/version");
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`The server answered /version with ${response.status}.`);
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
const body: unknown = await response.json();
|
|
43
|
+
const plugins = isRecord(body) ? body.plugins : undefined;
|
|
44
|
+
// A server from before `/version` listed its plugins reports none, so the
|
|
45
|
+
// console serves none of their features until the server is upgraded.
|
|
46
|
+
return Array.isArray(plugins)
|
|
47
|
+
? plugins.filter((id): id is string => typeof id === "string")
|
|
48
|
+
: [];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Runs `load` once and keeps its result; a failure is not kept, so the next call retries. */
|
|
52
|
+
const once = <T>(load: () => Promise<T>): (() => Promise<T>) => {
|
|
53
|
+
let pending: Promise<T> | undefined;
|
|
54
|
+
return () =>
|
|
55
|
+
(pending ??= load().catch((error: unknown) => {
|
|
56
|
+
pending = undefined;
|
|
57
|
+
throw error;
|
|
58
|
+
}));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const featureIds = Object.keys(consoleFeatureApis) as ConsoleFeature[];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What serves each feature, built by its factory: over the plugins' APIs the
|
|
65
|
+
* console assembled, or over a self-hosted server's admin API. A feature
|
|
66
|
+
* without its plugin, or without a factory for the admin API, has none.
|
|
67
|
+
*/
|
|
68
|
+
const featureApis = (
|
|
69
|
+
build: (feature: ConsoleFeature) => unknown,
|
|
70
|
+
): Partial<ConsoleFeatureApis> =>
|
|
71
|
+
Object.fromEntries(
|
|
72
|
+
featureIds.flatMap((feature) => {
|
|
73
|
+
const api = build(feature);
|
|
74
|
+
return api === undefined ? [] : [[feature, api]];
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The console's features and what serves them, for a console config:
|
|
80
|
+
* - a self-hosted server (`standaloneRepository`) runs its plugins and lists
|
|
81
|
+
* them on its admin `/version`; the console serves what that server's admin
|
|
82
|
+
* API serves, and nothing that needs the database;
|
|
83
|
+
* - otherwise the console assembles `plugins` over the database, as the
|
|
84
|
+
* server does, and serves the features of the plugins it assembled.
|
|
85
|
+
*/
|
|
86
|
+
export const createConsoleRuntime = (config: {
|
|
87
|
+
readonly database: ConfiguredDatabase;
|
|
88
|
+
readonly plugins?: readonly unknown[];
|
|
89
|
+
}): ConsoleRuntime => {
|
|
90
|
+
const { database } = config;
|
|
91
|
+
if (isRemoteDatabase(database)) {
|
|
92
|
+
const { fetchAdmin } = database;
|
|
93
|
+
return {
|
|
94
|
+
remote: true,
|
|
95
|
+
features: once(async () =>
|
|
96
|
+
resolveConsoleFeatures(await readServerPlugins(fetchAdmin), {
|
|
97
|
+
remote: true,
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
100
|
+
apis: featureApis((feature) =>
|
|
101
|
+
consoleFeatureApis[feature].remote?.(fetchAdmin),
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const api = createDatabasePluginApis(database, config.plugins ?? []);
|
|
106
|
+
const features = resolveConsoleFeatures(Object.keys(api), { remote: false });
|
|
107
|
+
return {
|
|
108
|
+
remote: false,
|
|
109
|
+
features: async () => features,
|
|
110
|
+
// A feature reads the plugin with its plugin's id, taken to be Hot
|
|
111
|
+
// Updater's own, as a self-hosted server's /version list is.
|
|
112
|
+
apis: featureApis((feature) => {
|
|
113
|
+
const pluginApi = api[consoleFeatures[feature].plugin];
|
|
114
|
+
return pluginApi === undefined
|
|
115
|
+
? undefined
|
|
116
|
+
: consoleFeatureApis[feature].local(pluginApi as never);
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* What serves `feature`. Every feature's server function asks for it first,
|
|
123
|
+
* so a feature the console does not serve is refused with one error the
|
|
124
|
+
* client recognizes, before anything is read.
|
|
125
|
+
*/
|
|
126
|
+
export const requireFeature = async <F extends ConsoleFeature>(
|
|
127
|
+
runtime: ConsoleRuntime,
|
|
128
|
+
feature: F,
|
|
129
|
+
): Promise<ConsoleFeatureApis[F]> => {
|
|
130
|
+
const api = runtime.apis[feature];
|
|
131
|
+
if (api === undefined || !(await runtime.features())[feature]) {
|
|
132
|
+
throw new ConsoleFeatureUnavailableError(feature, {
|
|
133
|
+
remote: runtime.remote,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return api as ConsoleFeatureApis[F];
|
|
137
|
+
};
|