@absolutejs/absolute 0.20.0-beta.82 → 0.20.0-beta.83

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/cli/index.js CHANGED
@@ -203,7 +203,7 @@ if (command === "dev") {
203
203
  sendTelemetryEvent("cli:command", {
204
204
  command: `mobile:${workspaceCommand ?? "unknown"}`
205
205
  });
206
- const { runMobile } = await import("./mobile-hkxh2ab5.js");
206
+ const { runMobile } = await import("./mobile-9dx39bd3.js");
207
207
  try {
208
208
  await runMobile(args);
209
209
  } catch (error) {
@@ -2,6 +2,9 @@
2
2
  import {
3
3
  installPackages
4
4
  } from "./index-zh6hhrwy.js";
5
+ import {
6
+ formatBytes
7
+ } from "./index-9r7n9dqp.js";
5
8
  import {
6
9
  start
7
10
  } from "./index-q78x1k9q.js";
@@ -21985,6 +21988,45 @@ var generateAbsoluteExpoCodeSigning = async (options) => {
21985
21988
  import { access as access11 } from "fs/promises";
21986
21989
  import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve11, sep as sep6 } from "path";
21987
21990
  import { pathToFileURL as pathToFileURL3 } from "url";
21991
+ var lifecycleMethod = (publisher, name) => {
21992
+ const method = publisher[name];
21993
+ if (typeof method !== "function")
21994
+ throw new TypeError(`Mobile update registry does not support ${name}. Re-run \`absolute mobile update provision --force\` after upgrading @absolutejs/deploy.`);
21995
+ return method;
21996
+ };
21997
+ var validateStorageIdentity = (result, appId) => {
21998
+ if (!object3(result) || result.appId !== appId || !Array.isArray(result.releases) || ![
21999
+ result.channelCount,
22000
+ result.reclaimableBytes,
22001
+ result.releaseBytes,
22002
+ result.releaseCount,
22003
+ result.totalBytes,
22004
+ result.totalObjectCount,
22005
+ result.untrackedBytes
22006
+ ].every((value) => Number.isSafeInteger(value) && value >= 0))
22007
+ throw new TypeError("Mobile update registry returned an invalid storage report.");
22008
+ return result;
22009
+ };
22010
+ var inspectAbsoluteMobileUpdateStorage = async (options) => validateStorageIdentity(await lifecycleMethod(options.publisher, "inspectUpdateStorage")({
22011
+ appId: options.appId,
22012
+ ...options.minAgeMs === undefined ? {} : { minAgeMs: options.minAgeMs },
22013
+ ...options.retainRecent === undefined ? {} : { retainRecent: options.retainRecent },
22014
+ ...options.signal ? { signal: options.signal } : {}
22015
+ }), options.appId);
22016
+ var pruneAbsoluteMobileUpdates = async (options) => {
22017
+ const result = await lifecycleMethod(options.publisher, "pruneUpdates")({
22018
+ appId: options.appId,
22019
+ ...options.apply === undefined ? {} : { apply: options.apply },
22020
+ ...options.gracePeriodMs === undefined ? {} : { gracePeriodMs: options.gracePeriodMs },
22021
+ ...options.minAgeMs === undefined ? {} : { minAgeMs: options.minAgeMs },
22022
+ ...options.retainRecent === undefined ? {} : { retainRecent: options.retainRecent },
22023
+ ...options.signal ? { signal: options.signal } : {}
22024
+ });
22025
+ validateStorageIdentity(result, options.appId);
22026
+ if (!Array.isArray(result.marked) || !Array.isArray(result.restored) || !Array.isArray(result.swept) || typeof result.dryRun !== "boolean" || !Number.isSafeInteger(result.reclaimedBytes) || result.reclaimedBytes < 0)
22027
+ throw new TypeError("Mobile update registry returned an invalid collection report.");
22028
+ return result;
22029
+ };
21988
22030
  var object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21989
22031
  var isPublisher2 = (value) => object3(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function";
21990
22032
  var projectPath3 = (projectRoot, requested, label) => {
@@ -22741,7 +22783,7 @@ var provisionMobileUpdate = async (args) => {
22741
22783
  throw new TypeError("--storage must be local or s3.");
22742
22784
  const modulePath = valueAfter(args, "--registry") ?? mobile.updateServer.registryModule;
22743
22785
  const packages = [
22744
- "@absolutejs/deploy@0.25.6",
22786
+ "@absolutejs/deploy@0.25.7",
22745
22787
  "@absolutejs/blob@0.5.2",
22746
22788
  ...requestedStorage === "s3" ? [
22747
22789
  "@aws-sdk/client-s3@3.1095.0",
@@ -22825,6 +22867,97 @@ var rollbackMobileUpdate = async (args) => {
22825
22867
  console.log(result.releaseId ? `Rolled ${result.channel} back to ${result.releaseId}.` : `Rolled ${result.channel} back to the embedded store build.`);
22826
22868
  return result;
22827
22869
  };
22870
+ var mobileUpdateDays = (args, flag) => {
22871
+ const value = valueAfter(args, flag);
22872
+ if (value === undefined)
22873
+ return;
22874
+ const parsed = Number(value);
22875
+ const milliseconds = parsed * 24 * 60 * 60 * 1000;
22876
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isSafeInteger(milliseconds))
22877
+ throw new TypeError(`${flag} must be a non-negative number of days.`);
22878
+ return milliseconds;
22879
+ };
22880
+ var mobileUpdateRetention = (args) => {
22881
+ const retainedValue = valueAfter(args, "--retain");
22882
+ const retainRecent = retainedValue === undefined ? undefined : Number(retainedValue);
22883
+ if (retainRecent !== undefined && (!Number.isSafeInteger(retainRecent) || retainRecent < 0))
22884
+ throw new TypeError("--retain must be a non-negative integer.");
22885
+ return {
22886
+ minAgeMs: mobileUpdateDays(args, "--min-age-days"),
22887
+ retainRecent
22888
+ };
22889
+ };
22890
+ var printMobileUpdateStorage = (report) => {
22891
+ console.log(`${report.appId}: ${report.releaseCount} releases use ${formatBytes(report.releaseBytes)}; ${formatBytes(report.reclaimableBytes)} is eligible across ${report.totalObjectCount} stored objects.`);
22892
+ for (const release of report.releases) {
22893
+ let state = "eligible";
22894
+ if (release.protectedBy.length > 0)
22895
+ state = `kept: ${release.protectedBy.join(", ")}`;
22896
+ else if (release.markedAt)
22897
+ state = `marked ${release.markedAt}`;
22898
+ console.log(` ${release.releaseId} ${release.channel} ${formatBytes(release.bytes)} (${state})`);
22899
+ }
22900
+ if (report.untrackedBytes > 0)
22901
+ console.log(`${formatBytes(report.untrackedBytes)} is used by channels, collection markers, or incomplete/unrecognized objects and will not be swept as a release.`);
22902
+ };
22903
+ var inspectMobileUpdateStorage = async (args) => {
22904
+ const startedAt = performance.now();
22905
+ const { mobile } = await loadMobile(valueAfter(args, "--config"));
22906
+ if (!mobile.updates)
22907
+ throw new TypeError("mobile update storage requires mobile.updates config.");
22908
+ const policy = mobileUpdateRetention(args);
22909
+ const { publisher } = await mobileUpdatePublisher(args);
22910
+ const report = await inspectAbsoluteMobileUpdateStorage({
22911
+ appId: mobile.appId,
22912
+ publisher,
22913
+ ...policy.minAgeMs === undefined ? {} : { minAgeMs: policy.minAgeMs },
22914
+ ...policy.retainRecent === undefined ? {} : { retainRecent: policy.retainRecent }
22915
+ });
22916
+ if (args.includes("--json"))
22917
+ console.log(JSON.stringify(report, null, 2));
22918
+ else
22919
+ printMobileUpdateStorage(report);
22920
+ sendTelemetryEvent("mobile:update-storage", {
22921
+ durationMs: Math.round(performance.now() - startedAt),
22922
+ reclaimableReleaseCount: report.releases.filter((release) => release.protectedBy.length === 0).length,
22923
+ releaseCount: report.releaseCount
22924
+ });
22925
+ return report;
22926
+ };
22927
+ var collectMobileUpdates = async (args) => {
22928
+ const startedAt = performance.now();
22929
+ const { mobile } = await loadMobile(valueAfter(args, "--config"));
22930
+ if (!mobile.updates)
22931
+ throw new TypeError("mobile update gc requires mobile.updates config.");
22932
+ const policy = mobileUpdateRetention(args);
22933
+ const gracePeriodMs = mobileUpdateDays(args, "--grace-days");
22934
+ const { publisher } = await mobileUpdatePublisher(args);
22935
+ const result = await pruneAbsoluteMobileUpdates({
22936
+ appId: mobile.appId,
22937
+ apply: args.includes("--apply"),
22938
+ publisher,
22939
+ ...gracePeriodMs === undefined ? {} : { gracePeriodMs },
22940
+ ...policy.minAgeMs === undefined ? {} : { minAgeMs: policy.minAgeMs },
22941
+ ...policy.retainRecent === undefined ? {} : { retainRecent: policy.retainRecent }
22942
+ });
22943
+ if (args.includes("--json"))
22944
+ console.log(JSON.stringify(result, null, 2));
22945
+ else {
22946
+ printMobileUpdateStorage(result);
22947
+ if (result.dryRun)
22948
+ console.log("No storage was changed. Re-run with --apply to mark eligible releases and sweep releases whose grace period has elapsed.");
22949
+ else
22950
+ console.log(`Collection applied: ${result.marked.length} marked, ${result.restored.length} restored, ${result.swept.length} swept, ${formatBytes(result.reclaimedBytes)} reclaimed.`);
22951
+ }
22952
+ sendTelemetryEvent("mobile:update-gc", {
22953
+ applied: !result.dryRun,
22954
+ durationMs: Math.round(performance.now() - startedAt),
22955
+ markedCount: result.marked.length,
22956
+ restoredCount: result.restored.length,
22957
+ sweptCount: result.swept.length
22958
+ });
22959
+ return result;
22960
+ };
22828
22961
  var requireValueAfter = (args, flag) => {
22829
22962
  const value = valueAfter(args, flag);
22830
22963
  if (!value || value.startsWith("-")) {
@@ -24165,6 +24298,14 @@ var runMobile = async (args) => {
24165
24298
  await rollbackMobileUpdate(args.slice(2));
24166
24299
  return;
24167
24300
  }
24301
+ if (command === "update" && args[1] === "storage") {
24302
+ await inspectMobileUpdateStorage(args.slice(2));
24303
+ return;
24304
+ }
24305
+ if (command === "update" && args[1] === "gc") {
24306
+ await collectMobileUpdates(args.slice(2));
24307
+ return;
24308
+ }
24168
24309
  if (command === "publish" && args[1] === "android") {
24169
24310
  await publishAndroid(args.slice(2));
24170
24311
  return;
@@ -24173,7 +24314,7 @@ var runMobile = async (args) => {
24173
24314
  await publishIos(args.slice(2));
24174
24315
  return;
24175
24316
  }
24176
- throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [inspect [name] [--json] | clean [name] --yes | --json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] [--require-bundle] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--remote name] [--outdir dir] [--web-outdir dir] [--unsigned] | update provision [--storage local|s3] [--registry module] [--force] [--yes] | update signing generate --private-key path [--certificate path] [--public-key path] [--key-id id] [--common-name name] [--validity-years n] | update build [server-entry] --classification bug-fix|content|security --key-id id --signing-key path --within-submitted-purpose [--outdir dir] [--web-outdir dir] | update publish <release-directory> [--rollout fraction] [--registry module] | update promote --release id --rollout fraction [--registry module] | update rollback [--release id] [--registry module] | publish android [server-entry] [--registry module] [--channel name] [--play-track track] [--play-status completed|draft|halted|in-progress] [--play-rollout fraction] [--play-name name] [--play-notes language=text] [--play-update-priority 0..5] [--play-hold-review] [--play-cancel-existing-review] [--outdir dir] [--web-outdir dir] [--unsigned] | publish ios [server-entry] [--remote name] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json] | test ios [--device identifier [--remote name] | --udid id] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--artifacts dir] [--json]> [--config path]");
24317
+ throw new TypeError("Usage: absolute mobile <pair mac <name> <user@host> [--port n] [--workspace path] | remotes [inspect [name] [--json] | clean [name] --yes | --json] | unpair mac <name> | init [--no-native] [--force] | sync [ios|android] | inspect [--json] [--require-bundle] | associations [--outdir dir] [--verify] | ci github [server-entry] [--publish] [--registry module] [--secret-env NAME] [--output path] [--force] [--json] | doctor [ios|android|release [ios|android]] [--remote name] [--json|--fix [--yes]] | build <android|ios> [server-entry] [--remote name] [--outdir dir] [--web-outdir dir] [--unsigned] | update provision [--storage local|s3] [--registry module] [--force] [--yes] | update signing generate --private-key path [--certificate path] [--public-key path] [--key-id id] [--common-name name] [--validity-years n] | update build [server-entry] --classification bug-fix|content|security --key-id id --signing-key path --within-submitted-purpose [--outdir dir] [--web-outdir dir] | update publish <release-directory> [--rollout fraction] [--registry module] | update promote --release id --rollout fraction [--registry module] | update rollback [--release id] [--registry module] | update storage [--retain count] [--min-age-days days] [--registry module] [--json] | update gc [--retain count] [--min-age-days days] [--grace-days days] [--apply] [--registry module] [--json] | publish android [server-entry] [--registry module] [--channel name] [--play-track track] [--play-status completed|draft|halted|in-progress] [--play-rollout fraction] [--play-name name] [--play-notes language=text] [--play-update-priority 0..5] [--play-hold-review] [--play-cancel-existing-review] [--outdir dir] [--web-outdir dir] [--unsigned] | publish ios [server-entry] [--remote name] [--registry module] [--channel name] [--testflight-group name-or-id] [--testflight-notes locale=text] [--testflight-submit-review] [--outdir dir] [--web-outdir dir] [--unsigned] | test android [--route path] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--serial id] [--artifacts dir] [--json] | test ios [--device identifier [--remote name] | --udid id] [--wait-for-hmr] [--report [dir]] [--timeout ms] [--port n] [--artifacts dir] [--json]> [--config path]");
24177
24318
  };
24178
24319
  export {
24179
24320
  runMobile
package/dist/index.js CHANGED
@@ -12288,7 +12288,7 @@ import {
12288
12288
  } from "crypto";
12289
12289
  import { readFile as readFile4, stat as stat2 } from "fs/promises";
12290
12290
  import path from "path";
12291
- var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
12291
+ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updates", MAX_FILE_BYTES, MAX_TOTAL_BYTES, DAY_MS, DEFAULT_MIN_AGE_MS, DEFAULT_GRACE_PERIOD_MS, DEFAULT_RETAIN_RECENT = 5, HASH, RELEASE, APP_ID, NAME, EXPO_DESCRIPTOR = "_absolute/expo-update.json", EXPO_CODE_SIGNING_ALGORITHM = "rsa-v1_5-sha256", MobileUpdateRegistryError, object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), text2 = (value, field) => {
12292
12292
  if (typeof value !== "string" || value.length === 0)
12293
12293
  throw new MobileUpdateRegistryError(`Mobile update ${field} is invalid`);
12294
12294
  return value;
@@ -12455,6 +12455,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12455
12455
  const releaseRoot = (manifest) => `${root(manifest.appId)}/releases/${manifest.releaseId}`;
12456
12456
  const manifestKey = (manifest) => `${releaseRoot(manifest)}/update.json`;
12457
12457
  const fileKey = (manifest, file2) => `${releaseRoot(manifest)}/files/${file2.path}`;
12458
+ const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
12458
12459
  const channelKey = (appId, channel) => {
12459
12460
  if (!APP_ID.test(appId) || !NAME.test(channel))
12460
12461
  throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
@@ -12483,6 +12484,12 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12483
12484
  throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
12484
12485
  return value;
12485
12486
  };
12487
+ const assertNotMarked = async (appId, releaseId) => {
12488
+ if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
12489
+ throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
12490
+ if (await options.store.head(tombstoneKey(appId, releaseId)))
12491
+ throw new MobileUpdateRegistryError("Mobile update release is marked for collection. Increase retention and apply garbage collection to restore it before promotion");
12492
+ };
12486
12493
  const writeChannel = async (input, signal) => {
12487
12494
  const value = {
12488
12495
  ...input,
@@ -12507,6 +12514,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12507
12514
  input.signal?.throwIfAborted();
12508
12515
  if (input.rollout <= 0 || input.rollout > 1)
12509
12516
  throw new MobileUpdateRegistryError("Mobile update rollout is invalid");
12517
+ await assertNotMarked(input.appId, input.releaseId);
12510
12518
  const release = await readManifest(input.appId, input.releaseId);
12511
12519
  if (!release || release.manifest.channel !== input.channel)
12512
12520
  throw new MobileUpdateRegistryError("Mobile update was not published to this channel");
@@ -12552,11 +12560,221 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12552
12560
  status: "selected"
12553
12561
  };
12554
12562
  };
12563
+ const retentionValues = (input) => {
12564
+ if (!APP_ID.test(input.appId))
12565
+ throw new MobileUpdateRegistryError("Mobile update appId is invalid");
12566
+ const retainRecent = input.retainRecent ?? DEFAULT_RETAIN_RECENT;
12567
+ const minAgeMs = input.minAgeMs ?? DEFAULT_MIN_AGE_MS;
12568
+ if (!Number.isSafeInteger(retainRecent) || retainRecent < 0)
12569
+ throw new MobileUpdateRegistryError("Mobile update retained release count is invalid");
12570
+ if (!Number.isSafeInteger(minAgeMs) || minAgeMs < 0)
12571
+ throw new MobileUpdateRegistryError("Mobile update minimum release age is invalid");
12572
+ return { minAgeMs, retainRecent };
12573
+ };
12574
+ const listObjects = async (appId, signal) => {
12575
+ const list = options.store.list;
12576
+ if (!list)
12577
+ throw new MobileUpdateRegistryError("Mobile update storage does not support lifecycle listing");
12578
+ const objects = [];
12579
+ const cursors = new Set;
12580
+ let cursor;
12581
+ do {
12582
+ signal?.throwIfAborted();
12583
+ const page = await list({
12584
+ ...cursor ? { cursor } : {},
12585
+ prefix: `${root(appId)}/`
12586
+ });
12587
+ objects.push(...page.objects);
12588
+ if (!page.truncated)
12589
+ break;
12590
+ if (!page.cursor || cursors.has(page.cursor))
12591
+ throw new MobileUpdateRegistryError("Mobile update storage returned an invalid lifecycle cursor");
12592
+ cursors.add(page.cursor);
12593
+ cursor = page.cursor;
12594
+ } while (true);
12595
+ return objects;
12596
+ };
12597
+ const inventory = async (input) => {
12598
+ const { minAgeMs, retainRecent } = retentionValues(input);
12599
+ const objects = await listObjects(input.appId, input.signal);
12600
+ const appRoot = `${root(input.appId)}/`;
12601
+ const releasePrefix = `${appRoot}releases/`;
12602
+ const channelPrefix = `${appRoot}channels/`;
12603
+ const markerPrefix = `${appRoot}gc/`;
12604
+ const objectKeysByRelease = new Map;
12605
+ const objectBytesByRelease = new Map;
12606
+ for (const blobObject of objects) {
12607
+ if (!blobObject.key.startsWith(releasePrefix))
12608
+ continue;
12609
+ const suffix = blobObject.key.slice(releasePrefix.length);
12610
+ const releaseId = suffix.slice(0, suffix.indexOf("/"));
12611
+ if (!RELEASE.test(releaseId))
12612
+ continue;
12613
+ objectKeysByRelease.set(releaseId, [
12614
+ ...objectKeysByRelease.get(releaseId) ?? [],
12615
+ blobObject.key
12616
+ ]);
12617
+ objectBytesByRelease.set(releaseId, (objectBytesByRelease.get(releaseId) ?? 0) + blobObject.size);
12618
+ }
12619
+ const channels = [];
12620
+ for (const blobObject of objects) {
12621
+ if (!blobObject.key.startsWith(channelPrefix) || !blobObject.key.endsWith(".json"))
12622
+ continue;
12623
+ const bytes = await options.store.get(blobObject.key);
12624
+ if (!bytes)
12625
+ throw new MobileUpdateRegistryError("Mobile update channel disappeared during lifecycle inspection");
12626
+ const channel = parseChannel(decode(bytes));
12627
+ if (channel.appId !== input.appId)
12628
+ throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
12629
+ channels.push(channel);
12630
+ }
12631
+ const markedAt = new Map;
12632
+ for (const blobObject of objects) {
12633
+ if (!blobObject.key.startsWith(markerPrefix) || !blobObject.key.endsWith(".json"))
12634
+ continue;
12635
+ const releaseId = blobObject.key.slice(markerPrefix.length, -".json".length);
12636
+ if (!RELEASE.test(releaseId))
12637
+ continue;
12638
+ const bytes = await options.store.get(blobObject.key);
12639
+ if (!bytes)
12640
+ continue;
12641
+ const marker = decode(bytes);
12642
+ if (!object3(marker) || marker.format !== 1 || marker.appId !== input.appId || marker.releaseId !== releaseId || !iso(marker.markedAt))
12643
+ throw new MobileUpdateRegistryError("Mobile update collection marker is invalid");
12644
+ markedAt.set(releaseId, marker.markedAt);
12645
+ }
12646
+ const active = new Set(channels.flatMap((channel) => channel.releaseId ? [channel.releaseId] : []));
12647
+ const fallback = new Set(channels.flatMap((channel) => channel.fallbackReleaseId ? [channel.fallbackReleaseId] : []));
12648
+ const manifests = [];
12649
+ for (const releaseId of objectKeysByRelease.keys()) {
12650
+ const release = await readManifest(input.appId, releaseId);
12651
+ if (release)
12652
+ manifests.push(release.manifest);
12653
+ }
12654
+ const recent = new Set;
12655
+ const manifestsByChannel = new Map;
12656
+ for (const manifest of manifests)
12657
+ manifestsByChannel.set(manifest.channel, [
12658
+ ...manifestsByChannel.get(manifest.channel) ?? [],
12659
+ manifest
12660
+ ]);
12661
+ for (const releases2 of manifestsByChannel.values())
12662
+ for (const manifest of releases2.toSorted((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, retainRecent))
12663
+ recent.add(manifest.releaseId);
12664
+ const now = clock().getTime();
12665
+ const releases = manifests.map((manifest) => {
12666
+ const protectedBy = [];
12667
+ if (active.has(manifest.releaseId))
12668
+ protectedBy.push("active");
12669
+ if (fallback.has(manifest.releaseId))
12670
+ protectedBy.push("fallback");
12671
+ if (recent.has(manifest.releaseId))
12672
+ protectedBy.push("recent");
12673
+ if (now - Date.parse(manifest.createdAt) < minAgeMs)
12674
+ protectedBy.push("age");
12675
+ return {
12676
+ bytes: objectBytesByRelease.get(manifest.releaseId) ?? 0,
12677
+ channel: manifest.channel,
12678
+ createdAt: manifest.createdAt,
12679
+ ...markedAt.has(manifest.releaseId) ? { markedAt: markedAt.get(manifest.releaseId) } : {},
12680
+ objectCount: objectKeysByRelease.get(manifest.releaseId)?.length ?? 0,
12681
+ protectedBy,
12682
+ releaseId: manifest.releaseId
12683
+ };
12684
+ }).toSorted((left, right) => right.createdAt.localeCompare(left.createdAt));
12685
+ const releaseBytes = releases.reduce((total, release) => total + release.bytes, 0);
12686
+ const totalBytes = objects.reduce((total, object22) => total + object22.size, 0);
12687
+ return {
12688
+ objectKeysByRelease,
12689
+ report: {
12690
+ appId: input.appId,
12691
+ channelCount: channels.length,
12692
+ reclaimableBytes: releases.filter((release) => release.protectedBy.length === 0).reduce((total, release) => total + release.bytes, 0),
12693
+ releaseBytes,
12694
+ releaseCount: releases.length,
12695
+ releases,
12696
+ totalBytes,
12697
+ totalObjectCount: objects.length,
12698
+ untrackedBytes: totalBytes - releaseBytes
12699
+ }
12700
+ };
12701
+ };
12702
+ const pruneUpdates = async (input) => {
12703
+ const initial = await inventory(input);
12704
+ const result = {
12705
+ ...initial.report,
12706
+ dryRun: input.apply !== true,
12707
+ marked: [],
12708
+ reclaimedBytes: 0,
12709
+ restored: [],
12710
+ swept: []
12711
+ };
12712
+ if (input.apply !== true)
12713
+ return result;
12714
+ const remove = options.store.delete;
12715
+ if (!remove)
12716
+ throw new MobileUpdateRegistryError("Mobile update storage does not support lifecycle deletion");
12717
+ const gracePeriodMs = input.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
12718
+ if (!Number.isSafeInteger(gracePeriodMs) || gracePeriodMs < 0)
12719
+ throw new MobileUpdateRegistryError("Mobile update collection grace period is invalid");
12720
+ const now = clock();
12721
+ for (const release of initial.report.releases) {
12722
+ input.signal?.throwIfAborted();
12723
+ const marker = tombstoneKey(input.appId, release.releaseId);
12724
+ if (release.protectedBy.length > 0) {
12725
+ if (release.markedAt) {
12726
+ await remove(marker);
12727
+ result.restored.push(release.releaseId);
12728
+ }
12729
+ continue;
12730
+ }
12731
+ if (!release.markedAt) {
12732
+ const bytes = json({
12733
+ appId: input.appId,
12734
+ format: 1,
12735
+ markedAt: now.toISOString(),
12736
+ releaseId: release.releaseId
12737
+ });
12738
+ await options.store.put(marker, bytes, {
12739
+ cacheControl: "no-cache",
12740
+ contentType: "application/json",
12741
+ maxBytes: bytes.byteLength,
12742
+ metadata: { releaseid: release.releaseId, sha256: digest(bytes) },
12743
+ signal: input.signal
12744
+ });
12745
+ result.marked.push(release.releaseId);
12746
+ }
12747
+ }
12748
+ const current = await inventory(input);
12749
+ for (const release of current.report.releases) {
12750
+ input.signal?.throwIfAborted();
12751
+ if (release.protectedBy.length > 0 || !release.markedAt || now.getTime() - Date.parse(release.markedAt) < gracePeriodMs || result.marked.includes(release.releaseId))
12752
+ continue;
12753
+ const checked = await inventory(input);
12754
+ const candidate = checked.report.releases.find((value) => value.releaseId === release.releaseId);
12755
+ if (!candidate || candidate.protectedBy.length > 0 || !candidate.markedAt)
12756
+ continue;
12757
+ const keys = checked.objectKeysByRelease.get(release.releaseId) ?? [];
12758
+ for (const key of keys.toSorted((left, right) => {
12759
+ const leftManifest = left.endsWith("/update.json");
12760
+ const rightManifest = right.endsWith("/update.json");
12761
+ return Number(leftManifest) - Number(rightManifest);
12762
+ }))
12763
+ await remove(key);
12764
+ await remove(tombstoneKey(input.appId, release.releaseId));
12765
+ result.reclaimedBytes += candidate.bytes;
12766
+ result.swept.push(release.releaseId);
12767
+ }
12768
+ return result;
12769
+ };
12555
12770
  return {
12771
+ inspectUpdateStorage: async (input) => (await inventory(input)).report,
12772
+ pruneUpdates,
12556
12773
  publishUpdate: async (input) => {
12557
12774
  input.signal?.throwIfAborted();
12558
12775
  const manifest = parseMobileUpdateManifest(input.manifest);
12559
12776
  verifyManifestSignature(manifest, options.publicKeys);
12777
+ await assertNotMarked(manifest.appId, manifest.releaseId);
12560
12778
  const localRoot = path.resolve(input.releaseDirectory);
12561
12779
  const localManifest = parseMobileUpdateManifest(JSON.parse(await readFile4(path.join(localRoot, "update.json"), "utf8")));
12562
12780
  if (JSON.stringify(localManifest) !== JSON.stringify(manifest))
@@ -12621,6 +12839,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12621
12839
  if (!existing)
12622
12840
  throw new MobileUpdateRegistryError("Mobile update channel does not exist");
12623
12841
  if (input.releaseId) {
12842
+ await assertNotMarked(input.appId, input.releaseId);
12624
12843
  const release = await readManifest(input.appId, input.releaseId);
12625
12844
  if (!release || release.manifest.channel !== input.channel)
12626
12845
  throw new MobileUpdateRegistryError("Mobile rollback release was not published");
@@ -12953,6 +13172,9 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12953
13172
  var init_mobileUpdate = __esm(() => {
12954
13173
  MAX_FILE_BYTES = 32 * 1024 * 1024;
12955
13174
  MAX_TOTAL_BYTES = 128 * 1024 * 1024;
13175
+ DAY_MS = 24 * 60 * 60 * 1000;
13176
+ DEFAULT_MIN_AGE_MS = 30 * DAY_MS;
13177
+ DEFAULT_GRACE_PERIOD_MS = 7 * DAY_MS;
12956
13178
  HASH = /^[a-f0-9]{64}$/;
12957
13179
  RELEASE = /^amu_[a-f0-9]{64}$/;
12958
13180
  APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
@@ -44587,5 +44809,5 @@ export {
44587
44809
  wrapPageHandlerWithStreamingSlots
44588
44810
  };
44589
44811
 
44590
- //# debugId=D254EAF4F11DFBA864756E2164756E21
44812
+ //# debugId=F0D48E431F12D4EB64756E2164756E21
44591
44813
  //# sourceMappingURL=index.js.map