@hot-updater/console 0.36.7 → 1.0.0-rc.1
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/BundleIdDisplay-D4ErdU5_.js +1 -0
- package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +1 -0
- package/.output/public/assets/api-keys-api-DGVTTvTJ.js +1 -0
- package/.output/public/assets/api-keys-z16OZIP4.js +1 -0
- package/.output/public/assets/badge-Br_KDO0J.js +1 -0
- package/.output/public/assets/card-DzuEXvyJ.js +1 -0
- package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
- package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
- package/.output/public/assets/dialog-BeAENgez.js +1 -0
- package/.output/public/assets/dist-Bssh_cv0.js +1 -0
- package/.output/public/assets/field-Buq148-A.js +1 -0
- package/.output/public/assets/index-uOpQAIC4.js +10 -0
- package/.output/public/assets/input-group-B3p5UpyW.js +1 -0
- package/.output/public/assets/insights-DJTr7Ozn.js +1 -0
- package/.output/public/assets/insights-api-DVMOoRMS.js +1 -0
- package/.output/public/assets/installations-BY64iIAK.js +2 -0
- package/.output/public/assets/installations-D5v9i3AL.js +1 -0
- package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +1 -0
- package/.output/public/assets/preload-helper-CQ36nxok.js +1 -0
- package/.output/public/assets/refresh-cw-BkbfZ62O.js +1 -0
- package/.output/public/assets/routes-BuoI5-Cr.js +2 -0
- package/.output/public/assets/routes-C8fvgETO.js +1 -0
- package/.output/public/assets/search-qsIDvS2I.js +1 -0
- package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
- package/.output/public/assets/signing-mJD88Y4C.js +1 -0
- package/.output/public/assets/styles-BZIG7t42.css +2 -0
- package/.output/public/assets/table-RamFEclP.js +1 -0
- package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +42 -0
- package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
- package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-Dftxlm2E.mjs +146 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
- 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 +1134 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +141 -13
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14117 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__query-core.mjs +114 -1
- package/.output/server/_libs/tanstack__react-query.mjs +3 -4
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +288 -0
- package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +13 -0
- package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +15 -0
- package/.output/server/_ssr/api-keys-CpBqHpuC.mjs +300 -0
- package/.output/server/_ssr/api-keys-api-Czkp57UV.mjs +60 -0
- package/.output/server/_ssr/api-keys-rpc-BTtMDSoR.mjs +136 -0
- package/.output/server/_ssr/api-rpc-BEX8ISai.mjs +1468 -0
- package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
- package/.output/server/_ssr/auth-rpc-BFMEC-TT.mjs +24 -0
- package/.output/server/_ssr/auth.server-DW71WzsD.mjs +47 -0
- package/.output/server/_ssr/badge-CpWXQwyF.mjs +26 -0
- package/.output/server/_ssr/bundle-signing-rpc-BbCFUQSu.mjs +16 -0
- package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
- package/.output/server/_ssr/card-Bsc8mRis.mjs +86 -0
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
- package/.output/server/_ssr/config.server-DQYEr_5X.mjs +361 -0
- package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
- package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
- package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-DhfrxEjg.mjs} +68 -50
- package/.output/server/_ssr/databaseRows-QlCYLi0H.mjs +166 -0
- package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
- package/.output/server/_ssr/dialog-C0vKUp8R.mjs +170 -0
- package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
- package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
- package/.output/server/_ssr/field-DASCh-5d.mjs +74 -0
- package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
- package/.output/server/_ssr/input-group-etIsBUqv.mjs +288 -0
- package/.output/server/_ssr/insights-api-C1F7UreR.mjs +71 -0
- package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +96 -0
- package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +155 -0
- package/.output/server/_ssr/insights-ygT_aIfo.mjs +858 -0
- package/.output/server/_ssr/installations-DqRT1QTd.mjs +535 -0
- package/.output/server/_ssr/installations-K8TCYTYh.mjs +19 -0
- package/.output/server/_ssr/router-BxBEvtF1.mjs +513 -0
- package/.output/server/_ssr/routes-BC7rn-tf.mjs +37 -0
- package/.output/server/_ssr/routes-CM5KAwpB.mjs +2285 -0
- package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +527 -0
- package/.output/server/_ssr/server-CpzW--lT.mjs +7468 -0
- package/.output/server/_ssr/signing-jk4HANnl.mjs +134 -0
- package/.output/server/_ssr/ssr.mjs +4 -5740
- package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
- package/.output/server/_ssr/table-CU58HeI7.mjs +53 -0
- package/.output/server/_ssr/uuidv7-CaKuKXrV.mjs +31 -0
- package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +103 -0
- package/.output/server/index.mjs +229 -47
- package/README.md +29 -8
- package/lib/index.d.mts +35 -0
- package/lib/index.mjs +4 -0
- package/lib/vite-internal.mjs +127 -0
- package/lib/vite.d.mts +10 -0
- package/lib/vite.mjs +5 -0
- package/package.json +51 -29
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo.svg +1 -0
- package/public/manifest.json +30 -0
- package/public/robots.txt +3 -0
- package/src/components/AppSidebar.tsx +159 -0
- package/src/components/BundleIdDisplay.tsx +18 -0
- package/src/components/ChannelBadge.tsx +31 -0
- package/src/components/ConsoleAccessPage.tsx +122 -0
- package/src/components/EnabledStatusIcon.tsx +54 -0
- package/src/components/HashValueDisplay.tsx +85 -0
- package/src/components/HotUpdaterLogo.tsx +30 -0
- package/src/components/NotFoundPage.tsx +33 -0
- package/src/components/PlatformIcon.tsx +39 -0
- package/src/components/RolloutPercentageBadge.tsx +28 -0
- package/src/components/ThemeProvider.tsx +116 -0
- package/src/components/TimestampDisplay.tsx +41 -0
- package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
- package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
- package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
- package/src/components/features/bundles/BundleInsightsSummary.tsx +123 -0
- package/src/components/features/bundles/BundleMetadata.tsx +136 -0
- package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
- package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
- package/src/components/features/insights/ActivityChart.tsx +150 -0
- package/src/components/features/insights/BundleDistribution.tsx +133 -0
- package/src/components/features/insights/BundleSelector.tsx +114 -0
- package/src/components/features/insights/InsightsCapabilityContext.tsx +24 -0
- package/src/components/features/insights/InsightsControls.tsx +115 -0
- package/src/components/features/insights/InsightsErrorAlert.tsx +21 -0
- package/src/components/features/insights/InsightsOverview.tsx +208 -0
- package/src/components/features/insights/InstallationHistoryCard.tsx +248 -0
- package/src/components/features/insights/InstallationMatchesCard.tsx +132 -0
- package/src/components/features/insights/InstallationPageHeader.tsx +138 -0
- package/src/components/features/insights/InstallationPagination.tsx +48 -0
- package/src/components/features/insights/InstallationSearch.tsx +230 -0
- package/src/components/features/insights/UpdateOutcomes.tsx +147 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
- package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
- package/src/components/features/signing/BundleSigningPage.tsx +139 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +89 -0
- package/src/components/ui/chart.tsx +371 -0
- package/src/components/ui/combobox.tsx +303 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/dropdown-menu.tsx +270 -0
- package/src/components/ui/field.tsx +235 -0
- package/src/components/ui/input-group.tsx +147 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/progress.tsx +84 -0
- package/src/components/ui/select.tsx +205 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +132 -0
- package/src/components/ui/sidebar.tsx +779 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +56 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/switch.tsx +25 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +30 -0
- package/src/index.ts +47 -0
- package/src/lib/api-keys-api.ts +69 -0
- package/src/lib/api-keys-rpc.ts +87 -0
- package/src/lib/api-rpc.ts +478 -0
- package/src/lib/api.ts +444 -0
- package/src/lib/auth-rpc.ts +16 -0
- package/src/lib/bundle-signing-api.ts +14 -0
- package/src/lib/bundle-signing-rpc.ts +16 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/git.ts +51 -0
- package/src/lib/insights-api.ts +144 -0
- package/src/lib/insights-error.ts +19 -0
- package/src/lib/insights-input.ts +187 -0
- package/src/lib/insights-overview.ts +170 -0
- package/src/lib/insights-rpc.ts +218 -0
- package/src/lib/server/auth.server.ts +43 -0
- package/src/lib/server/bundle-signing.server.ts +23 -0
- package/src/lib/server/config.server.ts +109 -0
- package/src/lib/server/console-runtime.server.ts +61 -0
- package/src/lib/server/deleteBundle.ts +145 -0
- package/src/lib/server/downloadBundle.ts +46 -0
- package/src/lib/server/getBundleChildren.ts +130 -0
- package/src/lib/server/listReleases.ts +61 -0
- package/src/lib/server/releaseActivity.ts +27 -0
- package/src/lib/server/releaseReachability.ts +95 -0
- package/src/lib/server/runtime.server.ts +172 -0
- package/src/lib/utils.ts +9 -0
- package/src/logo.svg +1 -0
- package/src/routeTree.gen.ts +199 -0
- package/src/router.tsx +21 -0
- package/src/routes/-installations-search.ts +19 -0
- package/src/routes/-releases-search.ts +74 -0
- package/src/routes/__root.tsx +174 -0
- package/src/routes/api/auth/$.ts +19 -0
- package/src/routes/api/bundles/$bundleId/download.ts +17 -0
- package/src/routes/api-keys.tsx +37 -0
- package/src/routes/index.tsx +982 -0
- package/src/routes/insights.tsx +139 -0
- package/src/routes/installations.tsx +194 -0
- package/src/routes/signing.tsx +37 -0
- package/src/styles.css +152 -0
- package/src/virtual-modules.d.ts +21 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vite-internal.ts +195 -0
- package/src/vite.ts +12 -0
- package/.output/public/assets/dist-DJX53faP.js +0 -3
- package/.output/public/assets/index-Iylis5kY.js +0 -10
- package/.output/public/assets/routes-HT21hs0C.js +0 -54
- package/.output/public/assets/styles-C0Re4quB.css +0 -2
- package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
- package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
- package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
- package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
- package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
- package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
- package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
- package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
- package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
- package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
- package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
- package/.output/server/_libs/radix-ui__number.mjs +0 -6
- package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
- package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
- package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
- package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
- package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
- package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
- package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
- package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
- package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
- package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
- package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
- package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
- package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
- package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
- package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
- package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
- package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
- package/.output/server/node_modules/tslib/modules/index.js +0 -70
- package/.output/server/node_modules/tslib/modules/package.json +0 -3
- package/.output/server/node_modules/tslib/package.json +0 -47
- package/.output/server/node_modules/tslib/tslib.js +0 -484
- package/.output/server/package.json +0 -9
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertStorageOperations,
|
|
3
|
+
createDatabaseClient,
|
|
4
|
+
type DatabaseClient,
|
|
5
|
+
type StoragePluginWith,
|
|
6
|
+
} from "@hot-updater/plugin-core";
|
|
7
|
+
import { getRequest } from "@tanstack/react-start/server";
|
|
8
|
+
|
|
9
|
+
import type { ResolvedHotUpdaterConsoleConfig } from "../../index";
|
|
10
|
+
import { requireConsoleAccess } from "./auth.server";
|
|
11
|
+
import { resolveConsoleConfig } from "./console-runtime.server";
|
|
12
|
+
|
|
13
|
+
type ResolvedConsoleConfig = ResolvedHotUpdaterConsoleConfig & {
|
|
14
|
+
readonly console: NonNullable<ResolvedHotUpdaterConsoleConfig["console"]>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let configPromise: Promise<ResolvedConsoleConfig> | null = null;
|
|
18
|
+
let databaseClient: DatabaseClient | null = null;
|
|
19
|
+
let hotUpdater: ReturnType<
|
|
20
|
+
typeof import("./runtime.server").createRuntimeHotUpdater
|
|
21
|
+
> | null = null;
|
|
22
|
+
let apiKeyStore: ReturnType<
|
|
23
|
+
typeof import("./runtime.server").createApiKeyStore
|
|
24
|
+
> | null = null;
|
|
25
|
+
let apiKeyStoreResolved = false;
|
|
26
|
+
let storagePluginPromise: Promise<
|
|
27
|
+
StoragePluginWith<"get" | "put" | "exists" | "delete">
|
|
28
|
+
> | null = null;
|
|
29
|
+
|
|
30
|
+
const loadCachedConfig = async (request: Request) => {
|
|
31
|
+
if (!configPromise) {
|
|
32
|
+
configPromise = resolveConsoleConfig(request)
|
|
33
|
+
.then((config) => ({
|
|
34
|
+
...config,
|
|
35
|
+
console: config.console ?? {},
|
|
36
|
+
}))
|
|
37
|
+
.catch((error) => {
|
|
38
|
+
configPromise = null;
|
|
39
|
+
throw error;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return configPromise;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const loadCachedStoragePlugin = async (config: ResolvedConsoleConfig) => {
|
|
47
|
+
if (!storagePluginPromise) {
|
|
48
|
+
storagePluginPromise = Promise.resolve(config.storage)
|
|
49
|
+
.then((storagePlugin) => {
|
|
50
|
+
assertStorageOperations(storagePlugin, [
|
|
51
|
+
"get",
|
|
52
|
+
"put",
|
|
53
|
+
"exists",
|
|
54
|
+
"delete",
|
|
55
|
+
]);
|
|
56
|
+
return storagePlugin;
|
|
57
|
+
})
|
|
58
|
+
.catch((error) => {
|
|
59
|
+
storagePluginPromise = null;
|
|
60
|
+
throw error;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return storagePluginPromise;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const prepareConfig = async (request: Request = getRequest()) => {
|
|
68
|
+
try {
|
|
69
|
+
await requireConsoleAccess(request);
|
|
70
|
+
const config = await loadCachedConfig(request);
|
|
71
|
+
|
|
72
|
+
if (!databaseClient) {
|
|
73
|
+
databaseClient = createDatabaseClient(config.database);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!hotUpdater) {
|
|
77
|
+
const { createRuntimeHotUpdater } = await import("./runtime.server");
|
|
78
|
+
hotUpdater = createRuntimeHotUpdater(config);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!apiKeyStoreResolved) {
|
|
82
|
+
const { createApiKeyStore } = await import("./runtime.server");
|
|
83
|
+
apiKeyStore = createApiKeyStore(config);
|
|
84
|
+
apiKeyStoreResolved = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const storagePlugin = await loadCachedStoragePlugin(config);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
config,
|
|
91
|
+
databaseClient,
|
|
92
|
+
hotUpdater,
|
|
93
|
+
apiKeyStore,
|
|
94
|
+
storagePlugin,
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (
|
|
98
|
+
!(
|
|
99
|
+
error instanceof Response &&
|
|
100
|
+
(error.status === 401 || error.status === 403)
|
|
101
|
+
)
|
|
102
|
+
) {
|
|
103
|
+
console.error("Error during configuration initialization:", error);
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const isConfigLoaded = () => Boolean(configPromise);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConsoleAuthAdapter,
|
|
3
|
+
ConsoleSigningConfig,
|
|
4
|
+
HotUpdaterConsoleConfigSource,
|
|
5
|
+
ResolvedHotUpdaterConsoleConfig,
|
|
6
|
+
} from "../../index";
|
|
7
|
+
|
|
8
|
+
const readString = (value: unknown, key: string): string | undefined => {
|
|
9
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
10
|
+
const member = Reflect.get(value, key);
|
|
11
|
+
return typeof member === "string" && member.trim().length > 0
|
|
12
|
+
? member.trim()
|
|
13
|
+
: undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const getProviderName = (signing: unknown): string => {
|
|
17
|
+
return (
|
|
18
|
+
readString(signing, "name")?.slice(0, 80) ??
|
|
19
|
+
(readString(signing, "privateKeyPath") ? "localSigning" : undefined) ??
|
|
20
|
+
"Configured provider"
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const sanitizeConsoleSigningConfig = (
|
|
25
|
+
signing: unknown,
|
|
26
|
+
): ConsoleSigningConfig | undefined => {
|
|
27
|
+
if (typeof signing !== "object" || signing === null) return undefined;
|
|
28
|
+
const privateKeyPath = readString(signing, "privateKeyPath");
|
|
29
|
+
if (
|
|
30
|
+
Reflect.get(signing, "enabled") === false ||
|
|
31
|
+
(privateKeyPath && Reflect.get(signing, "enabled") !== true)
|
|
32
|
+
)
|
|
33
|
+
return undefined;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
enabled: true,
|
|
37
|
+
provider: getProviderName(signing),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const getConsoleAuthAdapter = async (): Promise<ConsoleAuthAdapter> => {
|
|
42
|
+
const module = await import("virtual:hot-updater-console/auth");
|
|
43
|
+
return module.default;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const resolveConsoleConfig = async (
|
|
47
|
+
request: Request,
|
|
48
|
+
): Promise<ResolvedHotUpdaterConsoleConfig> => {
|
|
49
|
+
const { default: source } =
|
|
50
|
+
(await import("virtual:hot-updater-console/config")) as {
|
|
51
|
+
readonly default: HotUpdaterConsoleConfigSource;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const config = typeof source === "function" ? await source(request) : source;
|
|
55
|
+
const { signing: signingSource, ...configWithoutSigning } = config;
|
|
56
|
+
const signing = sanitizeConsoleSigningConfig(signingSource);
|
|
57
|
+
return {
|
|
58
|
+
...configWithoutSigning,
|
|
59
|
+
...(signing === undefined ? {} : { signing }),
|
|
60
|
+
};
|
|
61
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBundlePatches,
|
|
3
|
+
getManifestStorageUri,
|
|
4
|
+
getPatchStorageUri,
|
|
5
|
+
} from "@hot-updater/core";
|
|
6
|
+
import type {
|
|
7
|
+
Bundle,
|
|
8
|
+
DatabaseClient,
|
|
9
|
+
StoragePluginWith,
|
|
10
|
+
} from "@hot-updater/plugin-core";
|
|
11
|
+
|
|
12
|
+
interface DeleteBundleInput {
|
|
13
|
+
bundleId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DeleteBundlesInput {
|
|
17
|
+
bundleIds: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DeleteBundleDependencies {
|
|
21
|
+
databaseClient: DatabaseClient;
|
|
22
|
+
storagePlugin: StoragePluginWith<"delete">;
|
|
23
|
+
waitForStorageCleanup?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveStorageUriForDeletion(
|
|
27
|
+
storageUri: string,
|
|
28
|
+
storagePlugin: StoragePluginWith<"delete">,
|
|
29
|
+
) {
|
|
30
|
+
const protocol = new URL(storageUri).protocol.replace(":", "");
|
|
31
|
+
|
|
32
|
+
if (storagePlugin.protocol === protocol) {
|
|
33
|
+
return storageUri;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (protocol === "http" || protocol === "https") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error(`No storage plugin for protocol: ${protocol}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function cleanupBundleStorage(
|
|
44
|
+
bundle: Bundle,
|
|
45
|
+
storagePlugin: StoragePluginWith<"delete">,
|
|
46
|
+
) {
|
|
47
|
+
const cleanupUris = new Set<string>();
|
|
48
|
+
const addCleanupUri = (storageUri: string | undefined) => {
|
|
49
|
+
if (!storageUri) return;
|
|
50
|
+
const resolvedStorageUri = resolveStorageUriForDeletion(
|
|
51
|
+
storageUri,
|
|
52
|
+
storagePlugin,
|
|
53
|
+
);
|
|
54
|
+
if (resolvedStorageUri) cleanupUris.add(resolvedStorageUri);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
addCleanupUri(bundle.storageUri);
|
|
58
|
+
addCleanupUri(getManifestStorageUri(bundle) ?? undefined);
|
|
59
|
+
addCleanupUri(getPatchStorageUri(bundle) ?? undefined);
|
|
60
|
+
for (const patch of getBundlePatches(bundle)) {
|
|
61
|
+
addCleanupUri(patch.patchStorageUri);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const storageUri of cleanupUris) {
|
|
65
|
+
try {
|
|
66
|
+
await storagePlugin.delete({ storageUri });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("Failed to delete bundle from storage:", error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function deleteBundles(
|
|
74
|
+
{ bundleIds }: DeleteBundlesInput,
|
|
75
|
+
{
|
|
76
|
+
databaseClient,
|
|
77
|
+
storagePlugin,
|
|
78
|
+
waitForStorageCleanup = true,
|
|
79
|
+
}: DeleteBundleDependencies,
|
|
80
|
+
) {
|
|
81
|
+
const uniqueBundleIds = [...new Set(bundleIds)];
|
|
82
|
+
const { data: matchedBundles } = await databaseClient.getBundles({
|
|
83
|
+
where: { id: { in: uniqueBundleIds } },
|
|
84
|
+
limit: uniqueBundleIds.length,
|
|
85
|
+
});
|
|
86
|
+
const matchedById = new Map(
|
|
87
|
+
matchedBundles.map((bundle) => [bundle.id, bundle]),
|
|
88
|
+
);
|
|
89
|
+
const bundles = uniqueBundleIds.flatMap((bundleId) => {
|
|
90
|
+
const bundle = matchedById.get(bundleId);
|
|
91
|
+
return bundle ? [bundle] : [];
|
|
92
|
+
});
|
|
93
|
+
const missingBundleIds = uniqueBundleIds.filter(
|
|
94
|
+
(bundleId) => !matchedById.has(bundleId),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
for (const bundle of bundles) {
|
|
98
|
+
const cleanupCandidates = [
|
|
99
|
+
bundle.storageUri,
|
|
100
|
+
getManifestStorageUri(bundle),
|
|
101
|
+
getPatchStorageUri(bundle),
|
|
102
|
+
...getBundlePatches(bundle).map((patch) => patch.patchStorageUri),
|
|
103
|
+
].filter((value): value is string => Boolean(value));
|
|
104
|
+
for (const candidate of cleanupCandidates) {
|
|
105
|
+
resolveStorageUriForDeletion(candidate, storagePlugin);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (bundles.length > 0) {
|
|
110
|
+
await databaseClient.mutate(async (mutation) => {
|
|
111
|
+
for (const bundle of bundles) {
|
|
112
|
+
await mutation.deleteBundleById(bundle.id);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cleanupStorage = async () => {
|
|
118
|
+
for (const bundle of bundles) {
|
|
119
|
+
await cleanupBundleStorage(bundle, storagePlugin);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (waitForStorageCleanup) {
|
|
124
|
+
await cleanupStorage();
|
|
125
|
+
} else {
|
|
126
|
+
void cleanupStorage().catch((error) => {
|
|
127
|
+
console.error("Failed to clean up bundle storage:", error);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
deletedBundleIds: bundles.map((bundle) => bundle.id),
|
|
133
|
+
missingBundleIds,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function deleteBundle(
|
|
138
|
+
{ bundleId }: DeleteBundleInput,
|
|
139
|
+
dependencies: DeleteBundleDependencies,
|
|
140
|
+
) {
|
|
141
|
+
const result = await deleteBundles({ bundleIds: [bundleId] }, dependencies);
|
|
142
|
+
if (result.missingBundleIds.length > 0) {
|
|
143
|
+
throw new Error(`Bundle not found: ${bundleId}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatabaseClient,
|
|
3
|
+
StoragePluginWith,
|
|
4
|
+
} from "@hot-updater/plugin-core";
|
|
5
|
+
|
|
6
|
+
interface DownloadBundleDependencies {
|
|
7
|
+
readonly databaseClient: DatabaseClient;
|
|
8
|
+
readonly storagePlugin?: StoragePluginWith<"get">;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const downloadBundle = async (
|
|
12
|
+
bundleId: string,
|
|
13
|
+
{ databaseClient, storagePlugin }: DownloadBundleDependencies,
|
|
14
|
+
): Promise<Response> => {
|
|
15
|
+
const bundle = await databaseClient.getBundleById(bundleId);
|
|
16
|
+
if (!bundle) return new Response("Bundle not found", { status: 404 });
|
|
17
|
+
|
|
18
|
+
const storageUri = bundle.storageUri;
|
|
19
|
+
if (!storageUri) {
|
|
20
|
+
return new Response("Bundle has no storage URI", { status: 404 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const protocol = new URL(storageUri).protocol.replace(":", "");
|
|
24
|
+
if (storagePlugin?.protocol === protocol) {
|
|
25
|
+
const { response } = await storagePlugin.get({ storageUri });
|
|
26
|
+
if (!response)
|
|
27
|
+
return new Response("Storage object not found", { status: 404 });
|
|
28
|
+
|
|
29
|
+
const headers = new Headers(response.headers);
|
|
30
|
+
headers.set("cache-control", "private, no-store");
|
|
31
|
+
headers.set("content-disposition", "attachment");
|
|
32
|
+
return new Response(response.body, {
|
|
33
|
+
headers,
|
|
34
|
+
status: response.status,
|
|
35
|
+
statusText: response.statusText,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (protocol !== "http" && protocol !== "https") {
|
|
40
|
+
return new Response(`No storage plugin for protocol: ${protocol}`, {
|
|
41
|
+
status: 503,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Response.redirect(storageUri, 302);
|
|
46
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getBundlePatches } from "@hot-updater/core";
|
|
2
|
+
import type { Bundle, DatabaseClient } from "@hot-updater/plugin-core";
|
|
3
|
+
|
|
4
|
+
const CHILDREN_QUERY_LIMIT = 100;
|
|
5
|
+
|
|
6
|
+
interface GetBundleChildrenInput {
|
|
7
|
+
baseBundleId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface GetBundleChildrenDeps {
|
|
11
|
+
databaseClient: DatabaseClient;
|
|
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
|
+
|
|
103
|
+
export async function getBundleChildren(
|
|
104
|
+
{ baseBundleId }: GetBundleChildrenInput,
|
|
105
|
+
deps: GetBundleChildrenDeps,
|
|
106
|
+
): Promise<Bundle[]> {
|
|
107
|
+
const childrenByBaseId = await collectBundleChildrenByBaseIds(
|
|
108
|
+
[baseBundleId],
|
|
109
|
+
deps,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return childrenByBaseId[baseBundleId] ?? [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function getBundleChildCounts(
|
|
116
|
+
baseBundleIds: string[],
|
|
117
|
+
deps: GetBundleChildrenDeps,
|
|
118
|
+
): Promise<Record<string, number>> {
|
|
119
|
+
const childrenByBaseId = await collectBundleChildrenByBaseIds(
|
|
120
|
+
baseBundleIds,
|
|
121
|
+
deps,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return Object.fromEntries(
|
|
125
|
+
Object.entries(childrenByBaseId).map(([bundleId, bundles]) => [
|
|
126
|
+
bundleId,
|
|
127
|
+
bundles.length,
|
|
128
|
+
]),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReleaseModel, ReleaseRow } from "@hot-updater/plugin-core";
|
|
2
|
+
|
|
3
|
+
export interface ReleaseListInput {
|
|
4
|
+
afterReleaseId?: string;
|
|
5
|
+
beforeReleaseId?: string;
|
|
6
|
+
bundleId?: string;
|
|
7
|
+
channelId?: string;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
limit: number;
|
|
10
|
+
page?: number;
|
|
11
|
+
platform?: "ios" | "android";
|
|
12
|
+
targetAppVersion?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReleaseListResult {
|
|
16
|
+
data: readonly ReleaseRow[];
|
|
17
|
+
pagination: {
|
|
18
|
+
currentPage: number;
|
|
19
|
+
hasNextPage: boolean;
|
|
20
|
+
hasPreviousPage: boolean;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function listReleases(
|
|
25
|
+
releases: ReleaseModel,
|
|
26
|
+
input: ReleaseListInput,
|
|
27
|
+
): Promise<ReleaseListResult> {
|
|
28
|
+
const page = input.page ?? 1;
|
|
29
|
+
const rows = await releases.findMany({
|
|
30
|
+
...(input.afterReleaseId === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { afterReleaseId: input.afterReleaseId }),
|
|
33
|
+
...(input.beforeReleaseId === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { beforeReleaseId: input.beforeReleaseId }),
|
|
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,
|
|
44
|
+
});
|
|
45
|
+
const isPreviousRequest = input.afterReleaseId !== undefined;
|
|
46
|
+
const hasExtraRow = rows.length > input.limit;
|
|
47
|
+
const data = isPreviousRequest
|
|
48
|
+
? hasExtraRow
|
|
49
|
+
? rows.slice(1)
|
|
50
|
+
: rows
|
|
51
|
+
: rows.slice(0, input.limit);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
data,
|
|
55
|
+
pagination: {
|
|
56
|
+
currentPage: page,
|
|
57
|
+
hasNextPage: isPreviousRequest || hasExtraRow,
|
|
58
|
+
hasPreviousPage: page > 1,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReleaseRow } from "@hot-updater/plugin-core";
|
|
2
|
+
import type {
|
|
3
|
+
InsightsProvider,
|
|
4
|
+
BundleEventSummaryByBundle,
|
|
5
|
+
} from "@hot-updater/server";
|
|
6
|
+
|
|
7
|
+
export async function getReleaseActivity30d(
|
|
8
|
+
insights: Pick<InsightsProvider, "getBundleEventSummaries"> | null,
|
|
9
|
+
releases: readonly ReleaseRow[],
|
|
10
|
+
): Promise<ReadonlyMap<string, BundleEventSummaryByBundle>> {
|
|
11
|
+
const bundleIds = [
|
|
12
|
+
...new Set(
|
|
13
|
+
releases.flatMap(({ bundle_id }) =>
|
|
14
|
+
bundle_id === null ? [] : [bundle_id],
|
|
15
|
+
),
|
|
16
|
+
),
|
|
17
|
+
];
|
|
18
|
+
if (insights === null || bundleIds.length === 0) return new Map();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const summaries = await insights.getBundleEventSummaries(bundleIds, "30d");
|
|
22
|
+
return new Map(summaries.map((summary) => [summary.bundleId, summary]));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error("Error during Release list activity retrieval:", error);
|
|
25
|
+
return new Map();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isReleaseEligibleForCohort,
|
|
3
|
+
NUMERIC_COHORT_SIZE,
|
|
4
|
+
} from "@hot-updater/core";
|
|
5
|
+
import type {
|
|
6
|
+
CompiledReleaseCatalog,
|
|
7
|
+
ReleaseCatalogModel,
|
|
8
|
+
ReleaseRow,
|
|
9
|
+
} from "@hot-updater/plugin-core";
|
|
10
|
+
import type { BundleEventSummary } from "@hot-updater/server";
|
|
11
|
+
|
|
12
|
+
export type ReleaseReachabilityRow = ReleaseRow & {
|
|
13
|
+
readonly currentlyUnreachable: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ReleaseListRow = ReleaseReachabilityRow & {
|
|
17
|
+
readonly activity30d: BundleEventSummary | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const NON_TARGETED_COHORT = "release-catalog-non-targeted-cohort";
|
|
21
|
+
|
|
22
|
+
function collectReachableFromIndexes(
|
|
23
|
+
catalog: CompiledReleaseCatalog,
|
|
24
|
+
indexes: readonly number[],
|
|
25
|
+
reachableReleaseIds: Set<string>,
|
|
26
|
+
) {
|
|
27
|
+
const descriptors = indexes.flatMap((index) => {
|
|
28
|
+
const descriptor = catalog.releaseDescriptors[index];
|
|
29
|
+
return descriptor === undefined ? [] : [descriptor];
|
|
30
|
+
});
|
|
31
|
+
const cohorts = new Set<string | undefined>([
|
|
32
|
+
undefined,
|
|
33
|
+
...Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) =>
|
|
34
|
+
String(index + 1),
|
|
35
|
+
),
|
|
36
|
+
...descriptors.flatMap(({ targetCohorts }) => targetCohorts),
|
|
37
|
+
NON_TARGETED_COHORT,
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
for (const cohort of cohorts) {
|
|
41
|
+
const selected = descriptors.find(
|
|
42
|
+
(descriptor) =>
|
|
43
|
+
descriptor.kind === "BUNDLE" &&
|
|
44
|
+
descriptor.bundleId !== null &&
|
|
45
|
+
isReleaseEligibleForCohort(descriptor, cohort),
|
|
46
|
+
);
|
|
47
|
+
if (selected !== undefined) {
|
|
48
|
+
reachableReleaseIds.add(selected.releaseId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function collectCurrentlyReachableReleaseIds(
|
|
54
|
+
catalog: CompiledReleaseCatalog,
|
|
55
|
+
): ReadonlySet<string> {
|
|
56
|
+
const reachableReleaseIds = new Set<string>();
|
|
57
|
+
const indexGroups =
|
|
58
|
+
catalog.strategy === "APP_VERSION"
|
|
59
|
+
? catalog.segments.map(({ releaseIndexes }) => releaseIndexes)
|
|
60
|
+
: [catalog.releaseIndexes];
|
|
61
|
+
|
|
62
|
+
for (const indexes of indexGroups) {
|
|
63
|
+
collectReachableFromIndexes(catalog, indexes, reachableReleaseIds);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return reachableReleaseIds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function addReleaseReachability(
|
|
70
|
+
releaseCatalogs: Pick<ReleaseCatalogModel, "findByScopeKey">,
|
|
71
|
+
releases: readonly ReleaseRow[],
|
|
72
|
+
): Promise<readonly ReleaseReachabilityRow[]> {
|
|
73
|
+
const scopeKeys = [...new Set(releases.map(({ scope_key }) => scope_key))];
|
|
74
|
+
const reachableByScope = new Map(
|
|
75
|
+
await Promise.all(
|
|
76
|
+
scopeKeys.map(async (scopeKey) => {
|
|
77
|
+
const row = await releaseCatalogs.findByScopeKey(scopeKey);
|
|
78
|
+
const reachableReleaseIds =
|
|
79
|
+
row === null
|
|
80
|
+
? new Set<string>()
|
|
81
|
+
: collectCurrentlyReachableReleaseIds(
|
|
82
|
+
JSON.parse(row.payload) as CompiledReleaseCatalog,
|
|
83
|
+
);
|
|
84
|
+
return [scopeKey, reachableReleaseIds] as const;
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return releases.map((release) => ({
|
|
90
|
+
...release,
|
|
91
|
+
currentlyUnreachable: !reachableByScope
|
|
92
|
+
.get(release.scope_key)
|
|
93
|
+
?.has(release.id),
|
|
94
|
+
}));
|
|
95
|
+
}
|