@hot-updater/aws 0.31.4 → 0.33.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.
@@ -6,7 +6,7 @@ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames$1 = Object.getOwnPropertyNames;
7
7
  var __getProtoOf$1 = Object.getPrototypeOf;
8
8
  var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
9
- var __commonJSMin$1 = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
+ var __commonJSMin$1 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
10
10
  var __copyProps$1 = (to, from, except, desc) => {
11
11
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames$1(from), i = 0, n = keys.length, key; i < n; i++) {
12
12
  key = keys[i];
@@ -23,16 +23,16 @@ var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(
23
23
  }) : target, mod));
24
24
  //#endregion
25
25
  let node_path = require("node:path");
26
- node_path = __toESM$1(node_path, 1);
26
+ node_path = __toESM$1(node_path);
27
27
  let node_crypto = require("node:crypto");
28
- node_crypto = __toESM$1(node_crypto, 1);
28
+ node_crypto = __toESM$1(node_crypto);
29
29
  let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
30
30
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
31
31
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
32
32
  let fs_promises = require("fs/promises");
33
- fs_promises = __toESM$1(fs_promises, 1);
33
+ fs_promises = __toESM$1(fs_promises);
34
34
  let path = require("path");
35
- path = __toESM$1(path, 1);
35
+ path = __toESM$1(path);
36
36
  let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
37
37
  let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
38
38
  let _aws_sdk_cloudfront_signer = require("@aws-sdk/cloudfront-signer");
@@ -1440,6 +1440,49 @@ function getContentType(bundlePath) {
1440
1440
  return src_default.getType(bundlePath) ?? getCompressionMimeType(filename) ?? "application/octet-stream";
1441
1441
  }
1442
1442
  //#endregion
