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

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-pdck6a35.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,50 @@ 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 validOptionalCounters = (values) => values.every((value) => value === undefined) || values.every((value) => Number.isSafeInteger(value) && (value ?? -1) >= 0);
21998
+ var validateStorageIdentity = (result, appId) => {
21999
+ if (!object3(result) || result.appId !== appId || !Array.isArray(result.releases) || ![
22000
+ result.channelCount,
22001
+ result.reclaimableBytes,
22002
+ result.releaseBytes,
22003
+ result.releaseCount,
22004
+ result.totalBytes,
22005
+ result.totalObjectCount,
22006
+ result.untrackedBytes
22007
+ ].every((value) => Number.isSafeInteger(value) && value >= 0) || !validOptionalCounters([
22008
+ result.contentBlobBytes,
22009
+ result.contentBlobCount,
22010
+ result.reclaimableContentBytes
22011
+ ]))
22012
+ throw new TypeError("Mobile update registry returned an invalid storage report.");
22013
+ return result;
22014
+ };
22015
+ var inspectAbsoluteMobileUpdateStorage = async (options) => validateStorageIdentity(await lifecycleMethod(options.publisher, "inspectUpdateStorage")({
22016
+ appId: options.appId,
22017
+ ...options.minAgeMs === undefined ? {} : { minAgeMs: options.minAgeMs },
22018
+ ...options.retainRecent === undefined ? {} : { retainRecent: options.retainRecent },
22019
+ ...options.signal ? { signal: options.signal } : {}
22020
+ }), options.appId);
22021
+ var pruneAbsoluteMobileUpdates = async (options) => {
22022
+ const result = await lifecycleMethod(options.publisher, "pruneUpdates")({
22023
+ appId: options.appId,
22024
+ ...options.apply === undefined ? {} : { apply: options.apply },
22025
+ ...options.gracePeriodMs === undefined ? {} : { gracePeriodMs: options.gracePeriodMs },
22026
+ ...options.minAgeMs === undefined ? {} : { minAgeMs: options.minAgeMs },
22027
+ ...options.retainRecent === undefined ? {} : { retainRecent: options.retainRecent },
22028
+ ...options.signal ? { signal: options.signal } : {}
22029
+ });
22030
+ validateStorageIdentity(result, options.appId);
22031
+ if (!Array.isArray(result.marked) || !Array.isArray(result.restored) || !Array.isArray(result.swept) || result.sweptContentBlobs !== undefined && !Array.isArray(result.sweptContentBlobs) || typeof result.dryRun !== "boolean" || !Number.isSafeInteger(result.reclaimedBytes) || result.reclaimedBytes < 0)
22032
+ throw new TypeError("Mobile update registry returned an invalid collection report.");
22033
+ return result;
22034
+ };
21988
22035
  var object3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21989
22036
  var isPublisher2 = (value) => object3(value) && typeof value.publishUpdate === "function" && typeof value.promoteUpdate === "function" && typeof value.rollbackUpdate === "function";
21990
22037
  var projectPath3 = (projectRoot, requested, label) => {
@@ -22027,7 +22074,12 @@ var publishAbsoluteMobileUpdate = async (options) => {
22027
22074
  rollout: options.rollout,
22028
22075
  signal: options.signal
22029
22076
  });
22030
- if (result.appId !== manifest.appId || result.channel !== manifest.channel || result.releaseId !== manifest.releaseId || result.rollout !== options.rollout || result.stage !== "published" || typeof result.reused !== "boolean")
22077
+ if (result.appId !== manifest.appId || result.channel !== manifest.channel || result.releaseId !== manifest.releaseId || result.rollout !== options.rollout || result.stage !== "published" || typeof result.reused !== "boolean" || !validOptionalCounters([
22078
+ result.storedBytes,
22079
+ result.storedFiles,
22080
+ result.reusedBytes,
22081
+ result.reusedFiles
22082
+ ]))
22031
22083
  throw new TypeError("Mobile update registry returned a different publication identity.");
22032
22084
  return result;
22033
22085
  };
