hot-updater 0.35.12 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +374 -33
- package/package.json +14 -14
package/dist/index.mjs
CHANGED
|
@@ -29,11 +29,12 @@ import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWater
|
|
|
29
29
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
30
30
|
import { pipeline } from "stream/promises";
|
|
31
31
|
import { constants as constants$2, createBrotliCompress } from "zlib";
|
|
32
|
-
import { assertNodeStoragePlugin, getContentAddressedAssetStoragePath } from "@hot-updater/plugin-core";
|
|
32
|
+
import { BUNDLE_STORAGE_PREFIX, assertNodeStoragePlugin, createBundleStorageKey, createStorageRootUriWithPath, getContentAddressedAssetStoragePath, getManifestAssetDownloadPath, isContentAddressedAssetBaseStorageUri, resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
|
|
33
33
|
import { createBundleDiff, createMigrator, generateSchema } from "@hot-updater/server/db";
|
|
34
34
|
import net from "node:net";
|
|
35
35
|
import { setTimeout as setTimeout$1 } from "timers/promises";
|
|
36
36
|
import { createJiti } from "jiti";
|
|
37
|
+
import { getAssetBaseStorageUri, getBundlePatches, getManifestStorageUri, getPatchStorageUri } from "@hot-updater/core";
|
|
37
38
|
//#region ../../node_modules/.pnpm/commander@14.0.3/node_modules/commander/lib/error.js
|
|
38
39
|
var require_error = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
39
40
|
/**
|
|
@@ -26302,22 +26303,13 @@ const getRelativeStorageDir = (relativePath) => {
|
|
|
26302
26303
|
const dirname = path.posix.dirname(normalized);
|
|
26303
26304
|
return dirname === "." ? "" : dirname;
|
|
26304
26305
|
};
|
|
26305
|
-
const isBrotliManifestBundleAsset = (relativePath) => /(^|\/)index\.[^/]+\.bundle$/.test(relativePath.replace(/\\/g, "/"));
|
|
26306
|
-
const getManifestAssetUploadName = (relativePath) => isBrotliManifestBundleAsset(relativePath) ? `${relativePath}.br` : relativePath;
|
|
26307
|
-
const replaceBundleStorageUriPath = (storageUri, bundleId, nextPath) => {
|
|
26308
|
-
const storageUrl = new URL(storageUri);
|
|
26309
|
-
const segments = storageUrl.pathname.split("/").filter(Boolean);
|
|
26310
|
-
const bundleIndex = segments.lastIndexOf(bundleId);
|
|
26311
|
-
storageUrl.pathname = `/${[...bundleIndex >= 0 ? segments.slice(0, bundleIndex) : segments.slice(0, -2), nextPath].filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
26312
|
-
return storageUrl.toString();
|
|
26313
|
-
};
|
|
26314
26306
|
const createStorageUriWithRelativePath = (baseStorageUri, relativePath) => {
|
|
26315
26307
|
const storageUrl = new URL(baseStorageUri);
|
|
26316
26308
|
storageUrl.pathname = `${storageUrl.pathname.replace(/\/+$/, "")}/${relativePath.replace(/\\/g, "/").split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
26317
26309
|
return storageUrl.toString();
|
|
26318
26310
|
};
|
|
26319
26311
|
const ensureUploadSourcePath = async ({ outputPath, targetFile, uploadFilename }) => {
|
|
26320
|
-
const uploadName =
|
|
26312
|
+
const uploadName = getManifestAssetDownloadPath(targetFile.name);
|
|
26321
26313
|
const expectedFilename = uploadFilename ?? path.posix.basename(uploadName);
|
|
26322
26314
|
const actualFilename = path.basename(targetFile.path);
|
|
26323
26315
|
if (uploadName === targetFile.name && expectedFilename === actualFilename) return targetFile.path;
|
|
@@ -26334,7 +26326,7 @@ const getUniqueContentAddressedAssetUploadTargets = ({ manifest, targetFiles })
|
|
|
26334
26326
|
const manifestAsset = manifest.assets[targetFile.name];
|
|
26335
26327
|
if (!manifestAsset?.fileHash) throw new Error(`Manifest file hash not found for ${targetFile.name}`);
|
|
26336
26328
|
const storagePath = getContentAddressedAssetStoragePath({
|
|
26337
|
-
assetPath:
|
|
26329
|
+
assetPath: getManifestAssetDownloadPath(targetFile.name),
|
|
26338
26330
|
fileHash: manifestAsset.fileHash
|
|
26339
26331
|
});
|
|
26340
26332
|
if (!targets.has(storagePath)) targets.set(storagePath, {
|
|
@@ -26592,11 +26584,11 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
26592
26584
|
message(formatUploadProgress(uploadedStepCount, uploadStepCount, skippedUploadCount));
|
|
26593
26585
|
};
|
|
26594
26586
|
updateUploadProgress();
|
|
26595
|
-
const { storageUri } = await storagePlugin.profiles.node.upload(bundleId, bundlePath);
|
|
26587
|
+
const { storageUri } = await storagePlugin.profiles.node.upload(createBundleStorageKey(bundleId), bundlePath);
|
|
26596
26588
|
taskRef.storageUri = storageUri;
|
|
26597
26589
|
uploadedStepCount += 1;
|
|
26598
26590
|
updateUploadProgress();
|
|
26599
|
-
taskRef.assetBaseStorageUri =
|
|
26591
|
+
taskRef.assetBaseStorageUri = createStorageRootUriWithPath(storageUri, bundleId, "assets");
|
|
26600
26592
|
await runWithConcurrency(assetUploadTargets, MANIFEST_ASSET_UPLOAD_CONCURRENCY, async ({ storagePath, targetFile }) => {
|
|
26601
26593
|
const storageUri = createStorageUriWithRelativePath(taskRef.assetBaseStorageUri, storagePath);
|
|
26602
26594
|
const uploadKey = ["assets", getRelativeStorageDir(storagePath)].filter(Boolean).join("/");
|
|
@@ -26610,7 +26602,7 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
|
|
|
26610
26602
|
uploadedStepCount += 1;
|
|
26611
26603
|
updateUploadProgress();
|
|
26612
26604
|
});
|
|
26613
|
-
taskRef.manifestStorageUri = (await storagePlugin.profiles.node.upload(bundleId, taskRef.manifestPath)).storageUri;
|
|
26605
|
+
taskRef.manifestStorageUri = (await storagePlugin.profiles.node.upload(createBundleStorageKey(bundleId), taskRef.manifestPath)).storageUri;
|
|
26614
26606
|
uploadedStepCount += 1;
|
|
26615
26607
|
updateUploadProgress();
|
|
26616
26608
|
} catch (e) {
|
|
@@ -27522,6 +27514,8 @@ const LIST_COLUMNS = [
|
|
|
27522
27514
|
const DEFAULT_LIMIT = 20;
|
|
27523
27515
|
const DELETE_VERIFY_ATTEMPTS = 12;
|
|
27524
27516
|
const DELETE_VERIFY_DELAY_MS = 1e3;
|
|
27517
|
+
const STANDALONE_DATABASE_NAME$1 = "standalone-repository";
|
|
27518
|
+
const STANDALONE_DELETE_LOOKUP_LIMIT = 100;
|
|
27525
27519
|
const formatRow = (bundle) => {
|
|
27526
27520
|
const out = {};
|
|
27527
27521
|
for (const field of LIST_FIELDS) {
|
|
@@ -27557,7 +27551,7 @@ const refuseNonInteractiveMutation = (action) => {
|
|
|
27557
27551
|
p.log.error(`Cannot ${action} a bundle without confirmation in a non-interactive shell. Re-run with -y, or use a TTY.`);
|
|
27558
27552
|
process.exit(1);
|
|
27559
27553
|
};
|
|
27560
|
-
const safeOnUnmount$
|
|
27554
|
+
const safeOnUnmount$3 = async (databasePlugin) => {
|
|
27561
27555
|
try {
|
|
27562
27556
|
await databasePlugin.onUnmount?.();
|
|
27563
27557
|
} catch (err) {
|
|
@@ -27572,13 +27566,14 @@ const handleBundleList = async (options = {}) => {
|
|
|
27572
27566
|
const result = await databasePlugin.getBundles({
|
|
27573
27567
|
where: {
|
|
27574
27568
|
channel: options.channel,
|
|
27575
|
-
platform: options.platform
|
|
27569
|
+
platform: options.platform,
|
|
27570
|
+
targetAppVersion: options.targetAppVersion
|
|
27576
27571
|
},
|
|
27577
27572
|
limit
|
|
27578
27573
|
});
|
|
27579
27574
|
console.log(options.json ? JSON.stringify(result, null, 2) : tabulate(result.data));
|
|
27580
27575
|
} finally {
|
|
27581
|
-
await safeOnUnmount$
|
|
27576
|
+
await safeOnUnmount$3(databasePlugin);
|
|
27582
27577
|
}
|
|
27583
27578
|
};
|
|
27584
27579
|
const handleBundleShow = async (bundleId, options = {}) => {
|
|
@@ -27596,7 +27591,7 @@ const handleBundleShow = async (bundleId, options = {}) => {
|
|
|
27596
27591
|
}
|
|
27597
27592
|
p.log.message(formatBundleSummary(bundle));
|
|
27598
27593
|
} finally {
|
|
27599
|
-
await safeOnUnmount$
|
|
27594
|
+
await safeOnUnmount$3(databasePlugin);
|
|
27600
27595
|
}
|
|
27601
27596
|
};
|
|
27602
27597
|
const handleBundleSetEnabled = async (bundleId, nextEnabled, options = {}) => {
|
|
@@ -27637,7 +27632,7 @@ const handleBundleSetEnabled = async (bundleId, nextEnabled, options = {}) => {
|
|
|
27637
27632
|
p.log.info(` ${ui.id(bundleId)}`);
|
|
27638
27633
|
}
|
|
27639
27634
|
} finally {
|
|
27640
|
-
await safeOnUnmount$
|
|
27635
|
+
await safeOnUnmount$3(databasePlugin);
|
|
27641
27636
|
}
|
|
27642
27637
|
};
|
|
27643
27638
|
const handleBundleUpdate = async (bundleId, options = {}) => {
|
|
@@ -27685,23 +27680,35 @@ const handleBundleUpdate = async (bundleId, options = {}) => {
|
|
|
27685
27680
|
p.log.success("Updated bundle.");
|
|
27686
27681
|
p.log.info(` ${ui.id(bundleId)}`);
|
|
27687
27682
|
} finally {
|
|
27688
|
-
await safeOnUnmount$
|
|
27683
|
+
await safeOnUnmount$3(databasePlugin);
|
|
27689
27684
|
}
|
|
27690
27685
|
};
|
|
27691
27686
|
const handleBundleDelete = async (bundleIds, options = {}) => {
|
|
27692
27687
|
printBanner$1();
|
|
27693
|
-
const ids = bundleIds ?? [];
|
|
27688
|
+
const ids = [...new Set(bundleIds ?? [])];
|
|
27694
27689
|
if (ids.length === 0) {
|
|
27695
27690
|
p.log.error("Provide at least one bundle id.");
|
|
27696
27691
|
process.exit(1);
|
|
27697
27692
|
}
|
|
27698
27693
|
const databasePlugin = await (await loadConfig(null)).database();
|
|
27699
27694
|
try {
|
|
27700
|
-
const
|
|
27701
|
-
const
|
|
27702
|
-
|
|
27703
|
-
|
|
27704
|
-
|
|
27695
|
+
const lookupBatches = databasePlugin.name === STANDALONE_DATABASE_NAME$1 ? Array.from({ length: Math.ceil(ids.length / STANDALONE_DELETE_LOOKUP_LIMIT) }, (_, index) => ids.slice(index * STANDALONE_DELETE_LOOKUP_LIMIT, (index + 1) * STANDALONE_DELETE_LOOKUP_LIMIT)) : [ids];
|
|
27696
|
+
const matchedBundles = [];
|
|
27697
|
+
for (const batch of lookupBatches) {
|
|
27698
|
+
const { data } = await databasePlugin.getBundles({
|
|
27699
|
+
where: { id: { in: batch } },
|
|
27700
|
+
limit: batch.length
|
|
27701
|
+
});
|
|
27702
|
+
matchedBundles.push(...data);
|
|
27703
|
+
}
|
|
27704
|
+
const matchedById = new Map(matchedBundles.map((bundle) => [bundle.id, bundle]));
|
|
27705
|
+
const targets = ids.flatMap((id) => {
|
|
27706
|
+
const bundle = matchedById.get(id);
|
|
27707
|
+
if (!bundle) {
|
|
27708
|
+
p.log.info(`No bundle with id ${id}. Skipping.`);
|
|
27709
|
+
return [];
|
|
27710
|
+
}
|
|
27711
|
+
return [bundle];
|
|
27705
27712
|
});
|
|
27706
27713
|
if (targets.length === 0) {
|
|
27707
27714
|
p.log.info("No matching bundle records. No changes.");
|
|
@@ -27734,7 +27741,7 @@ const handleBundleDelete = async (bundleIds, options = {}) => {
|
|
|
27734
27741
|
p.log.info(` ${ui.id(firstTarget.id)}`);
|
|
27735
27742
|
} else p.log.success(`Deleted ${targets.length} bundle records.`);
|
|
27736
27743
|
} finally {
|
|
27737
|
-
await safeOnUnmount$
|
|
27744
|
+
await safeOnUnmount$3(databasePlugin);
|
|
27738
27745
|
}
|
|
27739
27746
|
};
|
|
27740
27747
|
async function waitForDeletedBundle(databasePlugin, bundleId) {
|
|
@@ -50298,7 +50305,7 @@ async function migrateWithMigrator(hotUpdater, skipConfirm, s) {
|
|
|
50298
50305
|
}
|
|
50299
50306
|
//#endregion
|
|
50300
50307
|
//#region src/commands/promote.ts
|
|
50301
|
-
const safeOnUnmount$
|
|
50308
|
+
const safeOnUnmount$2 = async (databasePlugin) => {
|
|
50302
50309
|
try {
|
|
50303
50310
|
await databasePlugin.onUnmount?.();
|
|
50304
50311
|
} catch (err) {
|
|
@@ -50376,14 +50383,14 @@ const handlePromote = async (bundleId, options) => {
|
|
|
50376
50383
|
p.log.info(` ${ui.id(promoted.id)}`);
|
|
50377
50384
|
}
|
|
50378
50385
|
} finally {
|
|
50379
|
-
await safeOnUnmount$
|
|
50386
|
+
await safeOnUnmount$2(databasePlugin);
|
|
50380
50387
|
}
|
|
50381
50388
|
};
|
|
50382
50389
|
//#endregion
|
|
50383
50390
|
//#region src/commands/rollback.ts
|
|
50384
50391
|
const summarizeTarget = (target) => ui.block(`${target.platform}`, [ui.kv("Disable", ui.id(target.bundle.id)), target.fallbackId ? ui.kv("Fallback", ui.id(target.fallbackId)) : ui.kv("Fallback", ui.warning("binary-shipped JS"))]);
|
|
50385
50392
|
const formatRetryHint = (channel, target) => `Re-run with: hot-updater rollback ${channel} -p ${target.platform} --target ${target.bundle.id}`;
|
|
50386
|
-
const safeOnUnmount = async (databasePlugin) => {
|
|
50393
|
+
const safeOnUnmount$1 = async (databasePlugin) => {
|
|
50387
50394
|
try {
|
|
50388
50395
|
await databasePlugin.onUnmount?.();
|
|
50389
50396
|
} catch (err) {
|
|
@@ -50497,10 +50504,329 @@ const handleRollback = async (channel, options = {}) => {
|
|
|
50497
50504
|
process.exit(1);
|
|
50498
50505
|
}
|
|
50499
50506
|
} finally {
|
|
50500
|
-
await safeOnUnmount(databasePlugin);
|
|
50507
|
+
await safeOnUnmount$1(databasePlugin);
|
|
50501
50508
|
}
|
|
50502
50509
|
};
|
|
50503
50510
|
//#endregion
|
|
50511
|
+
//#region src/commands/storage.ts
|
|
50512
|
+
const BUNDLE_PAGE_SIZE = 1e4;
|
|
50513
|
+
const STANDALONE_BUNDLE_PAGE_SIZE = 100;
|
|
50514
|
+
const STANDALONE_DATABASE_NAME = "standalone-repository";
|
|
50515
|
+
const MANIFEST_READ_CONCURRENCY = 4;
|
|
50516
|
+
const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
50517
|
+
const CONTENT_ADDRESSED_ASSET_KEY_RE = /^assets\/sha256\/[0-9a-f]{2}\/[0-9a-f]{64}(?:\.[^/]+)?$/i;
|
|
50518
|
+
const DEFAULT_STORAGE_PRUNE_PROTECTION_MS = 1440 * 60 * 1e3;
|
|
50519
|
+
function parseStoragePruneProtection(value) {
|
|
50520
|
+
const match = value.trim().match(/^(\d+)(m|h|d|w)$/i);
|
|
50521
|
+
if (!match) throw new Error("must use a duration such as 30m, 24h, or 7d");
|
|
50522
|
+
const amount = Number.parseInt(match[1], 10);
|
|
50523
|
+
const unit = match[2].toLowerCase();
|
|
50524
|
+
return amount * (unit === "m" ? 60 * 1e3 : unit === "h" ? 3600 * 1e3 : unit === "d" ? 1440 * 60 * 1e3 : 10080 * 60 * 1e3);
|
|
50525
|
+
}
|
|
50526
|
+
function formatBytes(value) {
|
|
50527
|
+
if (value < 1024) return `${value} B`;
|
|
50528
|
+
const units = [
|
|
50529
|
+
"KB",
|
|
50530
|
+
"MB",
|
|
50531
|
+
"GB",
|
|
50532
|
+
"TB"
|
|
50533
|
+
];
|
|
50534
|
+
let size = value / 1024;
|
|
50535
|
+
let unitIndex = 0;
|
|
50536
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
50537
|
+
size /= 1024;
|
|
50538
|
+
unitIndex += 1;
|
|
50539
|
+
}
|
|
50540
|
+
return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[unitIndex]}`;
|
|
50541
|
+
}
|
|
50542
|
+
function formatDuration(value) {
|
|
50543
|
+
const hour = 3600 * 1e3;
|
|
50544
|
+
const day = 24 * hour;
|
|
50545
|
+
if (value % day === 0) return `${value / day}d`;
|
|
50546
|
+
if (value % hour === 0) return `${value / hour}h`;
|
|
50547
|
+
return `${value / (60 * 1e3)}m`;
|
|
50548
|
+
}
|
|
50549
|
+
const PRUNE_CANDIDATE_COLUMNS = [
|
|
50550
|
+
{
|
|
50551
|
+
key: "type",
|
|
50552
|
+
label: "Type"
|
|
50553
|
+
},
|
|
50554
|
+
{
|
|
50555
|
+
key: "size",
|
|
50556
|
+
label: "Size"
|
|
50557
|
+
},
|
|
50558
|
+
{
|
|
50559
|
+
key: "modified",
|
|
50560
|
+
label: "Modified",
|
|
50561
|
+
format: ui.muted
|
|
50562
|
+
},
|
|
50563
|
+
{
|
|
50564
|
+
key: "key",
|
|
50565
|
+
label: "Key",
|
|
50566
|
+
format: ui.path
|
|
50567
|
+
}
|
|
50568
|
+
];
|
|
50569
|
+
function formatPruneCandidateTable(candidates) {
|
|
50570
|
+
const rows = candidates.map((candidate) => ({
|
|
50571
|
+
key: candidate.key,
|
|
50572
|
+
modified: candidate.lastModifiedAt?.toISOString() ?? "-",
|
|
50573
|
+
size: formatBytes(candidate.size),
|
|
50574
|
+
type: candidate.reason === "asset" ? "shared asset" : "bundle data"
|
|
50575
|
+
}));
|
|
50576
|
+
return ui.table(PRUNE_CANDIDATE_COLUMNS, rows);
|
|
50577
|
+
}
|
|
50578
|
+
function normalizeStorageUri(storageUri) {
|
|
50579
|
+
return new URL(storageUri).toString();
|
|
50580
|
+
}
|
|
50581
|
+
function isLegacyBundleArtifactPath(segments) {
|
|
50582
|
+
const [firstSegment] = segments;
|
|
50583
|
+
return firstSegment === "manifest.json" || firstSegment === "files" || firstSegment === "patches" || firstSegment !== void 0 && /^bundle(?:\..+)?$/.test(firstSegment);
|
|
50584
|
+
}
|
|
50585
|
+
function getBundleIdFromStorageKey(key) {
|
|
50586
|
+
const segments = key.split("/").filter(Boolean);
|
|
50587
|
+
if (segments[0] === BUNDLE_STORAGE_PREFIX) {
|
|
50588
|
+
const bundleId = segments[1];
|
|
50589
|
+
return bundleId && UUID_V7_RE.test(bundleId) ? bundleId.toLowerCase() : null;
|
|
50590
|
+
}
|
|
50591
|
+
const bundleId = segments[0];
|
|
50592
|
+
return bundleId && UUID_V7_RE.test(bundleId) && isLegacyBundleArtifactPath(segments.slice(1)) ? bundleId.toLowerCase() : null;
|
|
50593
|
+
}
|
|
50594
|
+
function isBundleManifest(value, bundleId) {
|
|
50595
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
50596
|
+
const candidate = value;
|
|
50597
|
+
if (candidate.bundleId !== bundleId) return false;
|
|
50598
|
+
const assets = candidate.assets;
|
|
50599
|
+
if (!assets || typeof assets !== "object" || Array.isArray(assets)) return false;
|
|
50600
|
+
return Object.values(assets).every((asset) => {
|
|
50601
|
+
return asset !== null && typeof asset === "object" && !Array.isArray(asset) && typeof asset.fileHash === "string" && /^[0-9a-f]{64}$/i.test(asset.fileHash);
|
|
50602
|
+
});
|
|
50603
|
+
}
|
|
50604
|
+
async function forEachWithConcurrency(values, concurrency, callback) {
|
|
50605
|
+
let nextIndex = 0;
|
|
50606
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
50607
|
+
while (nextIndex < values.length) {
|
|
50608
|
+
const index = nextIndex;
|
|
50609
|
+
nextIndex += 1;
|
|
50610
|
+
const value = values[index];
|
|
50611
|
+
if (value === void 0) return;
|
|
50612
|
+
await callback(value, index);
|
|
50613
|
+
}
|
|
50614
|
+
});
|
|
50615
|
+
const failure = (await Promise.allSettled(workers)).find((result) => result.status === "rejected");
|
|
50616
|
+
if (failure) throw failure.reason;
|
|
50617
|
+
}
|
|
50618
|
+
async function loadAllBundles(databasePlugin) {
|
|
50619
|
+
const bundles = [];
|
|
50620
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
50621
|
+
const pageSize = databasePlugin.name === STANDALONE_DATABASE_NAME ? STANDALONE_BUNDLE_PAGE_SIZE : BUNDLE_PAGE_SIZE;
|
|
50622
|
+
let after;
|
|
50623
|
+
while (true) {
|
|
50624
|
+
const { data, pagination } = await databasePlugin.getBundles({
|
|
50625
|
+
cursor: after ? { after } : void 0,
|
|
50626
|
+
limit: pageSize,
|
|
50627
|
+
orderBy: {
|
|
50628
|
+
direction: "desc",
|
|
50629
|
+
field: "id"
|
|
50630
|
+
}
|
|
50631
|
+
});
|
|
50632
|
+
bundles.push(...data);
|
|
50633
|
+
const nextCursor = pagination.nextCursor ?? void 0;
|
|
50634
|
+
if (pagination.hasNextPage && !nextCursor) throw new Error("Database cannot provide safe cursor pagination for storage prune.");
|
|
50635
|
+
if (!nextCursor) return bundles;
|
|
50636
|
+
if (seenCursors.has(nextCursor)) throw new Error(`Database returned a repeated cursor: ${nextCursor}`);
|
|
50637
|
+
seenCursors.add(nextCursor);
|
|
50638
|
+
after = nextCursor;
|
|
50639
|
+
}
|
|
50640
|
+
}
|
|
50641
|
+
async function readManifest(bundle, storagePlugin, workDir, index) {
|
|
50642
|
+
const manifestStorageUri = getManifestStorageUri(bundle);
|
|
50643
|
+
if (!manifestStorageUri) throw new Error(`Cannot prune shared assets: bundle ${bundle.id} has no manifest URI.`);
|
|
50644
|
+
const protocol = new URL(manifestStorageUri).protocol.replace(":", "");
|
|
50645
|
+
let manifestText;
|
|
50646
|
+
if (protocol === "http" || protocol === "https") {
|
|
50647
|
+
const response = await fetch(manifestStorageUri);
|
|
50648
|
+
if (!response.ok) throw new Error(`Cannot prune shared assets: failed to read manifest for bundle ${bundle.id}.`);
|
|
50649
|
+
manifestText = await response.text();
|
|
50650
|
+
} else {
|
|
50651
|
+
if (protocol !== storagePlugin.supportedProtocol) throw new Error(`No storage plugin for protocol: ${protocol}`);
|
|
50652
|
+
const manifestPath = path$1.join(workDir, `${index}.json`);
|
|
50653
|
+
try {
|
|
50654
|
+
await storagePlugin.profiles.node.downloadFile(manifestStorageUri, manifestPath);
|
|
50655
|
+
manifestText = await fs$1.readFile(manifestPath, "utf8");
|
|
50656
|
+
} catch (error) {
|
|
50657
|
+
throw new Error(`Cannot prune shared assets: failed to read manifest for bundle ${bundle.id}.`, { cause: error });
|
|
50658
|
+
}
|
|
50659
|
+
}
|
|
50660
|
+
let manifest;
|
|
50661
|
+
try {
|
|
50662
|
+
manifest = JSON.parse(manifestText);
|
|
50663
|
+
} catch (error) {
|
|
50664
|
+
throw new Error(`Cannot prune shared assets: invalid manifest for bundle ${bundle.id}.`, { cause: error });
|
|
50665
|
+
}
|
|
50666
|
+
if (!isBundleManifest(manifest, bundle.id)) throw new Error(`Cannot prune shared assets: invalid manifest for bundle ${bundle.id}.`);
|
|
50667
|
+
return manifest;
|
|
50668
|
+
}
|
|
50669
|
+
async function collectReferencedAssetUris(bundles, storagePlugin) {
|
|
50670
|
+
const bundlesWithSharedAssets = bundles.filter((bundle) => {
|
|
50671
|
+
const assetBaseStorageUri = getAssetBaseStorageUri(bundle);
|
|
50672
|
+
if (assetBaseStorageUri === null || !isContentAddressedAssetBaseStorageUri(assetBaseStorageUri)) return false;
|
|
50673
|
+
const protocol = new URL(assetBaseStorageUri).protocol.replace(":", "");
|
|
50674
|
+
if (protocol !== storagePlugin.supportedProtocol) throw new Error(`Cannot prune shared assets: bundle ${bundle.id} uses ${protocol} asset storage, but the configured storage plugin uses ${storagePlugin.supportedProtocol}.`);
|
|
50675
|
+
return true;
|
|
50676
|
+
});
|
|
50677
|
+
const referencedUris = /* @__PURE__ */ new Set();
|
|
50678
|
+
if (bundlesWithSharedAssets.length === 0) return {
|
|
50679
|
+
manifestCount: 0,
|
|
50680
|
+
referencedUris
|
|
50681
|
+
};
|
|
50682
|
+
const workDir = await fs$1.mkdtemp(path$1.join(os.tmpdir(), "hot-updater-storage-prune-"));
|
|
50683
|
+
try {
|
|
50684
|
+
await forEachWithConcurrency(bundlesWithSharedAssets, MANIFEST_READ_CONCURRENCY, async (bundle, index) => {
|
|
50685
|
+
const assetBaseStorageUri = getAssetBaseStorageUri(bundle);
|
|
50686
|
+
const manifest = await readManifest(bundle, storagePlugin, workDir, index);
|
|
50687
|
+
for (const [assetPath, asset] of Object.entries(manifest.assets)) {
|
|
50688
|
+
const downloadPath = getManifestAssetDownloadPath(assetPath);
|
|
50689
|
+
referencedUris.add(normalizeStorageUri(resolveManifestAssetStorageUri({
|
|
50690
|
+
assetBaseStorageUri,
|
|
50691
|
+
assetPath: downloadPath,
|
|
50692
|
+
fileHash: asset.fileHash
|
|
50693
|
+
})));
|
|
50694
|
+
}
|
|
50695
|
+
});
|
|
50696
|
+
} finally {
|
|
50697
|
+
await fs$1.rm(workDir, {
|
|
50698
|
+
force: true,
|
|
50699
|
+
recursive: true
|
|
50700
|
+
});
|
|
50701
|
+
}
|
|
50702
|
+
return {
|
|
50703
|
+
manifestCount: bundlesWithSharedAssets.length,
|
|
50704
|
+
referencedUris
|
|
50705
|
+
};
|
|
50706
|
+
}
|
|
50707
|
+
function collectBundleStorageReferences(bundles) {
|
|
50708
|
+
const exactUris = /* @__PURE__ */ new Set();
|
|
50709
|
+
const prefixUris = /* @__PURE__ */ new Set();
|
|
50710
|
+
const addExactUri = (storageUri) => {
|
|
50711
|
+
if (storageUri) exactUris.add(normalizeStorageUri(storageUri));
|
|
50712
|
+
};
|
|
50713
|
+
for (const bundle of bundles) {
|
|
50714
|
+
addExactUri(bundle.storageUri);
|
|
50715
|
+
addExactUri(getManifestStorageUri(bundle));
|
|
50716
|
+
addExactUri(getPatchStorageUri(bundle));
|
|
50717
|
+
for (const patch of getBundlePatches(bundle)) addExactUri(patch.patchStorageUri);
|
|
50718
|
+
const assetBaseStorageUri = getAssetBaseStorageUri(bundle);
|
|
50719
|
+
if (assetBaseStorageUri && !isContentAddressedAssetBaseStorageUri(assetBaseStorageUri)) prefixUris.add(`${normalizeStorageUri(assetBaseStorageUri).replace(/\/+$/, "")}/`);
|
|
50720
|
+
}
|
|
50721
|
+
return {
|
|
50722
|
+
exactUris,
|
|
50723
|
+
prefixUris: [...prefixUris]
|
|
50724
|
+
};
|
|
50725
|
+
}
|
|
50726
|
+
function getPruneCandidates({ bundleStorageReferences, objects, liveBundleIds, referencedAssetUris }) {
|
|
50727
|
+
const candidates = [];
|
|
50728
|
+
for (const object of objects) {
|
|
50729
|
+
const normalizedStorageUri = normalizeStorageUri(object.storageUri);
|
|
50730
|
+
if (bundleStorageReferences.exactUris.has(normalizedStorageUri) || bundleStorageReferences.prefixUris.some((prefix) => normalizedStorageUri.startsWith(prefix))) continue;
|
|
50731
|
+
const bundleId = getBundleIdFromStorageKey(object.key);
|
|
50732
|
+
if (bundleId && !liveBundleIds.has(bundleId)) {
|
|
50733
|
+
candidates.push({
|
|
50734
|
+
...object,
|
|
50735
|
+
reason: "bundle"
|
|
50736
|
+
});
|
|
50737
|
+
continue;
|
|
50738
|
+
}
|
|
50739
|
+
if (CONTENT_ADDRESSED_ASSET_KEY_RE.test(object.key) && !referencedAssetUris.has(normalizedStorageUri)) candidates.push({
|
|
50740
|
+
...object,
|
|
50741
|
+
reason: "asset"
|
|
50742
|
+
});
|
|
50743
|
+
}
|
|
50744
|
+
return candidates;
|
|
50745
|
+
}
|
|
50746
|
+
async function safeOnUnmount(databasePlugin) {
|
|
50747
|
+
try {
|
|
50748
|
+
await databasePlugin.onUnmount?.();
|
|
50749
|
+
} catch (error) {
|
|
50750
|
+
p.log.warn(`Database plugin onUnmount failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
50751
|
+
}
|
|
50752
|
+
}
|
|
50753
|
+
async function handleStoragePrune(options = {}) {
|
|
50754
|
+
printBanner$1();
|
|
50755
|
+
if (options.dryRun && options.yes) throw new Error("Storage prune --dry-run cannot be used with --yes.");
|
|
50756
|
+
const protectNewerThan = options.protectNewerThan ?? 864e5;
|
|
50757
|
+
if (!Number.isFinite(protectNewerThan) || protectNewerThan < 0) throw new Error("Storage prune protection must be a non-negative duration.");
|
|
50758
|
+
const config = await loadConfig(null);
|
|
50759
|
+
const [databasePlugin, loadedStoragePlugin] = await Promise.all([config.database(), config.storage()]);
|
|
50760
|
+
assertNodeStoragePlugin(loadedStoragePlugin);
|
|
50761
|
+
const storagePlugin = loadedStoragePlugin;
|
|
50762
|
+
try {
|
|
50763
|
+
const listObjects = storagePlugin.profiles.node.listObjects;
|
|
50764
|
+
if (!listObjects) throw new Error(`Storage plugin "${storagePlugin.name}" does not support storage prune.`);
|
|
50765
|
+
const deleteObjects = storagePlugin.profiles.node.deleteObjects;
|
|
50766
|
+
if (options.yes && !deleteObjects) throw new Error(`Storage plugin "${storagePlugin.name}" does not support exact object deletion.`);
|
|
50767
|
+
if (options.yes) {
|
|
50768
|
+
p.log.warn("Storage prune requires exclusive access. Stop deploy and promote operations first.");
|
|
50769
|
+
p.log.warn("The current database must own every object under this storage prefix. Use a separate storage basePath for each database or environment.");
|
|
50770
|
+
}
|
|
50771
|
+
const bundles = await loadAllBundles(databasePlugin);
|
|
50772
|
+
const liveBundleIds = new Set(bundles.map((bundle) => bundle.id.toLowerCase()));
|
|
50773
|
+
const bundleStorageReferences = collectBundleStorageReferences(bundles);
|
|
50774
|
+
const { manifestCount, referencedUris } = await collectReferencedAssetUris(bundles, storagePlugin);
|
|
50775
|
+
const objects = await listObjects();
|
|
50776
|
+
const unreferenced = getPruneCandidates({
|
|
50777
|
+
bundleStorageReferences,
|
|
50778
|
+
liveBundleIds,
|
|
50779
|
+
objects,
|
|
50780
|
+
referencedAssetUris: referencedUris
|
|
50781
|
+
});
|
|
50782
|
+
const cutoff = Date.now() - protectNewerThan;
|
|
50783
|
+
let candidates = unreferenced.filter((object) => {
|
|
50784
|
+
const modifiedAt = object.lastModifiedAt?.getTime();
|
|
50785
|
+
return modifiedAt !== void 0 && modifiedAt <= cutoff;
|
|
50786
|
+
});
|
|
50787
|
+
if (options.yes && candidates.length > 0) {
|
|
50788
|
+
const refreshedBundles = await loadAllBundles(databasePlugin);
|
|
50789
|
+
const refreshedBundleIds = new Set(refreshedBundles.map((bundle) => bundle.id.toLowerCase()));
|
|
50790
|
+
const refreshedBundleStorageReferences = collectBundleStorageReferences(refreshedBundles);
|
|
50791
|
+
const { referencedUris: refreshedReferencedUris } = await collectReferencedAssetUris(refreshedBundles, storagePlugin);
|
|
50792
|
+
candidates = getPruneCandidates({
|
|
50793
|
+
bundleStorageReferences: refreshedBundleStorageReferences,
|
|
50794
|
+
liveBundleIds: refreshedBundleIds,
|
|
50795
|
+
objects: candidates,
|
|
50796
|
+
referencedAssetUris: refreshedReferencedUris
|
|
50797
|
+
});
|
|
50798
|
+
}
|
|
50799
|
+
const protectedCount = unreferenced.length - candidates.length;
|
|
50800
|
+
const bundleObjects = candidates.filter((candidate) => candidate.reason === "bundle");
|
|
50801
|
+
const assetObjects = candidates.filter((candidate) => candidate.reason === "asset");
|
|
50802
|
+
const candidateBytes = candidates.reduce((total, candidate) => total + candidate.size, 0);
|
|
50803
|
+
p.log.message(ui.block("Storage prune", [
|
|
50804
|
+
ui.kv("Storage", storagePlugin.name),
|
|
50805
|
+
ui.kv("Bundles", bundles.length),
|
|
50806
|
+
ui.kv("Manifests", manifestCount),
|
|
50807
|
+
ui.kv("Objects", objects.length),
|
|
50808
|
+
ui.kv("Protect newer", formatDuration(protectNewerThan)),
|
|
50809
|
+
ui.kv("Bundle data", bundleObjects.length),
|
|
50810
|
+
ui.kv("Shared assets", assetObjects.length),
|
|
50811
|
+
ui.kv("Reclaimable", formatBytes(candidateBytes)),
|
|
50812
|
+
protectedCount > 0 ? ui.kv("Protected", `${protectedCount} newer/undated objects`) : null
|
|
50813
|
+
].filter((line) => line !== null)));
|
|
50814
|
+
if (candidates.length === 0) {
|
|
50815
|
+
p.log.success("No objects are eligible for pruning.");
|
|
50816
|
+
return;
|
|
50817
|
+
}
|
|
50818
|
+
if (!options.yes) {
|
|
50819
|
+
p.log.message(ui.block("Eligible objects", [formatPruneCandidateTable(candidates)]));
|
|
50820
|
+
p.log.info(`Dry run only. Delete with ${ui.command(`hot-updater storage prune --protect-newer-than ${formatDuration(protectNewerThan)} --yes`)}.`);
|
|
50821
|
+
return;
|
|
50822
|
+
}
|
|
50823
|
+
await deleteObjects(candidates.map((candidate) => candidate.key));
|
|
50824
|
+
p.log.success(`Pruned ${candidates.length} objects (${formatBytes(candidateBytes)}).`);
|
|
50825
|
+
} finally {
|
|
50826
|
+
await safeOnUnmount(databasePlugin);
|
|
50827
|
+
}
|
|
50828
|
+
}
|
|
50829
|
+
//#endregion
|
|
50504
50830
|
//#region src/index.ts
|
|
50505
50831
|
const DEFAULT_CHANNEL = "production";
|
|
50506
50832
|
const parseBooleanOption = (value) => {
|
|
@@ -50528,7 +50854,7 @@ const channelCommand = program.command("channel").description("Manage channels")
|
|
|
50528
50854
|
channelCommand.action(handleChannel);
|
|
50529
50855
|
channelCommand.command("set").description("Set the channel for Android (BuildConfig) and iOS (Info.plist)").argument("<channel>", "the channel to set").action(handleSetChannel);
|
|
50530
50856
|
const bundleCommand = program.command("bundle").description("Manage bundles");
|
|
50531
|
-
bundleCommand.command("list").description("List bundles, most recent first").option("-c, --channel <channel>", "filter by channel").option("--json", "output raw bundle data as JSON").addOption(platformCommandOption).option("--limit <n>", "limit the number of results", (value) => {
|
|
50857
|
+
bundleCommand.command("list").description("List bundles, most recent first").option("-c, --channel <channel>", "filter by channel").option("--target-app-version <targetAppVersion>", "filter by exact target app version").option("--json", "output raw bundle data as JSON").addOption(platformCommandOption).option("--limit <n>", "limit the number of results", (value) => {
|
|
50532
50858
|
const n = Number.parseInt(value, 10);
|
|
50533
50859
|
if (!Number.isInteger(n) || n <= 0) throw new InvalidArgumentError("must be a positive integer");
|
|
50534
50860
|
return n;
|
|
@@ -50539,6 +50865,21 @@ bundleCommand.command("enable").description("Re-enable a previously disabled bun
|
|
|
50539
50865
|
bundleCommand.command("update").description("Update bundle rollout and targeting metadata").argument("<bundle-id>", "the bundle id to update").option("--rollout-cohort-count <count>", "rollout cohort count from 0 to 1000", parseRolloutCohortCount).option("--force-update <value>", "set force update flag (true or false)", parseBooleanOption).option("--target-cohorts <cohorts>", "comma-separated target cohorts").option("--clear-target-cohorts", "clear target cohorts").option("--json", "output the updated bundle as JSON").option("-y, --yes", "skip confirmation prompt").action(handleBundleUpdate);
|
|
50540
50866
|
bundleCommand.command("delete").description("Delete one or more bundle records by id").argument("<bundle-ids...>", "the bundle id(s) to delete").option("-y, --yes", "skip confirmation prompt").action((bundleIds, options) => handleBundleDelete(bundleIds, options));
|
|
50541
50867
|
bundleCommand.command("promote").description("Move or copy a bundle to a different channel").argument("<bundle-id>", "the id of the bundle to promote").requiredOption("-t, --target <channel>", "channel to promote the bundle to").addOption(new Option("-a, --action <action>", "promote action (copy creates a new bundle id; move keeps the id)").choices(["copy", "move"]).default("copy")).option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handlePromote(bundleId, options));
|
|
50868
|
+
program.command("storage").description("Manage stored bundle artifacts").command("prune").description("Find or delete unreferenced bundle objects and shared assets").addOption(new Option("--protect-newer-than <duration>", "protect unreferenced objects modified within this duration").argParser((value) => {
|
|
50869
|
+
try {
|
|
50870
|
+
return parseStoragePruneProtection(value);
|
|
50871
|
+
} catch (error) {
|
|
50872
|
+
throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
|
|
50873
|
+
}
|
|
50874
|
+
}).default(DEFAULT_STORAGE_PRUNE_PROTECTION_MS, "24h")).addOption(new Option("--dry-run", "list eligible objects without deleting them (default)").conflicts("yes")).addOption(new Option("-y, --yes", "delete eligible objects after reference validation").conflicts("dryRun")).addHelpText("after", `
|
|
50875
|
+
Examples:
|
|
50876
|
+
$ hot-updater storage prune --dry-run
|
|
50877
|
+
$ hot-updater storage prune --protect-newer-than 24h --yes
|
|
50878
|
+
|
|
50879
|
+
Only unreferenced bundle objects and shared assets are eligible.
|
|
50880
|
+
Protection uses object modification time, not time since bundle deletion.
|
|
50881
|
+
Deletion requires exclusive storage access; stop deploy and promote first.
|
|
50882
|
+
`).action((options) => handleStoragePrune(options));
|
|
50542
50883
|
const keysCommand = program.command("keys").description("Code signing key management");
|
|
50543
50884
|
keysCommand.command("generate").description("Generate RSA key pair for code signing").option("-o, --output <dir>", "output directory for keys", "./keys").option("-k, --key-size <size>", "key size (2048 or 4096)", (value) => {
|
|
50544
50885
|
const size = Number.parseInt(value, 10);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hot-updater",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.36.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0"
|
|
7
7
|
},
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"jiti": "2.6.1",
|
|
52
|
-
"@hot-updater/android-helper": "0.
|
|
53
|
-
"@hot-updater/apple-helper": "0.
|
|
54
|
-
"@hot-updater/cli-tools": "0.
|
|
55
|
-
"@hot-updater/
|
|
56
|
-
"@hot-updater/
|
|
57
|
-
"@hot-updater/plugin-core": "0.
|
|
58
|
-
"@hot-updater/server": "0.
|
|
52
|
+
"@hot-updater/android-helper": "0.36.0",
|
|
53
|
+
"@hot-updater/apple-helper": "0.36.0",
|
|
54
|
+
"@hot-updater/cli-tools": "0.36.0",
|
|
55
|
+
"@hot-updater/console": "0.36.0",
|
|
56
|
+
"@hot-updater/core": "0.36.0",
|
|
57
|
+
"@hot-updater/plugin-core": "0.36.0",
|
|
58
|
+
"@hot-updater/server": "0.36.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@bacons/xcode": "1.0.0-alpha.33",
|
|
@@ -78,12 +78,12 @@
|
|
|
78
78
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
79
79
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
80
80
|
"verkit": "0.3.2",
|
|
81
|
-
"@hot-updater/
|
|
82
|
-
"@hot-updater/
|
|
83
|
-
"@hot-updater/server": "0.
|
|
84
|
-
"@hot-updater/
|
|
85
|
-
"@hot-updater/
|
|
86
|
-
"@hot-updater/
|
|
81
|
+
"@hot-updater/cloudflare": "0.36.0",
|
|
82
|
+
"@hot-updater/aws": "0.36.0",
|
|
83
|
+
"@hot-updater/server": "0.36.0",
|
|
84
|
+
"@hot-updater/supabase": "0.36.0",
|
|
85
|
+
"@hot-updater/firebase": "0.36.0",
|
|
86
|
+
"@hot-updater/test-utils": "0.36.0"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@expo/fingerprint": "*",
|