1443
+ //#region ../plugin-core/dist/contentAddressedAssets.mjs
1444
+ const getContentAddressedAssetStoragePath = ({ assetPath, fileHash }) => {
1445
+ const extension = assetPath.endsWith(".br") ? ".br" : assetPath.includes(".") ? `.${assetPath.split(".").pop()}` : "";
1446
+ return `sha256/${fileHash.slice(0, 2)}/${fileHash}${extension}`;
1447
+ };
1448
+ //#endregion
1449
+ //#region ../plugin-core/dist/legacyAssetStorageLayout.mjs
1450
+ /**
1451
+ * @internal
1452
+ *
1453
+ * Legacy manifest assets were stored below each bundle's `/files` directory
1454
+ * using their manifest-relative path. Keep all old-layout path decisions here
1455
+ * so support can be removed by deleting this module and the entrypoint branch
1456
+ * that imports it.
1457
+ */
1458
+ const getLegacyManifestAssetStoragePath = ({ assetPath }) => assetPath;
1459
+ //#endregion
1460
+ //#region ../plugin-core/dist/assetStorageLayout.mjs
1461
+ const createStorageUriWithRelativePath = ({ baseStorageUri, relativePath }) => {
1462
+ const storageUrl = new URL(baseStorageUri);
1463
+ storageUrl.pathname = `${storageUrl.pathname.replace(/\/+$/, "")}/${relativePath.replace(/\\/g, "/").split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
1464
+ return storageUrl.toString();
1465
+ };
1466
+ const getAssetStorageLayout = (assetBaseStorageUri) => {
1467
+ const pathname = new URL(assetBaseStorageUri).pathname.replace(/\/+$/, "");
1468
+ return pathname.endsWith("/assets") || pathname === "/assets" ? "content-addressed" : "legacy-files";
1469
+ };
1470
+ const getManifestAssetStoragePath = ({ assetBaseStorageUri, assetPath, fileHash }) => {
1471
+ if (getAssetStorageLayout(assetBaseStorageUri) === "content-addressed") return getContentAddressedAssetStoragePath({
1472
+ assetPath,
1473
+ fileHash
1474
+ });
1475
+ return getLegacyManifestAssetStoragePath({ assetPath });
1476
+ };
1477
+ const resolveManifestAssetStorageUri = ({ assetBaseStorageUri, assetPath, fileHash }) => createStorageUriWithRelativePath({
1478
+ baseStorageUri: assetBaseStorageUri,
1479
+ relativePath: getManifestAssetStoragePath({
1480
+ assetBaseStorageUri,
1481
+ assetPath,
1482
+ fileHash
1483
+ })
1484
+ });
1485
+ //#endregion
1443
1486
  //#region ../../node_modules/.pnpm/es-toolkit@1.32.0/node_modules/es-toolkit/dist/_internal/compareValues.mjs
1444
1487
  function compareValues(a, b, order) {
1445
1488
  if (a < b) return order === "asc" ? -1 : 1;
@@ -2795,7 +2838,6 @@ var require_min_version$2 = /* @__PURE__ */ __commonJSMin$1(((exports, module) =
2795
2838
  break;
2796
2839
  case "<":
2797
2840
  case "<=": break;
2798
- /* istanbul ignore next */
2799
2841
  default: throw new Error(`Unexpected operation: ${comparator.operator}`);
2800
2842
  }
2801
2843
  });
@@ -3183,6 +3225,24 @@ function paginateBundles({ bundles, limit, offset, cursor, orderBy }) {
3183
3225
  };
3184
3226
  }
3185
3227
  //#endregion
3228
+ //#region ../plugin-core/dist/requestUpdateBundleState.mjs
3229
+ const requestUpdateBundleSeeds = /* @__PURE__ */ new WeakMap();
3230
+ const isWeakMapKey = (value) => typeof value === "object" && value !== null || typeof value === "function";
3231
+ const toBundleSeeds = (seeds) => seeds.filter((seed) => !!seed);
3232
+ const seedRequestUpdateBundles = (context, seeds) => {
3233
+ if (!isWeakMapKey(context)) return;
3234
+ const nextSeeds = toBundleSeeds(seeds);
3235
+ if (nextSeeds.length === 0) return;
3236
+ const bundlesById = /* @__PURE__ */ new Map();
3237
+ for (const seed of requestUpdateBundleSeeds.get(context) ?? []) bundlesById.set(seed.id, seed);
3238
+ for (const seed of nextSeeds) bundlesById.set(seed.id, seed);
3239
+ requestUpdateBundleSeeds.set(context, [...bundlesById.values()]);
3240
+ };
3241
+ const getRequestUpdateBundleSeeds = (context) => {
3242
+ if (!isWeakMapKey(context)) return [];
3243
+ return requestUpdateBundleSeeds.get(context) ?? [];
3244
+ };
3245
+ //#endregion
3186
3246
  //#region ../../packages/core/dist/index.mjs
3187
3247
  const getManifestStorageUri = (bundle) => bundle.manifestStorageUri ?? null;
3188
3248
  const getManifestFileHash = (bundle) => bundle.manifestFileHash ?? null;
@@ -3319,7 +3379,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3319
3379
  var __getOwnPropNames = Object.getOwnPropertyNames;
3320
3380
  var __getProtoOf = Object.getPrototypeOf;
3321
3381
  var __hasOwnProp = Object.prototype.hasOwnProperty;
3322
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
3382
+ var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
3323
3383
  var __copyProps = (to, from, except, desc) => {
3324
3384
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
3325
3385
  key = keys[i];
@@ -4308,7 +4368,6 @@ var require_min_version$1 = /* @__PURE__ */ __commonJSMin(((exports, module) =>
4308
4368
  break;
4309
4369
  case "<":
4310
4370
  case "<=": break;
4311
- /* istanbul ignore next */
4312
4371
  default: throw new Error(`Unexpected operation: ${comparator.operator}`);
4313
4372
  }
4314
4373
  });
@@ -4678,7 +4737,34 @@ const day = 3600 * 24;
4678
4737
  day * 7;
4679
4738
  day * 365.25;
4680
4739
  //#endregion
4740
+ //#region ../plugin-core/dist/resolveUpdateInfoFromBundles.mjs
4741
+ const findSeedBundle = (bundles, bundleId) => bundles.find((bundle) => bundle.id === bundleId);
4742
+ const resolveUpdateInfoFromBundles = async ({ args, bundles, context }) => {
4743
+ const info = await getUpdateInfo(bundles, args);
4744
+ if (!info) return null;
4745
+ seedRequestUpdateBundles(context, [findSeedBundle(bundles, info.id), args.bundleId === "00000000-0000-0000-0000-000000000000" ? null : findSeedBundle(bundles, args.bundleId)]);
4746
+ return info;
4747
+ };
4748
+ //#endregion
4681
4749
  //#region ../plugin-core/dist/createBlobDatabasePlugin.mjs
4750
+ const STORAGE_OPERATION_CONCURRENCY = 8;
4751
+ async function mapWithConcurrency(items, concurrency, mapper) {
4752
+ const results = [];
4753
+ let nextIndex = 0;
4754
+ const workerCount = Math.min(concurrency, items.length);
4755
+ await Promise.all(Array.from({ length: workerCount }, async () => {
4756
+ while (true) {
4757
+ const index = nextIndex;
4758
+ nextIndex += 1;
4759
+ if (index >= items.length) break;
4760
+ results[index] = await mapper(items[index], index);
4761
+ }
4762
+ }));
4763
+ return results;
4764
+ }
4765
+ async function forEachWithConcurrency(items, concurrency, mapper) {
4766
+ await mapWithConcurrency(items, concurrency, mapper);
4767
+ }
4682
4768
  function removeBundleInternalKeys(bundle) {
4683
4769
  const { _updateJsonKey, _oldUpdateJsonKey, ...pureBundle } = bundle;
4684
4770
  return pureBundle;
@@ -4727,6 +4813,12 @@ const MANAGEMENT_INDEX_PREFIX = "_index";
4727
4813
  const MANAGEMENT_INDEX_VERSION = 1;
4728
4814
  const DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE = 128;
4729
4815
  const ALL_SCOPE_CACHE_KEY = "*|*";
4816
+ function summarizeManagementIndexArtifacts(artifacts) {
4817
+ return {
4818
+ pagesWritten: artifacts.pages.size,
4819
+ scopesWritten: artifacts.scopes.length
4820
+ };
4821
+ }
4730
4822
  function resolveManagementIndexPageSize(config) {
4731
4823
  const pageSize = config.managementIndexPageSize ?? DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE;
4732
4824
  if (!Number.isInteger(pageSize) || pageSize < 1) throw new Error("managementIndexPageSize must be a positive integer.");
@@ -4858,11 +4950,18 @@ function buildManagementIndexArtifacts(allBundles, pageSize) {
4858
4950
  const createBlobDatabasePlugin = ({ name, factory }) => {
4859
4951
  return (config, hooks) => {
4860
4952
  const managementIndexPageSize = resolveManagementIndexPageSize(config);
4861
- const { listObjects, loadObject, uploadObject, deleteObject, invalidatePaths, apiBasePath } = factory(config);
4953
+ const { listObjects, loadObject, uploadObject, deleteObject, shouldSkipLoadObjectError, invalidatePaths, apiBasePath } = factory(config);
4862
4954
  const bundlesMap = /* @__PURE__ */ new Map();
4863
4955
  const pendingBundlesMap = /* @__PURE__ */ new Map();
4864
4956
  const managementRootCache = /* @__PURE__ */ new Map();
4865
- const PLATFORMS = ["ios", "android"];
4957
+ const loadOptionalObject = async (key) => {
4958
+ try {
4959
+ return await loadObject(key);
4960
+ } catch (error) {
4961
+ if (shouldSkipLoadObjectError?.(error, key)) return null;
4962
+ throw error;
4963
+ }
4964
+ };
4866
4965
  const getAllManagementArtifact = (artifacts) => {
4867
4966
  const allArtifact = artifacts.scopes.find((scope) => scope.cacheKey === ALL_SCOPE_CACHE_KEY);
4868
4967
  if (!allArtifact) throw new Error("all-bundles management index artifact not found");
@@ -4879,7 +4978,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4879
4978
  };
4880
4979
  const loadStoredManagementRoot = async (scope) => {
4881
4980
  const cacheKey = getManagementScopeCacheKey(scope);
4882
- const storedRoot = await loadObject(getManagementRootKey(scope));
4981
+ const storedRoot = await loadOptionalObject(getManagementRootKey(scope));
4883
4982
  if (storedRoot) {
4884
4983
  managementRootCache.set(cacheKey, storedRoot);
4885
4984
  return storedRoot;
@@ -4889,7 +4988,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4889
4988
  };
4890
4989
  const loadManagementPage = async (descriptor, pageCache) => {
4891
4990
  if (pageCache?.has(descriptor.key)) return pageCache.get(descriptor.key) ?? null;
4892
- const page = await loadObject(descriptor.key);
4991
+ const page = await loadOptionalObject(descriptor.key);
4893
4992
  pageCache?.set(descriptor.key, page);
4894
4993
  return page;
4895
4994
  };
@@ -4912,13 +5011,13 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4912
5011
  return allBundles;
4913
5012
  };
4914
5013
  const persistManagementIndexArtifacts = async (nextArtifacts, previousArtifacts) => {
4915
- for (const [key, page] of nextArtifacts.pages.entries()) await uploadObject(key, page);
4916
- for (const scope of nextArtifacts.scopes) await uploadObject(scope.rootKey, scope.root);
5014
+ await forEachWithConcurrency(Array.from(nextArtifacts.pages.entries()), STORAGE_OPERATION_CONCURRENCY, ([key, page]) => uploadObject(key, page));
5015
+ await forEachWithConcurrency(nextArtifacts.scopes, STORAGE_OPERATION_CONCURRENCY, (scope) => uploadObject(scope.rootKey, scope.root));
4917
5016
  if (!previousArtifacts) return;
4918
5017
  const nextPageKeys = new Set(nextArtifacts.pages.keys());
4919
5018
  const nextRootKeys = new Set(nextArtifacts.scopes.map((scope) => scope.rootKey));
4920
- for (const [key] of previousArtifacts.pages.entries()) if (!nextPageKeys.has(key)) await deleteObject(key).catch(() => {});
4921
- for (const scope of previousArtifacts.scopes) if (!nextRootKeys.has(scope.rootKey)) await deleteObject(scope.rootKey).catch(() => {});
5019
+ await forEachWithConcurrency(Array.from(previousArtifacts.pages.keys()).filter((key) => !nextPageKeys.has(key)), STORAGE_OPERATION_CONCURRENCY, (key) => deleteObject(key).catch(() => {}));
5020
+ await forEachWithConcurrency(previousArtifacts.scopes.filter((scope) => !nextRootKeys.has(scope.rootKey)), STORAGE_OPERATION_CONCURRENCY, (scope) => deleteObject(scope.rootKey).catch(() => {}));
4922
5021
  };
4923
5022
  const ensureAllManagementRoot = async () => {
4924
5023
  const storedAllRoot = await loadStoredManagementRoot({});
@@ -4951,6 +5050,26 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4951
5050
  const loadCurrentBundlesForIndexRebuild = async () => {
4952
5051
  return loadAllBundlesForManagementFallback();
4953
5052
  };
5053
+ const loadBundlesFromCanonicalManifests = async () => {
5054
+ return sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
5055
+ };
5056
+ const loadStoredBundlesForIndexRebuild = loadBundlesFromCanonicalManifests;
5057
+ const loadCanonicalBundlesForIndexRepair = loadBundlesFromCanonicalManifests;
5058
+ const compareBundleIndex = ({ canonicalBundles, indexedBundles, rootMissing }) => {
5059
+ const canonicalIds = new Set(canonicalBundles.map((bundle) => bundle.id));
5060
+ const indexedIds = new Set(indexedBundles?.map((bundle) => bundle.id) ?? []);
5061
+ const missingBundleIds = Array.from(canonicalIds).filter((id) => !indexedIds.has(id)).sort((left, right) => right.localeCompare(left));
5062
+ const extraBundleIds = Array.from(indexedIds).filter((id) => !canonicalIds.has(id)).sort((left, right) => right.localeCompare(left));
5063
+ return {
5064
+ status: missingBundleIds.length === 0 && extraBundleIds.length === 0 && !rootMissing ? "ok" : rootMissing ? "missing" : "stale",
5065
+ canonicalBundles: canonicalBundles.length,
5066
+ indexedBundles: indexedBundles?.length ?? 0,
5067
+ missingBundles: missingBundleIds.length,
5068
+ extraBundles: extraBundleIds.length,
5069
+ missingBundleIds: missingBundleIds.slice(0, 20),
5070
+ extraBundleIds: extraBundleIds.slice(0, 20)
5071
+ };
5072
+ };
4954
5073
  const findPageIndexContainingId = (pages, id) => {
4955
5074
  return pages.findIndex((page) => id.localeCompare(page.firstId) <= 0 && id.localeCompare(page.lastId) >= 0);
4956
5075
  };
@@ -5126,16 +5245,15 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5126
5245
  };
5127
5246
  async function reloadBundles() {
5128
5247
  bundlesMap.clear();
5129
- const filePromises = (await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)).map(async (key) => {
5130
- return (await loadObject(key) ?? []).map((bundle) => ({
5248
+ const allBundles = (await mapWithConcurrency((await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)), STORAGE_OPERATION_CONCURRENCY, async (key) => {
5249
+ return (await loadOptionalObject(key) ?? []).map((bundle) => ({
5131
5250
  ...bundle,
5132
5251
  _updateJsonKey: key
5133
5252
  }));
5134
- });
5135
- const allBundles = (await Promise.all(filePromises)).flat();
5253
+ })).flat();
5136
5254
  for (const bundle of allBundles) bundlesMap.set(bundle.id, bundle);
5137
5255
  for (const [id, bundle] of pendingBundlesMap.entries()) bundlesMap.set(id, bundle);
5138
- return orderBy(allBundles, [(v) => v.id], ["desc"]);
5256
+ return orderBy(Array.from(bundlesMap.values()), [(v) => v.id], ["desc"]);
5139
5257
  }
5140
5258
  /**
5141
5259
  * Updates target-app-versions.json for each channel on the given platform.
@@ -5161,35 +5279,44 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5161
5279
  const updateKeys = keysByChannel[channel];
5162
5280
  const targetKey = `${channel}/${platform}/target-app-versions.json`;
5163
5281
  const currentVersions = updateKeys.map((key) => key.split("/")[2]);
5164
- const oldTargetVersions = await loadObject(targetKey) ?? [];
5282
+ const oldTargetVersions = await loadOptionalObject(targetKey) ?? [];
5165
5283
  const newTargetVersions = oldTargetVersions.filter((v) => currentVersions.includes(v));
5166
5284
  for (const v of currentVersions) if (!newTargetVersions.includes(v)) newTargetVersions.push(v);
5167
5285
  if (JSON.stringify(oldTargetVersions) !== JSON.stringify(newTargetVersions)) await uploadObject(targetKey, newTargetVersions);
5168
5286
  }
5169
5287
  }
5170
- const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }) => {
5171
- const matchingVersions = filterCompatibleAppVersions(await loadObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion);
5172
- return getUpdateInfo((await Promise.allSettled(matchingVersions.map(async (targetAppVersion) => {
5173
- return await loadObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`) ?? [];
5174
- }))).filter((entry) => entry.status === "fulfilled").flatMap((entry) => entry.value), {
5175
- _updateStrategy: "appVersion",
5176
- appVersion,
5177
- bundleId,
5178
- channel,
5179
- cohort,
5180
- minBundleId,
5181
- platform
5288
+ const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }, context) => {
5289
+ const bundles = (await mapWithConcurrency(filterCompatibleAppVersions(await loadOptionalObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion), STORAGE_OPERATION_CONCURRENCY, async (targetAppVersion) => {
5290
+ return await loadOptionalObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`) ?? [];
5291
+ })).flat();
5292
+ return resolveUpdateInfoFromBundles({
5293
+ args: {
5294
+ _updateStrategy: "appVersion",
5295
+ appVersion,
5296
+ bundleId,
5297
+ channel,
5298
+ cohort,
5299
+ minBundleId,
5300
+ platform
5301
+ },
5302
+ bundles,
5303
+ context
5182
5304
  });
5183
5305
  };
5184
- const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }) => {
5185
- return getUpdateInfo(await loadObject(`${channel}/${platform}/${fingerprintHash}/update.json`) ?? [], {
5186
- _updateStrategy: "fingerprint",
5187
- bundleId,
5188
- channel,
5189
- cohort,
5190
- fingerprintHash,
5191
- minBundleId,
5192
- platform
5306
+ const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }, context) => {
5307
+ const bundles = await loadOptionalObject(`${channel}/${platform}/${fingerprintHash}/update.json`) ?? [];
5308
+ return resolveUpdateInfoFromBundles({
5309
+ args: {
5310
+ _updateStrategy: "fingerprint",
5311
+ bundleId,
5312
+ channel,
5313
+ cohort,
5314
+ fingerprintHash,
5315
+ minBundleId,
5316
+ platform
5317
+ },
5318
+ bundles,
5319
+ context
5193
5320
  });
5194
5321
  };
5195
5322
  const addAppVersionInvalidationPaths = (pathsToInvalidate, { platform, channel, targetAppVersion }) => {
@@ -5211,7 +5338,35 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5211
5338
  targetAppVersion
5212
5339
  });
5213
5340
  };
5214
- return createDatabasePlugin({
5341
+ const bundleIndexDiagnostics = {
5342
+ async check() {
5343
+ const canonicalBundles = await loadCanonicalBundlesForIndexRepair();
5344
+ const allRoot = await loadStoredManagementRoot({});
5345
+ return compareBundleIndex({
5346
+ canonicalBundles,
5347
+ indexedBundles: allRoot ? await loadAllBundlesFromRoot(allRoot) : null,
5348
+ rootMissing: !allRoot
5349
+ });
5350
+ },
5351
+ async repair() {
5352
+ const canonicalBundles = await loadCanonicalBundlesForIndexRepair();
5353
+ const previousRoot = await loadStoredManagementRoot({});
5354
+ const previousBundles = previousRoot ? await loadAllBundlesFromRoot(previousRoot) : null;
5355
+ const previousArtifacts = previousRoot && previousBundles ? buildManagementIndexArtifacts(previousBundles, previousRoot.pageSize) : void 0;
5356
+ const nextArtifacts = buildManagementIndexArtifacts(canonicalBundles, managementIndexPageSize);
5357
+ const indexedObjectKeys = await listObjects(`${MANAGEMENT_INDEX_PREFIX}/`);
5358
+ const nextObjectKeys = new Set([...nextArtifacts.pages.keys(), ...nextArtifacts.scopes.map((scope) => scope.rootKey)]);
5359
+ await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
5360
+ await forEachWithConcurrency(indexedObjectKeys.filter((key) => !nextObjectKeys.has(key)), STORAGE_OPERATION_CONCURRENCY, (key) => deleteObject(key).catch(() => {}));
5361
+ replaceManagementRootCache(nextArtifacts);
5362
+ return {
5363
+ scannedBundles: canonicalBundles.length,
5364
+ indexedBundles: canonicalBundles.length,
5365
+ ...summarizeManagementIndexArtifacts(nextArtifacts)
5366
+ };
5367
+ }
5368
+ };
5369
+ const createPlugin = createDatabasePlugin({
5215
5370
  name,
5216
5371
  factory: () => ({
5217
5372
  supportsCursorPagination: true,
@@ -5235,9 +5390,9 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5235
5390
  if (!matchedBundle) return null;
5236
5391
  return removeBundleInternalKeys(matchedBundle);
5237
5392
  },
5238
- async getUpdateInfo(args) {
5239
- if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args);
5240
- return getFingerprintUpdateInfo(args);
5393
+ async getUpdateInfo(args, context) {
5394
+ if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args, context);
5395
+ return getFingerprintUpdateInfo(args, context);
5241
5396
  },
5242
5397
  async getBundles(options) {
5243
5398
  const { where, limit, offset, orderBy, cursor } = options;
@@ -5270,11 +5425,8 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5270
5425
  const changedBundlesByKey = {};
5271
5426
  const removalsByKey = {};
5272
5427
  const pathsToInvalidate = /* @__PURE__ */ new Set();
5273
- let isTargetAppVersionChanged = false;
5274
- let isChannelChanged = false;
5428
+ const targetVersionPlatforms = /* @__PURE__ */ new Set();
5275
5429
  for (const { operation, data } of changedSets) {
5276
- if (data.targetAppVersion !== void 0) isTargetAppVersionChanged = true;
5277
- if (operation === "update" && data.channel !== void 0) isChannelChanged = true;
5278
5430
  if (operation === "insert") {
5279
5431
  const target = resolveStorageTarget(data);
5280
5432
  const key = `${data.channel}/${data.platform}/${target}/update.json`;
@@ -5286,6 +5438,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5286
5438
  pendingBundlesMap.set(data.id, bundleWithKey);
5287
5439
  changedBundlesByKey[key] = changedBundlesByKey[key] || [];
5288
5440
  changedBundlesByKey[key].push(removeBundleInternalKeys(bundleWithKey));
5441
+ if (data.targetAppVersion !== void 0) targetVersionPlatforms.add(data.platform);
5289
5442
  addLookupInvalidationPaths(pathsToInvalidate, data);
5290
5443
  continue;
5291
5444
  }
@@ -5298,6 +5451,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5298
5451
  const key = bundle._updateJsonKey;
5299
5452
  removalsByKey[key] = removalsByKey[key] || [];
5300
5453
  removalsByKey[key].push(bundle.id);
5454
+ if (bundle.targetAppVersion !== void 0) targetVersionPlatforms.add(bundle.platform);
5301
5455
  addLookupInvalidationPaths(pathsToInvalidate, bundle);
5302
5456
  continue;
5303
5457
  }
@@ -5329,6 +5483,10 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5329
5483
  channel: nextChannel
5330
5484
  });
5331
5485
  }
5486
+ if (bundle.targetAppVersion !== void 0 || updatedBundle.targetAppVersion !== void 0) {
5487
+ targetVersionPlatforms.add(bundle.platform);
5488
+ targetVersionPlatforms.add(updatedBundle.platform);
5489
+ }
5332
5490
  addLookupInvalidationPaths(pathsToInvalidate, updatedBundle);
5333
5491
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
5334
5492
  continue;
@@ -5342,14 +5500,14 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5342
5500
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
5343
5501
  }
5344
5502
  }
5345
- for (const oldKey of Object.keys(removalsByKey)) await (async () => {
5346
- const updatedBundles = (await loadObject(oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
5503
+ await forEachWithConcurrency(Object.keys(removalsByKey), STORAGE_OPERATION_CONCURRENCY, async (oldKey) => {
5504
+ const updatedBundles = (await loadOptionalObject(oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
5347
5505
  updatedBundles.sort((a, b) => b.id.localeCompare(a.id));
5348
5506
  if (updatedBundles.length === 0) await deleteObject(oldKey);
5349
5507
  else await uploadObject(oldKey, updatedBundles);
5350
- })();
5351
- for (const key of Object.keys(changedBundlesByKey)) await (async () => {
5352
- const currentBundles = await loadObject(key) ?? [];
5508
+ });
5509
+ await forEachWithConcurrency(Object.keys(changedBundlesByKey), STORAGE_OPERATION_CONCURRENCY, async (key) => {
5510
+ const currentBundles = await loadOptionalObject(key) ?? [];
5353
5511
  const pureBundles = changedBundlesByKey[key].map((bundle) => bundle);
5354
5512
  for (const changedBundle of pureBundles) {
5355
5513
  const index = currentBundles.findIndex((b) => b.id === changedBundle.id);
@@ -5358,10 +5516,11 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5358
5516
  }
5359
5517
  currentBundles.sort((a, b) => b.id.localeCompare(a.id));
5360
5518
  await uploadObject(key, currentBundles);
5361
- })();
5362
- if (isTargetAppVersionChanged || isChannelChanged) for (const platform of PLATFORMS) await updateTargetVersionsForPlatform(platform);
5363
- const currentIndexBundles = await loadCurrentBundlesForIndexRebuild();
5364
- const nextIndexMap = new Map(currentIndexBundles.map((bundle) => [bundle.id, bundle]));
5519
+ });
5520
+ if (targetVersionPlatforms.size > 0) await Promise.all(Array.from(targetVersionPlatforms).map((platform) => updateTargetVersionsForPlatform(platform)));
5521
+ const previousIndexBundles = await loadCurrentBundlesForIndexRebuild();
5522
+ const storedIndexBundles = await loadStoredBundlesForIndexRebuild();
5523
+ const nextIndexMap = new Map(storedIndexBundles.map((bundle) => [bundle.id, bundle]));
5365
5524
  for (const { operation, data } of changedSets) {
5366
5525
  if (operation === "delete") {
5367
5526
  nextIndexMap.delete(data.id);
@@ -5370,7 +5529,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5370
5529
  nextIndexMap.set(data.id, data);
5371
5530
  }
5372
5531
  const nextIndexBundles = sortManagedBundles(Array.from(nextIndexMap.values()));
5373
- const previousArtifacts = buildManagementIndexArtifacts(currentIndexBundles, managementIndexPageSize);
5532
+ const previousArtifacts = buildManagementIndexArtifacts(previousIndexBundles, managementIndexPageSize);
5374
5533
  const nextArtifacts = buildManagementIndexArtifacts(nextIndexBundles, managementIndexPageSize);
5375
5534
  await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
5376
5535
  replaceManagementRootCache(nextArtifacts);
@@ -5381,6 +5540,15 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5381
5540
  }
5382
5541
  })
5383
5542
  })({}, hooks);
5543
+ return () => {
5544
+ const plugin = createPlugin();
5545
+ Object.defineProperty(plugin, "diagnostics", {
5546
+ configurable: true,
5547
+ enumerable: true,
5548
+ value: { bundleIndex: bundleIndexDiagnostics }
5549
+ });
5550
+ return plugin;
5551
+ };
5384
5552
  };
5385
5553
  };
5386
5554
  //#endregion
@@ -5436,6 +5604,9 @@ const createProfiledStoragePlugin = ({ createProfiles, name, profileShape, suppo
5436
5604
  async downloadFile(storageUri, filePath) {
5437
5605
  return requireNodeProfile().downloadFile(storageUri, filePath);
5438
5606
  },
5607
+ async exists(storageUri) {
5608
+ return requireNodeProfile().exists(storageUri);
5609
+ },
5439
5610
  async upload(key, filePath) {
5440
5611
  return requireNodeProfile().upload(key, filePath);
5441
5612
  }
@@ -5519,6 +5690,34 @@ function assertRuntimeStoragePlugin(plugin) {
5519
5690
  if (!isRuntimeStoragePlugin(plugin)) throw createMissingProfileError(plugin, "runtime");
5520
5691
  }
5521
5692
  //#endregion
5693
+ //#region ../../packages/server/src/db/requestBundleIdentityMap.ts
5694
+ const createRequestBundleIdentityMap = ({ context, loadBundleById, seeds }) => {
5695
+ const bundles = /* @__PURE__ */ new Map();
5696
+ const pendingBundles = /* @__PURE__ */ new Map();
5697
+ for (const seed of seeds) if (seed) bundles.set(seed.id, seed);
5698
+ const get = async (bundleId) => {
5699
+ const cachedBundle = bundles.get(bundleId);
5700
+ if (cachedBundle) return cachedBundle;
5701
+ const pendingBundle = pendingBundles.get(bundleId);
5702
+ if (pendingBundle) return pendingBundle;
5703
+ const lookup = loadBundleById(bundleId, context).then((bundle) => {
5704
+ pendingBundles.delete(bundleId);
5705
+ if (bundle) bundles.set(bundle.id, bundle);
5706
+ return bundle;
5707
+ }, (error) => {
5708
+ pendingBundles.delete(bundleId);
5709
+ throw error;
5710
+ });
5711
+ pendingBundles.set(bundleId, lookup);
5712
+ return lookup;
5713
+ };
5714
+ const peek = (bundleId) => bundles.get(bundleId) ?? null;
5715
+ return {
5716
+ get,
5717
+ peek
5718
+ };
5719
+ };
5720
+ //#endregion
5522
5721
  //#region ../../packages/server/src/db/schemaEnhancements.ts
5523
5722
  const normalizeNullableString = (value) => {
5524
5723
  if (value === null || value === void 0) return null;
@@ -5554,11 +5753,6 @@ const isBundleManifest = (value) => {
5554
5753
  return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
5555
5754
  });
5556
5755
  };
5557
- const createChildStorageUri = (baseStorageUri, relativePath) => {
5558
- const baseUrl = new URL(baseStorageUri);
5559
- baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, "")}/${relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
5560
- return baseUrl.toString();
5561
- };
5562
5756
  async function fetchBundleManifest(storageUri, readStorageText, resolveFileUrl, context) {
5563
5757
  const [storageText, fileUrl] = await Promise.all([readStorageText(storageUri, context), resolveFileUrl(storageUri, context)]);
5564
5758
  if (storageText === null) return null;
@@ -5586,7 +5780,11 @@ async function resolveChangedAssets({ assetBaseStorageUri, currentManifest, curr
5586
5780
  const changedEntries = await Promise.all(Object.entries(targetManifest.assets).map(async ([assetPath, asset]) => {
5587
5781
  if ((currentManifest?.assets[assetPath])?.fileHash === asset.fileHash) return null;
5588
5782
  const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
5589
- const storageUri = createChildStorageUri(assetBaseStorageUri, usesBrotliAsset ? `${assetPath}.br` : assetPath);
5783
+ const storageUri = resolveManifestAssetStorageUri({
5784
+ assetBaseStorageUri,
5785
+ assetPath: usesBrotliAsset ? `${assetPath}.br` : assetPath,
5786
+ fileHash: asset.fileHash
5787
+ });
5590
5788
  const patch = patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
5591
5789
  let fileUrl = null;
5592
5790
  try {
@@ -5756,109 +5954,148 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
5756
5954
  enabled: true,
5757
5955
  id: { gte: minBundleId }
5758
5956
  });
5759
- return {
5760
- api: {
5761
- async getBundleById(id, context) {
5762
- return getPlugin().getBundleById(id, context);
5763
- },
5764
- async getUpdateInfo(args, context) {
5765
- const directGetUpdateInfo = getPlugin().getUpdateInfo;
5766
- if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
5767
- const channel = args.channel ?? "production";
5768
- const minBundleId = args.minBundleId ?? "00000000-0000-0000-0000-000000000000";
5769
- const baseWhere = getBaseWhere({
5770
- platform: args.platform,
5771
- channel,
5772
- minBundleId
5773
- });
5774
- if (args._updateStrategy === "fingerprint") return findUpdateInfoByScanning({
5775
- args,
5776
- queryWhere: {
5777
- ...baseWhere,
5778
- fingerprintHash: args.fingerprintHash
5779
- },
5780
- context,
5781
- isCandidate: (bundle) => {
5782
- return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && bundle.fingerprintHash === args.fingerprintHash;
5783
- }
5784
- });
5785
- return findUpdateInfoByScanning({
5786
- args,
5787
- queryWhere: { ...baseWhere },
5788
- context,
5789
- isCandidate: (bundle) => {
5790
- return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && !!bundle.targetAppVersion && semverSatisfies$1(bundle.targetAppVersion, args.appVersion);
5791
- }
5792
- });
5793
- },
5794
- async getAppUpdateInfo(args, context) {
5795
- const info = await this.getUpdateInfo(args, context);
5796
- if (!info) return null;
5797
- const { storageUri, ...rest } = info;
5798
- const readStorageText = options?.readStorageText;
5799
- if (info.id === "00000000-0000-0000-0000-000000000000" || !readStorageText) {
5800
- const fileUrl = await resolveFileUrl(storageUri ?? null, context);
5801
- return {
5802
- ...rest,
5803
- fileUrl
5804
- };
5957
+ const api = {
5958
+ async getBundleById(id, context) {
5959
+ return getPlugin().getBundleById(id, context);
5960
+ },
5961
+ async getUpdateInfo(args, context) {
5962
+ const directGetUpdateInfo = getPlugin().getUpdateInfo;
5963
+ if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
5964
+ const channel = args.channel ?? "production";
5965
+ const minBundleId = args.minBundleId ?? "00000000-0000-0000-0000-000000000000";
5966
+ const baseWhere = getBaseWhere({
5967
+ platform: args.platform,
5968
+ channel,
5969
+ minBundleId
5970
+ });
5971
+ if (args._updateStrategy === "fingerprint") return findUpdateInfoByScanning({
5972
+ args,
5973
+ queryWhere: {
5974
+ ...baseWhere,
5975
+ fingerprintHash: args.fingerprintHash
5976
+ },
5977
+ context,
5978
+ isCandidate: (bundle) => {
5979
+ return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && bundle.fingerprintHash === args.fingerprintHash;
5980
+ }
5981
+ });
5982
+ return findUpdateInfoByScanning({
5983
+ args,
5984
+ queryWhere: { ...baseWhere },
5985
+ context,
5986
+ isCandidate: (bundle) => {
5987
+ return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && !!bundle.targetAppVersion && semverSatisfies$1(bundle.targetAppVersion, args.appVersion);
5805
5988
  }
5806
- const [fileUrl, targetBundle, currentBundle] = await Promise.all([
5807
- resolveFileUrl(storageUri ?? null, context),
5808
- getPlugin().getBundleById(info.id, context),
5809
- args.bundleId !== "00000000-0000-0000-0000-000000000000" ? getPlugin().getBundleById(args.bundleId, context) : null
5810
- ]);
5811
- const baseResponse = {
5989
+ });
5990
+ },
5991
+ async getAppUpdateInfo(args, context) {
5992
+ const info = await this.getUpdateInfo(args, context);
5993
+ if (!info) return null;
5994
+ const { storageUri, ...rest } = info;
5995
+ const readStorageText = options?.readStorageText;
5996
+ if (info.id === "00000000-0000-0000-0000-000000000000" || !readStorageText) {
5997
+ const fileUrl = await resolveFileUrl(storageUri ?? null, context);
5998
+ return {
5812
5999
  ...rest,
5813
6000
  fileUrl
5814
6001
  };
5815
- const manifestArtifacts = await resolveManifestArtifacts({
5816
- currentBundle,
5817
- resolveFileUrl,
5818
- readStorageText,
5819
- targetBundle,
5820
- context
5821
- });
5822
- if (!manifestArtifacts) return baseResponse;
5823
- return {
5824
- ...baseResponse,
5825
- ...manifestArtifacts
5826
- };
5827
- },
5828
- async getChannels(context) {
5829
- return getPlugin().getChannels(context);
5830
- },
5831
- async getBundles(options, context) {
5832
- return getPlugin().getBundles(options, context);
5833
- },
5834
- async insertBundle(bundle, context) {
5835
- assertBundlePersistenceConstraints(bundle);
5836
- await runWithMutationPlugin(async (plugin) => {
5837
- await plugin.appendBundle(bundle, context);
5838
- await plugin.commitBundle(context);
5839
- });
5840
- },
5841
- async updateBundleById(bundleId, newBundle, context) {
5842
- await runWithMutationPlugin(async (plugin) => {
5843
- const current = await plugin.getBundleById(bundleId, context);
5844
- if (!current) throw new Error("targetBundleId not found");
5845
- assertBundlePersistenceConstraints({
5846
- ...current,
5847
- ...newBundle
5848
- });
5849
- await plugin.updateBundle(bundleId, newBundle, context);
5850
- await plugin.commitBundle(context);
6002
+ }
6003
+ const requestBundleSeeds = getRequestUpdateBundleSeeds(context);
6004
+ const requestBundles = createRequestBundleIdentityMap({
6005
+ context,
6006
+ loadBundleById: (bundleId, requestContext) => getPlugin().getBundleById(bundleId, requestContext),
6007
+ seeds: requestBundleSeeds
6008
+ });
6009
+ const getCurrentBundle = () => {
6010
+ if (args.bundleId === "00000000-0000-0000-0000-000000000000") return null;
6011
+ const seededCurrentBundle = requestBundles.peek(args.bundleId);
6012
+ if (seededCurrentBundle || requestBundleSeeds.length > 0) return seededCurrentBundle;
6013
+ return requestBundles.get(args.bundleId);
6014
+ };
6015
+ const [fileUrl, targetBundle, currentBundle] = await Promise.all([
6016
+ resolveFileUrl(storageUri ?? null, context),
6017
+ requestBundles.get(info.id),
6018
+ getCurrentBundle()
6019
+ ]);
6020
+ const baseResponse = {
6021
+ ...rest,
6022
+ fileUrl
6023
+ };
6024
+ const manifestArtifacts = await resolveManifestArtifacts({
6025
+ currentBundle,
6026
+ resolveFileUrl,
6027
+ readStorageText,
6028
+ targetBundle,
6029
+ context
6030
+ });
6031
+ if (!manifestArtifacts) return baseResponse;
6032
+ return {
6033
+ ...baseResponse,
6034
+ ...manifestArtifacts
6035
+ };
6036
+ },
6037
+ async getChannels(context) {
6038
+ return getPlugin().getChannels(context);
6039
+ },
6040
+ async getBundles(options, context) {
6041
+ return getPlugin().getBundles(options, context);
6042
+ },
6043
+ async insertBundle(bundle, context) {
6044
+ assertBundlePersistenceConstraints(bundle);
6045
+ await runWithMutationPlugin(async (plugin) => {
6046
+ await plugin.appendBundle(bundle, context);
6047
+ await plugin.commitBundle(context);
6048
+ });
6049
+ },
6050
+ async updateBundleById(bundleId, newBundle, context) {
6051
+ await runWithMutationPlugin(async (plugin) => {
6052
+ const current = await plugin.getBundleById(bundleId, context);
6053
+ if (!current) throw new Error("targetBundleId not found");
6054
+ assertBundlePersistenceConstraints({
6055
+ ...current,
6056
+ ...newBundle
5851
6057
  });
5852
- },
5853
- async deleteBundleById(bundleId, context) {
5854
- await runWithMutationPlugin(async (plugin) => {
5855
- const bundle = await plugin.getBundleById(bundleId, context);
5856
- if (!bundle) return;
5857
- await plugin.deleteBundle(bundle, context);
5858
- await plugin.commitBundle(context);
6058
+ await plugin.updateBundle(bundleId, newBundle, context);
6059
+ await plugin.commitBundle(context);
6060
+ });
6061
+ },
6062
+ async deleteBundleById(bundleId, context) {
6063
+ await runWithMutationPlugin(async (plugin) => {
6064
+ const bundle = await plugin.getBundleById(bundleId, context);
6065
+ if (!bundle) return;
6066
+ await plugin.deleteBundle(bundle, context);
6067
+ await plugin.commitBundle(context);
6068
+ });
6069
+ }
6070
+ };
6071
+ Object.defineProperty(api, "diagnostics", {
6072
+ configurable: true,
6073
+ enumerable: true,
6074
+ get() {
6075
+ const diagnostics = getPlugin().diagnostics;
6076
+ if (!diagnostics) {
6077
+ Object.defineProperty(this, "diagnostics", {
6078
+ configurable: true,
6079
+ enumerable: true,
6080
+ value: void 0
5859
6081
  });
6082
+ return;
5860
6083
  }
5861
- },
6084
+ const wrappedDiagnostics = {};
6085
+ if (diagnostics.bundleIndex) wrappedDiagnostics.bundleIndex = {
6086
+ check: (context) => getPlugin().diagnostics.bundleIndex.check(context),
6087
+ ...diagnostics.bundleIndex.repair ? { repair: (context) => getPlugin().diagnostics.bundleIndex.repair(context) } : {}
6088
+ };
6089
+ Object.defineProperty(this, "diagnostics", {
6090
+ configurable: true,
6091
+ enumerable: true,
6092
+ value: wrappedDiagnostics
6093
+ });
6094
+ return wrappedDiagnostics;
6095
+ }
6096
+ });
6097
+ return {
6098
+ api,
5862
6099
  adapterName: getPlugin().name,
5863
6100
  createMigrator: () => {
5864
6101
  throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
@@ -6924,7 +7161,6 @@ var require_min_version = /* @__PURE__ */ __commonJSMin$1(((exports, module) =>
6924
7161
  break;
6925
7162
  case "<":
6926
7163
  case "<=": break;
6927
- /* istanbul ignore next */
6928
7164
  default: throw new Error(`Unexpected operation: ${comparator.operator}`);
6929
7165
  }
6930
7166
  });
@@ -7274,7 +7510,7 @@ function findRoute(router, method, path) {
7274
7510
  }
7275
7511
  //#endregion
7276
7512
  //#region ../../packages/server/src/version.ts
7277
- const HOT_UPDATER_SERVER_VERSION = "0.31.4";
7513
+ const HOT_UPDATER_SERVER_VERSION = "0.33.0";
7278
7514
  //#endregion
7279
7515
  //#region ../../packages/server/src/handler.ts
7280
7516
  var HandlerBadRequestError = class extends Error {
@@ -7285,6 +7521,8 @@ var HandlerBadRequestError = class extends Error {
7285
7521
  };
7286
7522
  const SDK_VERSION_HEADER = "Hot-Updater-SDK-Version";
7287
7523
  const EXPLICIT_NO_UPDATE_MIN_SDK_VERSION = "0.31.0";
7524
+ const DEFAULT_BUNDLE_LIST_LIMIT = 50;
7525
+ const MAX_BUNDLE_LIST_LIMIT = 100;
7288
7526
  const supportsExplicitNoUpdateResponse = (request) => {
7289
7527
  const sdkVersion = request.headers.get(SDK_VERSION_HEADER)?.trim();
7290
7528
  if (!sdkVersion) return false;
@@ -7334,6 +7572,13 @@ const parseStringArraySearchParam = (url, key) => {
7334
7572
  const values = url.searchParams.getAll(key);
7335
7573
  return values.length > 0 ? values : void 0;
7336
7574
  };
7575
+ const parsePositiveIntegerSearchParam = (url, key, defaultValue, maxValue) => {
7576
+ const value = url.searchParams.get(key);
7577
+ if (value === null) return defaultValue;
7578
+ const parsed = Number(value);
7579
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maxValue) throw new HandlerBadRequestError(`The '${key}' query parameter must be a positive integer between 1 and ${maxValue}.`);
7580
+ return parsed;
7581
+ };
7337
7582
  const requirePlatformParam = (params) => {
7338
7583
  const platform = requireRouteParam(params, "platform");
7339
7584
  if (!isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
@@ -7402,7 +7647,7 @@ const handleGetBundles = async (_params, request, api, context) => {
7402
7647
  const url = new URL(request.url);
7403
7648
  const channel = url.searchParams.get("channel") ?? void 0;
7404
7649
  const platform = url.searchParams.get("platform");
7405
- const limit = Number(url.searchParams.get("limit")) || 50;
7650
+ const limit = parsePositiveIntegerSearchParam(url, "limit", DEFAULT_BUNDLE_LIST_LIMIT, MAX_BUNDLE_LIST_LIMIT);
7406
7651
  const pageParam = url.searchParams.get("page");
7407
7652
  const offset = url.searchParams.get("offset");
7408
7653
  const after = url.searchParams.get("after") ?? void 0;
@@ -7504,18 +7749,19 @@ const routes = {
7504
7749
  */
7505
7750
  function createHandler(api, options = {}) {
7506
7751
  const basePath = options.basePath ?? "/api";
7507
- const updateCheckEnabled = options.routes?.updateCheck ?? true;
7508
- const versionEnabled = options.routes?.version ?? true;
7509
- const bundlesEnabled = options.routes?.bundles ?? true;
7752
+ const routeOptions = {
7753
+ updateCheck: options.routes?.updateCheck ?? true,
7754
+ bundles: options.routes?.bundles ?? false
7755
+ };
7510
7756
  const router = createRouter();
7511
- if (versionEnabled) addRoute(router, "GET", "/version", "version");
7512
- if (updateCheckEnabled) {
7757
+ addRoute(router, "GET", "/version", "version");
7758
+ if (routeOptions.updateCheck) {
7513
7759
  addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdateWithCohort");
7514
7760
  addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort", "fingerprintUpdateWithCohort");
7515
7761
  addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdateWithCohort");
7516
7762
  addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort", "appVersionUpdateWithCohort");
7517
7763
  }
7518
- if (bundlesEnabled) {
7764
+ if (routeOptions.bundles) {
7519
7765
  addRoute(router, "GET", "/api/bundles/channels", "getChannels");
7520
7766
  addRoute(router, "GET", "/api/bundles/:id", "getBundle");
7521
7767
  addRoute(router, "GET", "/api/bundles", "getBundles");
@@ -7556,7 +7802,7 @@ function createHandler(api, options = {}) {
7556
7802
  }
7557
7803
  //#endregion
7558
7804
  //#region ../../packages/server/src/route.ts
7559
- const normalizeBasePath = (basePath) => {
7805
+ const normalizeBasePath$1 = (basePath) => {
7560
7806
  if (!basePath || basePath === "/") return "/";
7561
7807
  return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
7562
7808
  };
@@ -7607,7 +7853,7 @@ const createStorageAccess = (storagePlugins) => {
7607
7853
  //#region ../../packages/server/src/runtime.ts
7608
7854
  function createHotUpdater(options) {
7609
7855
  const database = options.database;
7610
- const basePath = normalizeBasePath(options.basePath ?? "/api");
7856
+ const basePath = normalizeBasePath$1(options.basePath ?? "/api");
7611
7857
  const { readStorageText, resolveFileUrl } = createStorageAccess((options.storages ?? options.storagePlugins ?? []).map((plugin) => {
7612
7858
  const storagePlugin = typeof plugin === "function" ? plugin() : plugin;
7613
7859
  assertRuntimeStoragePlugin(storagePlugin);
@@ -7619,23 +7865,21 @@ function createHotUpdater(options) {
7619
7865
  createMutationPlugin: () => database(),
7620
7866
  readStorageText
7621
7867
  } : { readStorageText });
7622
- const api = {
7623
- ...core.api,
7624
- handler: createHandler(core.api, {
7625
- basePath,
7626
- routes: options.routes
7627
- }),
7628
- adapterName: core.adapterName
7629
- };
7868
+ const internalHandler = createHandler(core.api, {
7869
+ basePath,
7870
+ routes: options.routes
7871
+ });
7630
7872
  const handler = (request, context, ...extraArgs) => {
7631
- if (extraArgs.length > 0) return api.handler(request);
7632
- return api.handler(request, context);
7873
+ if (extraArgs.length > 0) return internalHandler(request);
7874
+ return internalHandler(request, context);
7633
7875
  };
7634
- return {
7635
- ...api,
7876
+ const api = {
7636
7877
  basePath,
7878
+ adapterName: core.adapterName,
7637
7879
  handler
7638
7880
  };
7881
+ Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
7882
+ return api;
7639
7883
  }
7640
7884
  //#endregion
7641
7885
  //#region ../../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/compose.js
@@ -9530,6 +9774,37 @@ const streamToString = (stream) => {
9530
9774
  const DEFAULT_INVALIDATION_POLL_INTERVAL_MS = 2e3;
9531
9775
  const DEFAULT_INVALIDATION_TIMEOUT_MS = 300 * 1e3;
9532
9776
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9777
+ const getS3ErrorProperty = (error, key) => {
9778
+ if (typeof error !== "object" || error === null) return;
9779
+ const value = error[key];
9780
+ return typeof value === "string" ? value : void 0;
9781
+ };
9782
+ const isArchivedS3ObjectError = (error) => {
9783
+ if (!(error instanceof Error)) return false;
9784
+ return error.name === "InvalidObjectState" || getS3ErrorProperty(error, "Code") === "InvalidObjectState";
9785
+ };
9786
+ const createArchivedS3ObjectError = ({ bucket, key, error }) => {
9787
+ const storageClass = getS3ErrorProperty(error, "StorageClass") ?? "archived storage";
9788
+ const nextError = new Error(`S3 object "${key}" in bucket "${bucket}" is archived (${storageClass}) and cannot be read. Restore the object in S3 or exclude Hot Updater metadata from lifecycle archival: "_index/**", "**/target-app-versions.json", and "**/update.json".`, { cause: error });
9789
+ nextError.name = "S3ArchivedObjectError";
9790
+ return nextError;
9791
+ };
9792
+ function normalizeBasePath(basePath) {
9793
+ return basePath?.replace(/^\/+|\/+$/g, "") ?? "";
9794
+ }
9795
+ function createDatabaseKeyBuilder(basePath) {
9796
+ const normalizedBasePath = normalizeBasePath(basePath);
9797
+ const toStorageKey = (key) => [normalizedBasePath, key].filter(Boolean).join("/");
9798
+ const fromStorageKey = (key) => {
9799
+ if (!normalizedBasePath) return key;
9800
+ const prefix = `${normalizedBasePath}/`;
9801
+ return key.startsWith(prefix) ? key.slice(prefix.length) : key;
9802
+ };
9803
+ return {
9804
+ fromStorageKey,
9805
+ toStorageKey
9806
+ };
9807
+ }
9533
9808
  /**
9534
9809
  * Loads JSON data from S3.
9535
9810
  * Returns null if NoSuchKey error occurs.
@@ -9545,6 +9820,11 @@ async function loadJsonFromS3(client, bucket, key) {
9545
9820
  return JSON.parse(bodyContents);
9546
9821
  } catch (e) {
9547
9822
  if (e instanceof _aws_sdk_client_s3.NoSuchKey) return null;
9823
+ if (isArchivedS3ObjectError(e)) throw createArchivedS3ObjectError({
9824
+ bucket,
9825
+ key,
9826
+ error: e
9827
+ });
9548
9828
  throw e;
9549
9829
  }
9550
9830
  }
@@ -9628,18 +9908,20 @@ async function invalidateCloudFront(client, distributionId, paths, options) {
9628
9908
  const s3Database = createBlobDatabasePlugin({
9629
9909
  name: "s3Database",
9630
9910
  factory: (config) => {
9631
- const { bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
9911
+ const { basePath, bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
9632
9912
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
9913
+ const { fromStorageKey, toStorageKey } = createDatabaseKeyBuilder(basePath);
9633
9914
  const cloudfrontClient = cloudfrontDistributionId ? new _aws_sdk_client_cloudfront.CloudFrontClient({
9634
9915
  credentials: s3Config.credentials,
9635
9916
  region: s3Config.region
9636
9917
  }) : void 0;
9637
9918
  return {
9638
9919
  apiBasePath,
9639
- listObjects: (prefix) => listObjectsInS3(client, bucketName, prefix),
9640
- loadObject: (key) => loadJsonFromS3(client, bucketName, key),
9641
- uploadObject: (key, data) => uploadJsonToS3(client, bucketName, key, data),
9642
- deleteObject: (key) => deleteObjectInS3(client, bucketName, key),
9920
+ listObjects: (prefix) => listObjectsInS3(client, bucketName, toStorageKey(prefix)).then((keys) => keys.map(fromStorageKey)),
9921
+ loadObject: (key) => loadJsonFromS3(client, bucketName, toStorageKey(key)),
9922
+ uploadObject: (key, data) => uploadJsonToS3(client, bucketName, toStorageKey(key), data),
9923
+ deleteObject: (key) => deleteObjectInS3(client, bucketName, toStorageKey(key)),
9924
+ shouldSkipLoadObjectError: (error) => error instanceof Error && error.name === "S3ArchivedObjectError",
9643
9925
  invalidatePaths: (pathsToInvalidate) => {
9644
9926
  if (cloudfrontClient && cloudfrontDistributionId && pathsToInvalidate.length > 0) return invalidateCloudFront(cloudfrontClient, cloudfrontDistributionId, pathsToInvalidate, { shouldWaitForInvalidation });
9645
9927
  return Promise.resolve();
@@ -9696,6 +9978,20 @@ const s3Storage = createUniversalStoragePlugin({
9696
9978
  if (!response.Bucket || !response.Key) throw new Error("Upload Failed");
9697
9979
  return { storageUri: `s3://${bucketName}/${Key}` };
9698
9980
  },
9981
+ async exists(storageUri) {
9982
+ const { bucket, key } = parseStorageUri(storageUri, "s3");
9983
+ if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
9984
+ try {
9985
+ await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
9986
+ Bucket: bucketName,
9987
+ Key: key
9988
+ }));
9989
+ return true;
9990
+ } catch (error) {
9991
+ if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return false;
9992
+ throw error;
9993
+ }
9994
+ },
9699
9995
  async downloadFile(storageUri, filePath) {
9700
9996
  const { bucket, key } = parseStorageUri(storageUri, "s3");
9701
9997
  if (bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`);
@@ -9848,7 +10144,6 @@ const hotUpdater = createHotUpdater({
9848
10144
  basePath: HOT_UPDATER_BASE_PATH,
9849
10145
  routes: {
9850
10146
  updateCheck: true,
9851
- version: true,
9852
10147
  bundles: false
9853
10148
  }
9854
10149
  });