@@ -22741,7 +22793,7 @@ var provisionMobileUpdate = async (args) => {
22741
22793
  throw new TypeError("--storage must be local or s3.");
22742
22794
  const modulePath = valueAfter(args, "--registry") ?? mobile.updateServer.registryModule;
22743
22795
  const packages = [
22744
- "@absolutejs/deploy@0.25.6",
22796
+ "@absolutejs/deploy@0.25.7",
22745
22797
  "@absolutejs/blob@0.5.2",
22746
22798
  ...requestedStorage === "s3" ? [
22747
22799
  "@aws-sdk/client-s3@3.1095.0",
@@ -22790,6 +22842,8 @@ var publishMobileUpdate = async (args) => {
22790
22842
  rollout: updateRollout(args, 0.05)
22791
22843
  });
22792
22844
  console.log(`${result.reused ? "Reused" : "Published"} mobile update ${result.releaseId} to ${result.channel} at ${Math.round(result.rollout * 100)}%.`);
22845
+ if (result.storedBytes !== undefined && result.reusedBytes !== undefined && result.storedFiles !== undefined && result.reusedFiles !== undefined)
22846
+ console.log(`Stored ${formatBytes(result.storedBytes)} in ${result.storedFiles} new content blobs; reused ${formatBytes(result.reusedBytes)} across ${result.reusedFiles} files.`);
22793
22847
  return result;
22794
22848
  };
22795
22849
  var promoteMobileUpdate = async (args) => {
@@ -22825,6 +22879,99 @@ var rollbackMobileUpdate = async (args) => {
22825
22879
  console.log(result.releaseId ? `Rolled ${result.channel} back to ${result.releaseId}.` : `Rolled ${result.channel} back to the embedded store build.`);
22826
22880
  return result;
22827
22881
  };
22882
+ var mobileUpdateDays = (args, flag) => {
22883
+ const value = valueAfter(args, flag);
22884
+ if (value === undefined)
22885
+ return;
22886
+ const parsed = Number(value);
22887
+ const milliseconds = parsed * 24 * 60 * 60 * 1000;
22888
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isSafeInteger(milliseconds))
22889
+ throw new TypeError(`${flag} must be a non-negative number of days.`);
22890
+ return milliseconds;
22891
+ };
22892
+ var mobileUpdateRetention = (args) => {
22893
+ const retainedValue = valueAfter(args, "--retain");
22894
+ const retainRecent = retainedValue === undefined ? undefined : Number(retainedValue);
22895
+ if (retainRecent !== undefined && (!Number.isSafeInteger(retainRecent) || retainRecent < 0))
22896
+ throw new TypeError("--retain must be a non-negative integer.");
22897
+ return {
22898
+ minAgeMs: mobileUpdateDays(args, "--min-age-days"),
22899
+ retainRecent
22900
+ };
22901
+ };
22902
+ var printMobileUpdateStorage = (report) => {
22903
+ console.log(`${report.appId}: ${report.releaseCount} releases use ${formatBytes(report.releaseBytes)}; ${formatBytes(report.reclaimableBytes)} is eligible across ${report.totalObjectCount} stored objects.`);
22904
+ if (typeof report.contentBlobCount === "number" && typeof report.contentBlobBytes === "number" && typeof report.reclaimableContentBytes === "number" && report.contentBlobCount > 0)
22905
+ console.log(` Shared content: ${report.contentBlobCount} blobs use ${formatBytes(report.contentBlobBytes)}; ${formatBytes(report.reclaimableContentBytes)} becomes reclaimable with the eligible releases.`);
22906
+ for (const release of report.releases) {
22907
+ let state = "eligible";
22908
+ if (release.protectedBy.length > 0)
22909
+ state = `kept: ${release.protectedBy.join(", ")}`;
22910
+ else if (release.markedAt)
22911
+ state = `marked ${release.markedAt}`;
22912
+ console.log(` ${release.releaseId} ${release.channel} ${formatBytes(release.bytes)} (${state})`);
22913
+ }
22914
+ if (report.untrackedBytes > 0)
22915
+ console.log(`${formatBytes(report.untrackedBytes)} is used by channels, collection markers, or incomplete/unrecognized objects and will not be swept as a release.`);
22916
+ };
22917
+ var inspectMobileUpdateStorage = async (args) => {
22918
+ const startedAt = performance.now();
22919
+ const { mobile } = await loadMobile(valueAfter(args, "--config"));
22920
+ if (!mobile.updates)
22921
+ throw new TypeError("mobile update storage requires mobile.updates config.");
22922
+ const policy = mobileUpdateRetention(args);
22923
+ const { publisher } = await mobileUpdatePublisher(args);
22924
+ const report = await inspectAbsoluteMobileUpdateStorage({
22925
+ appId: mobile.appId,
22926
+ publisher,
22927
+ ...policy.minAgeMs === undefined ? {} : { minAgeMs: policy.minAgeMs },
22928
+ ...policy.retainRecent === undefined ? {} : { retainRecent: policy.retainRecent }
22929
+ });
22930
+ if (args.includes("--json"))
22931
+ console.log(JSON.stringify(report, null, 2));
22932
+ else
22933
+ printMobileUpdateStorage(report);
22934
+ sendTelemetryEvent("mobile:update-storage", {
22935
+ durationMs: Math.round(performance.now() - startedAt),
22936
+ reclaimableReleaseCount: report.releases.filter((release) => release.protectedBy.length === 0).length,
22937
+ releaseCount: report.releaseCount
22938
+ });
22939
+ return report;
22940
+ };
22941
+ var collectMobileUpdates = async (args) => {
22942
+ const startedAt = performance.now();
22943
+ const { mobile } = await loadMobile(valueAfter(args, "--config"));
22944
+ if (!mobile.updates)
22945
+ throw new TypeError("mobile update gc requires mobile.updates config.");
22946
+ const policy = mobileUpdateRetention(args);
22947
+ const gracePeriodMs = mobileUpdateDays(args, "--grace-days");
22948
+ const { publisher } = await mobileUpdatePublisher(args);
22949
+ const result = await pruneAbsoluteMobileUpdates({
22950
+ appId: mobile.appId,
22951
+ apply: args.includes("--apply"),
22952
+ publisher,
22953
+ ...gracePeriodMs === undefined ? {} : { gracePeriodMs },
22954
+ ...policy.minAgeMs === undefined ? {} : { minAgeMs: policy.minAgeMs },
22955
+ ...policy.retainRecent === undefined ? {} : { retainRecent: policy.retainRecent }
22956
+ });
22957
+ if (args.includes("--json"))
22958
+ console.log(JSON.stringify(result, null, 2));
22959
+ else {
22960
+ printMobileUpdateStorage(result);
22961
+ if (result.dryRun)
22962
+ console.log("No storage was changed. Re-run with --apply to mark eligible releases and sweep releases whose grace period has elapsed.");
22963
+ else
22964
+ console.log(`Collection applied: ${result.marked.length} marked, ${result.restored.length} restored, ${result.swept.length} swept, ${formatBytes(result.reclaimedBytes)} reclaimed.`);
22965
+ }
22966
+ sendTelemetryEvent("mobile:update-gc", {
22967
+ applied: !result.dryRun,
22968
+ durationMs: Math.round(performance.now() - startedAt),
22969
+ markedCount: result.marked.length,
22970
+ restoredCount: result.restored.length,
22971
+ sweptCount: result.swept.length
22972
+ });
22973
+ return result;
22974
+ };
22828
22975
  var requireValueAfter = (args, flag) => {
22829
22976
  const value = valueAfter(args, flag);
22830
22977
  if (!value || value.startsWith("-")) {
@@ -24165,6 +24312,14 @@ var runMobile = async (args) => {
24165
24312
  await rollbackMobileUpdate(args.slice(2));
24166
24313
  return;
24167
24314
  }
24315
+ if (command === "update" && args[1] === "storage") {
24316
+ await inspectMobileUpdateStorage(args.slice(2));
24317
+ return;
24318
+ }
24319
+ if (command === "update" && args[1] === "gc") {
24320
+ await collectMobileUpdates(args.slice(2));
24321
+ return;
24322
+ }
24168
24323
  if (command === "publish" && args[1] === "android") {
24169
24324
  await publishAndroid(args.slice(2));
24170
24325
  return;
@@ -24173,7 +24328,7 @@ var runMobile = async (args) => {
24173
24328
  await publishIos(args.slice(2));
24174
24329
  return;
24175
24330
  }
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]");
24331
+ 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
24332
  };
24178
24333
  export {
24179
24334
  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,8 @@ 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 contentBlobKey = (appId, sha256) => `${root(appId)}/blobs/${sha256}`;
12459
+ const tombstoneKey = (appId, releaseId) => `${root(appId)}/gc/${releaseId}.json`;
12458
12460
  const channelKey = (appId, channel) => {
12459
12461
  if (!APP_ID.test(appId) || !NAME.test(channel))
12460
12462
  throw new MobileUpdateRegistryError("Mobile update channel identity is invalid");
@@ -12483,6 +12485,12 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12483
12485
  throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
12484
12486
  return value;
12485
12487
  };
12488
+ const assertNotMarked = async (appId, releaseId) => {
12489
+ if (!APP_ID.test(appId) || !RELEASE.test(releaseId))
12490
+ throw new MobileUpdateRegistryError("Mobile update release identity is invalid");
12491
+ if (await options.store.head(tombstoneKey(appId, releaseId)))
12492
+ throw new MobileUpdateRegistryError("Mobile update release is marked for collection. Increase retention and apply garbage collection to restore it before promotion");
12493
+ };
12486
12494
  const writeChannel = async (input, signal) => {
12487
12495
  const value = {
12488
12496
  ...input,
@@ -12507,6 +12515,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12507
12515
  input.signal?.throwIfAborted();
12508
12516
  if (input.rollout <= 0 || input.rollout > 1)
12509
12517
  throw new MobileUpdateRegistryError("Mobile update rollout is invalid");
12518
+ await assertNotMarked(input.appId, input.releaseId);
12510
12519
  const release = await readManifest(input.appId, input.releaseId);
12511
12520
  if (!release || release.manifest.channel !== input.channel)
12512
12521
  throw new MobileUpdateRegistryError("Mobile update was not published to this channel");
@@ -12552,19 +12561,271 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12552
12561
  status: "selected"
12553
12562
  };
12554
12563
  };
12564
+ const retentionValues = (input) => {
12565
+ if (!APP_ID.test(input.appId))
12566
+ throw new MobileUpdateRegistryError("Mobile update appId is invalid");
12567
+ const retainRecent = input.retainRecent ?? DEFAULT_RETAIN_RECENT;
12568
+ const minAgeMs = input.minAgeMs ?? DEFAULT_MIN_AGE_MS;
12569
+ if (!Number.isSafeInteger(retainRecent) || retainRecent < 0)
12570
+ throw new MobileUpdateRegistryError("Mobile update retained release count is invalid");
12571
+ if (!Number.isSafeInteger(minAgeMs) || minAgeMs < 0)
12572
+ throw new MobileUpdateRegistryError("Mobile update minimum release age is invalid");
12573
+ return { minAgeMs, retainRecent };
12574
+ };
12575
+ const listObjects = async (appId, signal) => {
12576
+ const list = options.store.list;
12577
+ if (!list)
12578
+ throw new MobileUpdateRegistryError("Mobile update storage does not support lifecycle listing");
12579
+ const objects = [];
12580
+ const cursors = new Set;
12581
+ let cursor;
12582
+ do {
12583
+ signal?.throwIfAborted();
12584
+ const page = await list({
12585
+ ...cursor ? { cursor } : {},
12586
+ prefix: `${root(appId)}/`
12587
+ });
12588
+ objects.push(...page.objects);
12589
+ if (!page.truncated)
12590
+ break;
12591
+ if (!page.cursor || cursors.has(page.cursor))
12592
+ throw new MobileUpdateRegistryError("Mobile update storage returned an invalid lifecycle cursor");
12593
+ cursors.add(page.cursor);
12594
+ cursor = page.cursor;
12595
+ } while (true);
12596
+ return objects;
12597
+ };
12598
+ const inventory = async (input) => {
12599
+ const { minAgeMs, retainRecent } = retentionValues(input);
12600
+ const objects = await listObjects(input.appId, input.signal);
12601
+ const appRoot = `${root(input.appId)}/`;
12602
+ const releasePrefix = `${appRoot}releases/`;
12603
+ const channelPrefix = `${appRoot}channels/`;
12604
+ const markerPrefix = `${appRoot}gc/`;
12605
+ const blobPrefix = `${appRoot}blobs/`;
12606
+ const objectKeysByRelease = new Map;
12607
+ const objectBytesByRelease = new Map;
12608
+ const objectKeysByDigest = new Map;
12609
+ const objectBytesByDigest = new Map;
12610
+ for (const blobObject of objects) {
12611
+ if (blobObject.key.startsWith(blobPrefix)) {
12612
+ const sha256 = blobObject.key.slice(blobPrefix.length);
12613
+ if (HASH.test(sha256)) {
12614
+ objectKeysByDigest.set(sha256, blobObject.key);
12615
+ objectBytesByDigest.set(sha256, blobObject.size);
12616
+ }
12617
+ }
12618
+ if (!blobObject.key.startsWith(releasePrefix))
12619
+ continue;
12620
+ const suffix = blobObject.key.slice(releasePrefix.length);
12621
+ const releaseId = suffix.slice(0, suffix.indexOf("/"));
12622
+ if (!RELEASE.test(releaseId))
12623
+ continue;
12624
+ objectKeysByRelease.set(releaseId, [
12625
+ ...objectKeysByRelease.get(releaseId) ?? [],
12626
+ blobObject.key
12627
+ ]);
12628
+ objectBytesByRelease.set(releaseId, (objectBytesByRelease.get(releaseId) ?? 0) + blobObject.size);
12629
+ }
12630
+ const channels = [];
12631
+ for (const blobObject of objects) {
12632
+ if (!blobObject.key.startsWith(channelPrefix) || !blobObject.key.endsWith(".json"))
12633
+ continue;
12634
+ const bytes = await options.store.get(blobObject.key);
12635
+ if (!bytes)
12636
+ throw new MobileUpdateRegistryError("Mobile update channel disappeared during lifecycle inspection");
12637
+ const channel = parseChannel(decode(bytes));
12638
+ if (channel.appId !== input.appId)
12639
+ throw new MobileUpdateRegistryError("Stored mobile update channel identity changed");
12640
+ channels.push(channel);
12641
+ }
12642
+ const markedAt = new Map;
12643
+ for (const blobObject of objects) {
12644
+ if (!blobObject.key.startsWith(markerPrefix) || !blobObject.key.endsWith(".json"))
12645
+ continue;
12646
+ const releaseId = blobObject.key.slice(markerPrefix.length, -".json".length);
12647
+ if (!RELEASE.test(releaseId))
12648
+ continue;
12649
+ const bytes = await options.store.get(blobObject.key);
12650
+ if (!bytes)
12651
+ continue;
12652
+ const marker = decode(bytes);
12653
+ if (!object3(marker) || marker.format !== 1 || marker.appId !== input.appId || marker.releaseId !== releaseId || !iso(marker.markedAt))
12654
+ throw new MobileUpdateRegistryError("Mobile update collection marker is invalid");
12655
+ markedAt.set(releaseId, marker.markedAt);
12656
+ }
12657
+ const active = new Set(channels.flatMap((channel) => channel.releaseId ? [channel.releaseId] : []));
12658
+ const fallback = new Set(channels.flatMap((channel) => channel.fallbackReleaseId ? [channel.fallbackReleaseId] : []));
12659
+ const manifests = [];
12660
+ for (const releaseId of objectKeysByRelease.keys()) {
12661
+ const release = await readManifest(input.appId, releaseId);
12662
+ if (release)
12663
+ manifests.push(release.manifest);
12664
+ }
12665
+ const recent = new Set;
12666
+ const manifestsByChannel = new Map;
12667
+ for (const manifest of manifests)
12668
+ manifestsByChannel.set(manifest.channel, [
12669
+ ...manifestsByChannel.get(manifest.channel) ?? [],
12670
+ manifest
12671
+ ]);
12672
+ for (const releases2 of manifestsByChannel.values())
12673
+ for (const manifest of releases2.toSorted((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, retainRecent))
12674
+ recent.add(manifest.releaseId);
12675
+ const now = clock().getTime();
12676
+ const releases = manifests.map((manifest) => {
12677
+ const protectedBy = [];
12678
+ if (active.has(manifest.releaseId))
12679
+ protectedBy.push("active");
12680
+ if (fallback.has(manifest.releaseId))
12681
+ protectedBy.push("fallback");
12682
+ if (recent.has(manifest.releaseId))
12683
+ protectedBy.push("recent");
12684
+ if (now - Date.parse(manifest.createdAt) < minAgeMs)
12685
+ protectedBy.push("age");
12686
+ return {
12687
+ bytes: objectBytesByRelease.get(manifest.releaseId) ?? 0,
12688
+ channel: manifest.channel,
12689
+ createdAt: manifest.createdAt,
12690
+ ...markedAt.has(manifest.releaseId) ? { markedAt: markedAt.get(manifest.releaseId) } : {},
12691
+ objectCount: objectKeysByRelease.get(manifest.releaseId)?.length ?? 0,
12692
+ protectedBy,
12693
+ releaseId: manifest.releaseId
12694
+ };
12695
+ }).toSorted((left, right) => right.createdAt.localeCompare(left.createdAt));
12696
+ const protectedReleaseIds = new Set(releases.filter((release) => release.protectedBy.length > 0).map((release) => release.releaseId));
12697
+ const protectedDigests = new Set(manifests.filter((manifest) => protectedReleaseIds.has(manifest.releaseId)).flatMap((manifest) => manifest.files.map((file2) => file2.sha256)));
12698
+ const reclaimableContentBytes = [...objectBytesByDigest].filter(([sha256]) => !protectedDigests.has(sha256)).reduce((total, [, bytes]) => total + bytes, 0);
12699
+ const contentBlobBytes = [...objectBytesByDigest.values()].reduce((total, bytes) => total + bytes, 0);
12700
+ const releaseBytes = releases.reduce((total, release) => total + release.bytes, 0);
12701
+ const totalBytes = objects.reduce((total, object22) => total + object22.size, 0);
12702
+ return {
12703
+ objectKeysByRelease,
12704
+ objectKeysByDigest,
12705
+ report: {
12706
+ appId: input.appId,
12707
+ channelCount: channels.length,
12708
+ contentBlobBytes,
12709
+ contentBlobCount: objectKeysByDigest.size,
12710
+ reclaimableBytes: releases.filter((release) => release.protectedBy.length === 0).reduce((total, release) => total + release.bytes, 0) + reclaimableContentBytes,
12711
+ reclaimableContentBytes,
12712
+ releaseBytes,
12713
+ releaseCount: releases.length,
12714
+ releases,
12715
+ totalBytes,
12716
+ totalObjectCount: objects.length,
12717
+ untrackedBytes: totalBytes - releaseBytes - contentBlobBytes
12718
+ }
12719
+ };
12720
+ };
12721
+ const pruneUpdates = async (input) => {
12722
+ const initial = await inventory(input);
12723
+ const result = {
12724
+ ...initial.report,
12725
+ dryRun: input.apply !== true,
12726
+ marked: [],
12727
+ reclaimedBytes: 0,
12728
+ restored: [],
12729
+ swept: [],
12730
+ sweptContentBlobs: []
12731
+ };
12732
+ if (input.apply !== true)
12733
+ return result;
12734
+ const remove = options.store.delete;
12735
+ if (!remove)
12736
+ throw new MobileUpdateRegistryError("Mobile update storage does not support lifecycle deletion");
12737
+ const gracePeriodMs = input.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
12738
+ if (!Number.isSafeInteger(gracePeriodMs) || gracePeriodMs < 0)
12739
+ throw new MobileUpdateRegistryError("Mobile update collection grace period is invalid");
12740
+ const now = clock();
12741
+ for (const release of initial.report.releases) {
12742
+ input.signal?.throwIfAborted();
12743
+ const marker = tombstoneKey(input.appId, release.releaseId);
12744
+ if (release.protectedBy.length > 0) {
12745
+ if (release.markedAt) {
12746
+ await remove(marker);
12747
+ result.restored.push(release.releaseId);
12748
+ }
12749
+ continue;
12750
+ }
12751
+ if (!release.markedAt) {
12752
+ const bytes = json({
12753
+ appId: input.appId,
12754
+ format: 1,
12755
+ markedAt: now.toISOString(),
12756
+ releaseId: release.releaseId
12757
+ });
12758
+ await options.store.put(marker, bytes, {
12759
+ cacheControl: "no-cache",
12760
+ contentType: "application/json",
12761
+ maxBytes: bytes.byteLength,
12762
+ metadata: { releaseid: release.releaseId, sha256: digest(bytes) },
12763
+ signal: input.signal
12764
+ });
12765
+ result.marked.push(release.releaseId);
12766
+ }
12767
+ }
12768
+ const current = await inventory(input);
12769
+ for (const release of current.report.releases) {
12770
+ input.signal?.throwIfAborted();
12771
+ if (release.protectedBy.length > 0 || !release.markedAt || now.getTime() - Date.parse(release.markedAt) < gracePeriodMs || result.marked.includes(release.releaseId))
12772
+ continue;
12773
+ const checked = await inventory(input);
12774
+ const candidate = checked.report.releases.find((value) => value.releaseId === release.releaseId);
12775
+ if (!candidate || candidate.protectedBy.length > 0 || !candidate.markedAt)
12776
+ continue;
12777
+ const keys = checked.objectKeysByRelease.get(release.releaseId) ?? [];
12778
+ for (const key of keys.toSorted((left, right) => {
12779
+ const leftManifest = left.endsWith("/update.json");
12780
+ const rightManifest = right.endsWith("/update.json");
12781
+ return Number(leftManifest) - Number(rightManifest);
12782
+ }))
12783
+ await remove(key);
12784
+ await remove(tombstoneKey(input.appId, release.releaseId));
12785
+ result.reclaimedBytes += candidate.bytes;
12786
+ result.swept.push(release.releaseId);
12787
+ }
12788
+ const afterReleaseSweep = await inventory(input);
12789
+ const referencedDigests = new Set;
12790
+ for (const release of afterReleaseSweep.report.releases) {
12791
+ const manifest = await readManifest(input.appId, release.releaseId);
12792
+ for (const file2 of manifest?.manifest.files ?? [])
12793
+ referencedDigests.add(file2.sha256);
12794
+ }
12795
+ for (const [sha256, key] of afterReleaseSweep.objectKeysByDigest) {
12796
+ if (referencedDigests.has(sha256))
12797
+ continue;
12798
+ const head = await options.store.head(key);
12799
+ await remove(key);
12800
+ result.reclaimedBytes += head?.size ?? 0;
12801
+ result.sweptContentBlobs.push(sha256);
12802
+ }
12803
+ return result;
12804
+ };
12555
12805
  return {
12806
+ inspectUpdateStorage: async (input) => (await inventory(input)).report,
12807
+ pruneUpdates,
12556
12808
  publishUpdate: async (input) => {
12557
12809
  input.signal?.throwIfAborted();
12558
12810
  const manifest = parseMobileUpdateManifest(input.manifest);
12559
12811
  verifyManifestSignature(manifest, options.publicKeys);
12812
+ await assertNotMarked(manifest.appId, manifest.releaseId);
12560
12813
  const localRoot = path.resolve(input.releaseDirectory);
12561
12814
  const localManifest = parseMobileUpdateManifest(JSON.parse(await readFile4(path.join(localRoot, "update.json"), "utf8")));
12562
12815
  if (JSON.stringify(localManifest) !== JSON.stringify(manifest))
12563
12816
  throw new MobileUpdateRegistryError("Local mobile update manifest changed");
12564
12817
  const existing = await readManifest(manifest.appId, manifest.releaseId);
12565
12818
  let reused = existing !== null;
12819
+ let storedBytes = 0;
12820
+ let storedFiles = 0;
12821
+ let reusedBytes = 0;
12822
+ let reusedFiles = 0;
12566
12823
  if (existing && JSON.stringify(existing.manifest) !== JSON.stringify(manifest))
12567
12824
  throw new MobileUpdateRegistryError("Published mobile update is immutable");
12825
+ if (existing) {
12826
+ reusedBytes = manifest.files.reduce((total, file2) => total + file2.bytes, 0);
12827
+ reusedFiles = manifest.files.length;
12828
+ }
12568
12829
  if (!existing) {
12569
12830
  for (const file2 of manifest.files) {
12570
12831
  const local = path.join(localRoot, "files", file2.path);
@@ -12573,18 +12834,24 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12573
12834
  throw new MobileUpdateRegistryError(`Mobile update file ${file2.path} size changed`);
12574
12835
  if (await fileDigest(Bun.file(local)) !== file2.sha256)
12575
12836
  throw new MobileUpdateRegistryError(`Mobile update file ${file2.path} integrity failed`);
12576
- const key = fileKey(manifest, file2);
12837
+ const key = contentBlobKey(manifest.appId, file2.sha256);
12577
12838
  const stored = await options.store.head(key);
12578
12839
  if (!stored) {
12579
12840
  await options.store.put(key, Bun.file(local).stream(), {
12580
12841
  cacheControl: "public, max-age=31536000, immutable",
12581
12842
  contentType: "application/octet-stream",
12582
12843
  maxBytes: file2.bytes,
12583
- metadata: { releaseid: manifest.releaseId, sha256: file2.sha256 },
12844
+ metadata: { sha256: file2.sha256 },
12584
12845
  signal: input.signal
12585
12846
  });
12586
- } else if (stored.size !== file2.bytes || stored.metadata?.sha256 !== file2.sha256 || (stored.metadata?.releaseid ?? stored.metadata?.releaseId) !== manifest.releaseId)
12587
- throw new MobileUpdateRegistryError("Stored mobile update file identity changed");
12847
+ storedBytes += file2.bytes;
12848
+ storedFiles += 1;
12849
+ } else if (stored.size !== file2.bytes || stored.metadata?.sha256 !== file2.sha256)
12850
+ throw new MobileUpdateRegistryError("Stored mobile update content identity changed");
12851
+ else {
12852
+ reusedBytes += file2.bytes;
12853
+ reusedFiles += 1;
12854
+ }
12588
12855
  }
12589
12856
  const bytes = json(manifest);
12590
12857
  await options.store.put(manifestKey(manifest), bytes, {
@@ -12608,8 +12875,12 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12608
12875
  return {
12609
12876
  appId: manifest.appId,
12610
12877
  channel: manifest.channel,
12878
+ storedBytes,
12879
+ storedFiles,
12611
12880
  releaseId: manifest.releaseId,
12612
12881
  reused,
12882
+ reusedBytes,
12883
+ reusedFiles,
12613
12884
  rollout: input.rollout,
12614
12885
  stage: "published"
12615
12886
  };
@@ -12621,6 +12892,7 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12621
12892
  if (!existing)
12622
12893
  throw new MobileUpdateRegistryError("Mobile update channel does not exist");
12623
12894
  if (input.releaseId) {
12895
+ await assertNotMarked(input.appId, input.releaseId);
12624
12896
  const release = await readManifest(input.appId, input.releaseId);
12625
12897
  if (!release || release.manifest.channel !== input.channel)
12626
12898
  throw new MobileUpdateRegistryError("Mobile rollback release was not published");
@@ -12657,14 +12929,16 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12657
12929
  const file2 = release.manifest.files.find((candidate) => candidate.path === requested);
12658
12930
  if (!file2)
12659
12931
  return null;
12660
- const key = fileKey(release.manifest, file2);
12932
+ const legacyKey = fileKey(release.manifest, file2);
12933
+ const legacyHead = await options.store.head(legacyKey);
12934
+ const key = legacyHead ? legacyKey : contentBlobKey(release.manifest.appId, file2.sha256);
12661
12935
  const [bytes, head] = await Promise.all([
12662
12936
  options.store.get(key),
12663
- options.store.head(key)
12937
+ legacyHead ? Promise.resolve(legacyHead) : options.store.head(key)
12664
12938
  ]);
12665
12939
  if (!bytes || !head)
12666
12940
  return null;
12667
- if (bytes.byteLength !== file2.bytes || head.size !== file2.bytes || head.metadata?.sha256 !== file2.sha256 || (head.metadata?.releaseid ?? head.metadata?.releaseId) !== release.manifest.releaseId || digest(bytes) !== file2.sha256)
12941
+ if (bytes.byteLength !== file2.bytes || head.size !== file2.bytes || head.metadata?.sha256 !== file2.sha256 || legacyHead && (head.metadata?.releaseid ?? head.metadata?.releaseId) !== release.manifest.releaseId || digest(bytes) !== file2.sha256)
12668
12942
  throw new MobileUpdateRegistryError("Stored mobile update file integrity failed");
12669
12943
  return { bytes, file: file2 };
12670
12944
  }
@@ -12953,6 +13227,9 @@ var MOBILE_UPDATE_REGISTRY_FORMAT = 1, DEFAULT_PREFIX = "absolutejs/mobile-updat
12953
13227
  var init_mobileUpdate = __esm(() => {
12954
13228
  MAX_FILE_BYTES = 32 * 1024 * 1024;
12955
13229
  MAX_TOTAL_BYTES = 128 * 1024 * 1024;
13230
+ DAY_MS = 24 * 60 * 60 * 1000;
13231
+ DEFAULT_MIN_AGE_MS = 30 * DAY_MS;
13232
+ DEFAULT_GRACE_PERIOD_MS = 7 * DAY_MS;
12956
13233
  HASH = /^[a-f0-9]{64}$/;
12957
13234
  RELEASE = /^amu_[a-f0-9]{64}$/;
12958
13235
  APP_ID = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
@@ -44587,5 +44864,5 @@ export {
44587
44864
  wrapPageHandlerWithStreamingSlots
44588
44865
  };
44589
44866
 
44590
- //# debugId=D254EAF4F11DFBA864756E2164756E21
44867
+ //# debugId=CE82990F0B81B09764756E2164756E21
44591
44868
  //# sourceMappingURL=index.js.map