@hot-updater/aws 0.32.0 → 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.
package/dist/index.cjs CHANGED
@@ -1412,6 +1412,37 @@ const streamToString = (stream) => {
1412
1412
  const DEFAULT_INVALIDATION_POLL_INTERVAL_MS = 2e3;
1413
1413
  const DEFAULT_INVALIDATION_TIMEOUT_MS = 300 * 1e3;
1414
1414
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1415
+ const getS3ErrorProperty = (error, key) => {
1416
+ if (typeof error !== "object" || error === null) return;
1417
+ const value = error[key];
1418
+ return typeof value === "string" ? value : void 0;
1419
+ };
1420
+ const isArchivedS3ObjectError = (error) => {
1421
+ if (!(error instanceof Error)) return false;
1422
+ return error.name === "InvalidObjectState" || getS3ErrorProperty(error, "Code") === "InvalidObjectState";
1423
+ };
1424
+ const createArchivedS3ObjectError = ({ bucket, key, error }) => {
1425
+ const storageClass = getS3ErrorProperty(error, "StorageClass") ?? "archived storage";
1426
+ 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 });
1427
+ nextError.name = "S3ArchivedObjectError";
1428
+ return nextError;
1429
+ };
1430
+ function normalizeBasePath(basePath) {
1431
+ return basePath?.replace(/^\/+|\/+$/g, "") ?? "";
1432
+ }
1433
+ function createDatabaseKeyBuilder(basePath) {
1434
+ const normalizedBasePath = normalizeBasePath(basePath);
1435
+ const toStorageKey = (key) => [normalizedBasePath, key].filter(Boolean).join("/");
1436
+ const fromStorageKey = (key) => {
1437
+ if (!normalizedBasePath) return key;
1438
+ const prefix = `${normalizedBasePath}/`;
1439
+ return key.startsWith(prefix) ? key.slice(prefix.length) : key;
1440
+ };
1441
+ return {
1442
+ fromStorageKey,
1443
+ toStorageKey
1444
+ };
1445
+ }
1415
1446
  /**
1416
1447
  * Loads JSON data from S3.
1417
1448
  * Returns null if NoSuchKey error occurs.
@@ -1427,6 +1458,11 @@ async function loadJsonFromS3(client, bucket, key) {
1427
1458
  return JSON.parse(bodyContents);
1428
1459
  } catch (e) {
1429
1460
  if (e instanceof _aws_sdk_client_s3.NoSuchKey) return null;
1461
+ if (isArchivedS3ObjectError(e)) throw createArchivedS3ObjectError({
1462
+ bucket,
1463
+ key,
1464
+ error: e
1465
+ });
1430
1466
  throw e;
1431
1467
  }
1432
1468
  }
@@ -1510,18 +1546,20 @@ async function invalidateCloudFront(client, distributionId, paths, options) {
1510
1546
  const s3Database = (0, _hot_updater_plugin_core.createBlobDatabasePlugin)({
1511
1547
  name: "s3Database",
1512
1548
  factory: (config) => {
1513
- const { bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
1549
+ const { basePath, bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
1514
1550
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
1551
+ const { fromStorageKey, toStorageKey } = createDatabaseKeyBuilder(basePath);
1515
1552
  const cloudfrontClient = cloudfrontDistributionId ? new _aws_sdk_client_cloudfront.CloudFrontClient({
1516
1553
  credentials: s3Config.credentials,
1517
1554
  region: s3Config.region
1518
1555
  }) : void 0;
1519
1556
  return {
1520
1557
  apiBasePath,
1521
- listObjects: (prefix) => listObjectsInS3(client, bucketName, prefix),
1522
- loadObject: (key) => loadJsonFromS3(client, bucketName, key),
1523
- uploadObject: (key, data) => uploadJsonToS3(client, bucketName, key, data),
1524
- deleteObject: (key) => deleteObjectInS3(client, bucketName, key),
1558
+ listObjects: (prefix) => listObjectsInS3(client, bucketName, toStorageKey(prefix)).then((keys) => keys.map(fromStorageKey)),
1559
+ loadObject: (key) => loadJsonFromS3(client, bucketName, toStorageKey(key)),
1560
+ uploadObject: (key, data) => uploadJsonToS3(client, bucketName, toStorageKey(key), data),
1561
+ deleteObject: (key) => deleteObjectInS3(client, bucketName, toStorageKey(key)),
1562
+ shouldSkipLoadObjectError: (error) => error instanceof Error && error.name === "S3ArchivedObjectError",
1525
1563
  invalidatePaths: (pathsToInvalidate) => {
1526
1564
  if (cloudfrontClient && cloudfrontDistributionId && pathsToInvalidate.length > 0) return invalidateCloudFront(cloudfrontClient, cloudfrontDistributionId, pathsToInvalidate, { shouldWaitForInvalidation });
1527
1565
  return Promise.resolve();
package/dist/index.d.cts CHANGED
@@ -5,6 +5,10 @@ import { S3ClientConfig } from "@aws-sdk/client-s3";
5
5
  //#region src/s3Database.d.ts
6
6
  interface S3DatabaseConfig extends S3ClientConfig, BlobDatabasePluginConfig {
7
7
  bucketName: string;
8
+ /**
9
+ * Base path where database objects will be stored in the bucket.
10
+ */
11
+ basePath?: string;
8
12
  /**
9
13
  * CloudFront distribution ID used for cache invalidation.
10
14
  *
package/dist/index.d.mts CHANGED
@@ -5,6 +5,10 @@ import { BlobDatabasePluginConfig, RuntimeStoragePlugin, StoragePluginHooks, Sto
5
5
  //#region src/s3Database.d.ts
6
6
  interface S3DatabaseConfig extends S3ClientConfig, BlobDatabasePluginConfig {
7
7
  bucketName: string;
8
+ /**
9
+ * Base path where database objects will be stored in the bucket.
10
+ */
11
+ basePath?: string;
8
12
  /**
9
13
  * CloudFront distribution ID used for cache invalidation.
10
14
  *
package/dist/index.mjs CHANGED
@@ -1387,6 +1387,37 @@ const streamToString = (stream) => {
1387
1387
  const DEFAULT_INVALIDATION_POLL_INTERVAL_MS = 2e3;
1388
1388
  const DEFAULT_INVALIDATION_TIMEOUT_MS = 300 * 1e3;
1389
1389
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1390
+ const getS3ErrorProperty = (error, key) => {
1391
+ if (typeof error !== "object" || error === null) return;
1392
+ const value = error[key];
1393
+ return typeof value === "string" ? value : void 0;
1394
+ };
1395
+ const isArchivedS3ObjectError = (error) => {
1396
+ if (!(error instanceof Error)) return false;
1397
+ return error.name === "InvalidObjectState" || getS3ErrorProperty(error, "Code") === "InvalidObjectState";
1398
+ };
1399
+ const createArchivedS3ObjectError = ({ bucket, key, error }) => {
1400
+ const storageClass = getS3ErrorProperty(error, "StorageClass") ?? "archived storage";
1401
+ 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 });
1402
+ nextError.name = "S3ArchivedObjectError";
1403
+ return nextError;
1404
+ };
1405
+ function normalizeBasePath(basePath) {
1406
+ return basePath?.replace(/^\/+|\/+$/g, "") ?? "";
1407
+ }
1408
+ function createDatabaseKeyBuilder(basePath) {
1409
+ const normalizedBasePath = normalizeBasePath(basePath);
1410
+ const toStorageKey = (key) => [normalizedBasePath, key].filter(Boolean).join("/");
1411
+ const fromStorageKey = (key) => {
1412
+ if (!normalizedBasePath) return key;
1413
+ const prefix = `${normalizedBasePath}/`;
1414
+ return key.startsWith(prefix) ? key.slice(prefix.length) : key;
1415
+ };
1416
+ return {
1417
+ fromStorageKey,
1418
+ toStorageKey
1419
+ };
1420
+ }
1390
1421
  /**
1391
1422
  * Loads JSON data from S3.
1392
1423
  * Returns null if NoSuchKey error occurs.
@@ -1402,6 +1433,11 @@ async function loadJsonFromS3(client, bucket, key) {
1402
1433
  return JSON.parse(bodyContents);
1403
1434
  } catch (e) {
1404
1435
  if (e instanceof NoSuchKey) return null;
1436
+ if (isArchivedS3ObjectError(e)) throw createArchivedS3ObjectError({
1437
+ bucket,
1438
+ key,
1439
+ error: e
1440
+ });
1405
1441
  throw e;
1406
1442
  }
1407
1443
  }
@@ -1485,18 +1521,20 @@ async function invalidateCloudFront(client, distributionId, paths, options) {
1485
1521
  const s3Database = createBlobDatabasePlugin({
1486
1522
  name: "s3Database",
1487
1523
  factory: (config) => {
1488
- const { bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
1524
+ const { basePath, bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
1489
1525
  const client = new S3Client(applyS3RuntimeAwsConfig(s3Config));
1526
+ const { fromStorageKey, toStorageKey } = createDatabaseKeyBuilder(basePath);
1490
1527
  const cloudfrontClient = cloudfrontDistributionId ? new CloudFrontClient({
1491
1528
  credentials: s3Config.credentials,
1492
1529
  region: s3Config.region
1493
1530
  }) : void 0;
1494
1531
  return {
1495
1532
  apiBasePath,
1496
- listObjects: (prefix) => listObjectsInS3(client, bucketName, prefix),
1497
- loadObject: (key) => loadJsonFromS3(client, bucketName, key),
1498
- uploadObject: (key, data) => uploadJsonToS3(client, bucketName, key, data),
1499
- deleteObject: (key) => deleteObjectInS3(client, bucketName, key),
1533
+ listObjects: (prefix) => listObjectsInS3(client, bucketName, toStorageKey(prefix)).then((keys) => keys.map(fromStorageKey)),
1534
+ loadObject: (key) => loadJsonFromS3(client, bucketName, toStorageKey(key)),
1535
+ uploadObject: (key, data) => uploadJsonToS3(client, bucketName, toStorageKey(key), data),
1536
+ deleteObject: (key) => deleteObjectInS3(client, bucketName, toStorageKey(key)),
1537
+ shouldSkipLoadObjectError: (error) => error instanceof Error && error.name === "S3ArchivedObjectError",
1500
1538
  invalidatePaths: (pathsToInvalidate) => {
1501
1539
  if (cloudfrontClient && cloudfrontDistributionId && pathsToInvalidate.length > 0) return invalidateCloudFront(cloudfrontClient, cloudfrontDistributionId, pathsToInvalidate, { shouldWaitForInvalidation });
1502
1540
  return Promise.resolve();
@@ -3225,6 +3225,24 @@ function paginateBundles({ bundles, limit, offset, cursor, orderBy }) {
3225
3225
  };
3226
3226
  }
3227
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
3228
3246
  //#region ../../packages/core/dist/index.mjs
3229
3247
  const getManifestStorageUri = (bundle) => bundle.manifestStorageUri ?? null;
3230
3248
  const getManifestFileHash = (bundle) => bundle.manifestFileHash ?? null;
@@ -4719,7 +4737,34 @@ const day = 3600 * 24;
4719
4737
  day * 7;
4720
4738
  day * 365.25;
4721
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
4722
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
+ }
4723
4768
  function removeBundleInternalKeys(bundle) {
4724
4769
  const { _updateJsonKey, _oldUpdateJsonKey, ...pureBundle } = bundle;
4725
4770
  return pureBundle;
@@ -4768,6 +4813,12 @@ const MANAGEMENT_INDEX_PREFIX = "_index";
4768
4813
  const MANAGEMENT_INDEX_VERSION = 1;
4769
4814
  const DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE = 128;
4770
4815
  const ALL_SCOPE_CACHE_KEY = "*|*";
4816
+ function summarizeManagementIndexArtifacts(artifacts) {
4817
+ return {
4818
+ pagesWritten: artifacts.pages.size,
4819
+ scopesWritten: artifacts.scopes.length
4820
+ };
4821
+ }
4771
4822
  function resolveManagementIndexPageSize(config) {
4772
4823
  const pageSize = config.managementIndexPageSize ?? DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE;
4773
4824
  if (!Number.isInteger(pageSize) || pageSize < 1) throw new Error("managementIndexPageSize must be a positive integer.");
@@ -4899,11 +4950,18 @@ function buildManagementIndexArtifacts(allBundles, pageSize) {
4899
4950
  const createBlobDatabasePlugin = ({ name, factory }) => {
4900
4951
  return (config, hooks) => {
4901
4952
  const managementIndexPageSize = resolveManagementIndexPageSize(config);
4902
- const { listObjects, loadObject, uploadObject, deleteObject, invalidatePaths, apiBasePath } = factory(config);
4953
+ const { listObjects, loadObject, uploadObject, deleteObject, shouldSkipLoadObjectError, invalidatePaths, apiBasePath } = factory(config);
4903
4954
  const bundlesMap = /* @__PURE__ */ new Map();
4904
4955
  const pendingBundlesMap = /* @__PURE__ */ new Map();
4905
4956
  const managementRootCache = /* @__PURE__ */ new Map();
4906
- 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
+ };
4907
4965
  const getAllManagementArtifact = (artifacts) => {
4908
4966
  const allArtifact = artifacts.scopes.find((scope) => scope.cacheKey === ALL_SCOPE_CACHE_KEY);
4909
4967
  if (!allArtifact) throw new Error("all-bundles management index artifact not found");
@@ -4920,7 +4978,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4920
4978
  };
4921
4979
  const loadStoredManagementRoot = async (scope) => {
4922
4980
  const cacheKey = getManagementScopeCacheKey(scope);
4923
- const storedRoot = await loadObject(getManagementRootKey(scope));
4981
+ const storedRoot = await loadOptionalObject(getManagementRootKey(scope));
4924
4982
  if (storedRoot) {
4925
4983
  managementRootCache.set(cacheKey, storedRoot);
4926
4984
  return storedRoot;
@@ -4930,7 +4988,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4930
4988
  };
4931
4989
  const loadManagementPage = async (descriptor, pageCache) => {
4932
4990
  if (pageCache?.has(descriptor.key)) return pageCache.get(descriptor.key) ?? null;
4933
- const page = await loadObject(descriptor.key);
4991
+ const page = await loadOptionalObject(descriptor.key);
4934
4992
  pageCache?.set(descriptor.key, page);
4935
4993
  return page;
4936
4994
  };
@@ -4953,13 +5011,13 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4953
5011
  return allBundles;
4954
5012
  };
4955
5013
  const persistManagementIndexArtifacts = async (nextArtifacts, previousArtifacts) => {
4956
- for (const [key, page] of nextArtifacts.pages.entries()) await uploadObject(key, page);
4957
- 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));
4958
5016
  if (!previousArtifacts) return;
4959
5017
  const nextPageKeys = new Set(nextArtifacts.pages.keys());
4960
5018
  const nextRootKeys = new Set(nextArtifacts.scopes.map((scope) => scope.rootKey));
4961
- for (const [key] of previousArtifacts.pages.entries()) if (!nextPageKeys.has(key)) await deleteObject(key).catch(() => {});
4962
- 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(() => {}));
4963
5021
  };
4964
5022
  const ensureAllManagementRoot = async () => {
4965
5023
  const storedAllRoot = await loadStoredManagementRoot({});
@@ -4992,6 +5050,26 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4992
5050
  const loadCurrentBundlesForIndexRebuild = async () => {
4993
5051
  return loadAllBundlesForManagementFallback();
4994
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
+ };
4995
5073
  const findPageIndexContainingId = (pages, id) => {
4996
5074
  return pages.findIndex((page) => id.localeCompare(page.firstId) <= 0 && id.localeCompare(page.lastId) >= 0);
4997
5075
  };
@@ -5167,16 +5245,15 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5167
5245
  };
5168
5246
  async function reloadBundles() {
5169
5247
  bundlesMap.clear();
5170
- const filePromises = (await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)).map(async (key) => {
5171
- 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) => ({
5172
5250
  ...bundle,
5173
5251
  _updateJsonKey: key
5174
5252
  }));
5175
- });
5176
- const allBundles = (await Promise.all(filePromises)).flat();
5253
+ })).flat();
5177
5254
  for (const bundle of allBundles) bundlesMap.set(bundle.id, bundle);
5178
5255
  for (const [id, bundle] of pendingBundlesMap.entries()) bundlesMap.set(id, bundle);
5179
- return orderBy(allBundles, [(v) => v.id], ["desc"]);
5256
+ return orderBy(Array.from(bundlesMap.values()), [(v) => v.id], ["desc"]);
5180
5257
  }
5181
5258
  /**
5182
5259
  * Updates target-app-versions.json for each channel on the given platform.
@@ -5202,35 +5279,44 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5202
5279
  const updateKeys = keysByChannel[channel];
5203
5280
  const targetKey = `${channel}/${platform}/target-app-versions.json`;
5204
5281
  const currentVersions = updateKeys.map((key) => key.split("/")[2]);
5205
- const oldTargetVersions = await loadObject(targetKey) ?? [];
5282
+ const oldTargetVersions = await loadOptionalObject(targetKey) ?? [];
5206
5283
  const newTargetVersions = oldTargetVersions.filter((v) => currentVersions.includes(v));
5207
5284
  for (const v of currentVersions) if (!newTargetVersions.includes(v)) newTargetVersions.push(v);
5208
5285
  if (JSON.stringify(oldTargetVersions) !== JSON.stringify(newTargetVersions)) await uploadObject(targetKey, newTargetVersions);
5209
5286
  }
5210
5287
  }
5211
- const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }) => {
5212
- const matchingVersions = filterCompatibleAppVersions(await loadObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion);
5213
- return getUpdateInfo((await Promise.allSettled(matchingVersions.map(async (targetAppVersion) => {
5214
- return await loadObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`) ?? [];
5215
- }))).filter((entry) => entry.status === "fulfilled").flatMap((entry) => entry.value), {
5216
- _updateStrategy: "appVersion",
5217
- appVersion,
5218
- bundleId,
5219
- channel,
5220
- cohort,
5221
- minBundleId,
5222
- 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
5223
5304
  });
5224
5305
  };
5225
- const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }) => {
5226
- return getUpdateInfo(await loadObject(`${channel}/${platform}/${fingerprintHash}/update.json`) ?? [], {
5227
- _updateStrategy: "fingerprint",
5228
- bundleId,
5229
- channel,
5230
- cohort,
5231
- fingerprintHash,
5232
- minBundleId,
5233
- 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
5234
5320
  });
5235
5321
  };
5236
5322
  const addAppVersionInvalidationPaths = (pathsToInvalidate, { platform, channel, targetAppVersion }) => {
@@ -5252,7 +5338,35 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5252
5338
  targetAppVersion
5253
5339
  });
5254
5340
  };
5255
- 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({
5256
5370
  name,
5257
5371
  factory: () => ({
5258
5372
  supportsCursorPagination: true,
@@ -5276,9 +5390,9 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5276
5390
  if (!matchedBundle) return null;
5277
5391
  return removeBundleInternalKeys(matchedBundle);
5278
5392
  },
5279
- async getUpdateInfo(args) {
5280
- if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args);
5281
- return getFingerprintUpdateInfo(args);
5393
+ async getUpdateInfo(args, context) {
5394
+ if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args, context);
5395
+ return getFingerprintUpdateInfo(args, context);
5282
5396
  },
5283
5397
  async getBundles(options) {
5284
5398
  const { where, limit, offset, orderBy, cursor } = options;
@@ -5311,11 +5425,8 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5311
5425
  const changedBundlesByKey = {};
5312
5426
  const removalsByKey = {};
5313
5427
  const pathsToInvalidate = /* @__PURE__ */ new Set();
5314
- let isTargetAppVersionChanged = false;
5315
- let isChannelChanged = false;
5428
+ const targetVersionPlatforms = /* @__PURE__ */ new Set();
5316
5429
  for (const { operation, data } of changedSets) {
5317
- if (data.targetAppVersion !== void 0) isTargetAppVersionChanged = true;
5318
- if (operation === "update" && data.channel !== void 0) isChannelChanged = true;
5319
5430
  if (operation === "insert") {
5320
5431
  const target = resolveStorageTarget(data);
5321
5432
  const key = `${data.channel}/${data.platform}/${target}/update.json`;
@@ -5327,6 +5438,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5327
5438
  pendingBundlesMap.set(data.id, bundleWithKey);
5328
5439
  changedBundlesByKey[key] = changedBundlesByKey[key] || [];
5329
5440
  changedBundlesByKey[key].push(removeBundleInternalKeys(bundleWithKey));
5441
+ if (data.targetAppVersion !== void 0) targetVersionPlatforms.add(data.platform);
5330
5442
  addLookupInvalidationPaths(pathsToInvalidate, data);
5331
5443
  continue;
5332
5444
  }
@@ -5339,6 +5451,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5339
5451
  const key = bundle._updateJsonKey;
5340
5452
  removalsByKey[key] = removalsByKey[key] || [];
5341
5453
  removalsByKey[key].push(bundle.id);
5454
+ if (bundle.targetAppVersion !== void 0) targetVersionPlatforms.add(bundle.platform);
5342
5455
  addLookupInvalidationPaths(pathsToInvalidate, bundle);
5343
5456
  continue;
5344
5457
  }
@@ -5370,6 +5483,10 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5370
5483
  channel: nextChannel
5371
5484
  });
5372
5485
  }
5486
+ if (bundle.targetAppVersion !== void 0 || updatedBundle.targetAppVersion !== void 0) {
5487
+ targetVersionPlatforms.add(bundle.platform);
5488
+ targetVersionPlatforms.add(updatedBundle.platform);
5489
+ }
5373
5490
  addLookupInvalidationPaths(pathsToInvalidate, updatedBundle);
5374
5491
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
5375
5492
  continue;
@@ -5383,14 +5500,14 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5383
5500
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
5384
5501
  }
5385
5502
  }
5386
- for (const oldKey of Object.keys(removalsByKey)) await (async () => {
5387
- 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));
5388
5505
  updatedBundles.sort((a, b) => b.id.localeCompare(a.id));
5389
5506
  if (updatedBundles.length === 0) await deleteObject(oldKey);
5390
5507
  else await uploadObject(oldKey, updatedBundles);
5391
- })();
5392
- for (const key of Object.keys(changedBundlesByKey)) await (async () => {
5393
- const currentBundles = await loadObject(key) ?? [];
5508
+ });
5509
+ await forEachWithConcurrency(Object.keys(changedBundlesByKey), STORAGE_OPERATION_CONCURRENCY, async (key) => {
5510
+ const currentBundles = await loadOptionalObject(key) ?? [];
5394
5511
  const pureBundles = changedBundlesByKey[key].map((bundle) => bundle);
5395
5512
  for (const changedBundle of pureBundles) {
5396
5513
  const index = currentBundles.findIndex((b) => b.id === changedBundle.id);
@@ -5399,10 +5516,11 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5399
5516
  }
5400
5517
  currentBundles.sort((a, b) => b.id.localeCompare(a.id));
5401
5518
  await uploadObject(key, currentBundles);
5402
- })();
5403
- if (isTargetAppVersionChanged || isChannelChanged) for (const platform of PLATFORMS) await updateTargetVersionsForPlatform(platform);
5404
- const currentIndexBundles = await loadCurrentBundlesForIndexRebuild();
5405
- 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]));
5406
5524
  for (const { operation, data } of changedSets) {
5407
5525
  if (operation === "delete") {
5408
5526
  nextIndexMap.delete(data.id);
@@ -5411,7 +5529,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5411
5529
  nextIndexMap.set(data.id, data);
5412
5530
  }
5413
5531
  const nextIndexBundles = sortManagedBundles(Array.from(nextIndexMap.values()));
5414
- const previousArtifacts = buildManagementIndexArtifacts(currentIndexBundles, managementIndexPageSize);
5532
+ const previousArtifacts = buildManagementIndexArtifacts(previousIndexBundles, managementIndexPageSize);
5415
5533
  const nextArtifacts = buildManagementIndexArtifacts(nextIndexBundles, managementIndexPageSize);
5416
5534
  await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
5417
5535
  replaceManagementRootCache(nextArtifacts);
@@ -5422,6 +5540,15 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
5422
5540
  }
5423
5541
  })
5424
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
+ };
5425
5552
  };
5426
5553
  };
5427
5554
  //#endregion
@@ -5563,6 +5690,34 @@ function assertRuntimeStoragePlugin(plugin) {
5563
5690
  if (!isRuntimeStoragePlugin(plugin)) throw createMissingProfileError(plugin, "runtime");
5564
5691
  }
5565
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
5566
5721
  //#region ../../packages/server/src/db/schemaEnhancements.ts
5567
5722
  const normalizeNullableString = (value) => {
5568
5723
  if (value === null || value === void 0) return null;
@@ -5799,109 +5954,148 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
5799
5954
  enabled: true,
5800
5955
  id: { gte: minBundleId }
5801
5956
  });
5802
- return {
5803
- api: {
5804
- async getBundleById(id, context) {
5805
- return getPlugin().getBundleById(id, context);
5806
- },
5807
- async getUpdateInfo(args, context) {
5808
- const directGetUpdateInfo = getPlugin().getUpdateInfo;
5809
- if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
5810
- const channel = args.channel ?? "production";
5811
- const minBundleId = args.minBundleId ?? "00000000-0000-0000-0000-000000000000";
5812
- const baseWhere = getBaseWhere({
5813
- platform: args.platform,
5814
- channel,
5815
- minBundleId
5816
- });
5817
- if (args._updateStrategy === "fingerprint") return findUpdateInfoByScanning({
5818
- args,
5819
- queryWhere: {
5820
- ...baseWhere,
5821
- fingerprintHash: args.fingerprintHash
5822
- },
5823
- context,
5824
- isCandidate: (bundle) => {
5825
- return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && bundle.fingerprintHash === args.fingerprintHash;
5826
- }
5827
- });
5828
- return findUpdateInfoByScanning({
5829
- args,
5830
- queryWhere: { ...baseWhere },
5831
- context,
5832
- isCandidate: (bundle) => {
5833
- return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && !!bundle.targetAppVersion && semverSatisfies$1(bundle.targetAppVersion, args.appVersion);
5834
- }
5835
- });
5836
- },
5837
- async getAppUpdateInfo(args, context) {
5838
- const info = await this.getUpdateInfo(args, context);
5839
- if (!info) return null;
5840
- const { storageUri, ...rest } = info;
5841
- const readStorageText = options?.readStorageText;
5842
- if (info.id === "00000000-0000-0000-0000-000000000000" || !readStorageText) {
5843
- const fileUrl = await resolveFileUrl(storageUri ?? null, context);
5844
- return {
5845
- ...rest,
5846
- fileUrl
5847
- };
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);
5848
5988
  }
5849
- const [fileUrl, targetBundle, currentBundle] = await Promise.all([
5850
- resolveFileUrl(storageUri ?? null, context),
5851
- getPlugin().getBundleById(info.id, context),
5852
- args.bundleId !== "00000000-0000-0000-0000-000000000000" ? getPlugin().getBundleById(args.bundleId, context) : null
5853
- ]);
5854
- 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 {
5855
5999
  ...rest,
5856
6000
  fileUrl
5857
6001
  };
5858
- const manifestArtifacts = await resolveManifestArtifacts({
5859
- currentBundle,
5860
- resolveFileUrl,
5861
- readStorageText,
5862
- targetBundle,
5863
- context
5864
- });
5865
- if (!manifestArtifacts) return baseResponse;
5866
- return {
5867
- ...baseResponse,
5868
- ...manifestArtifacts
5869
- };
5870
- },
5871
- async getChannels(context) {
5872
- return getPlugin().getChannels(context);
5873
- },
5874
- async getBundles(options, context) {
5875
- return getPlugin().getBundles(options, context);
5876
- },
5877
- async insertBundle(bundle, context) {
5878
- assertBundlePersistenceConstraints(bundle);
5879
- await runWithMutationPlugin(async (plugin) => {
5880
- await plugin.appendBundle(bundle, context);
5881
- await plugin.commitBundle(context);
5882
- });
5883
- },
5884
- async updateBundleById(bundleId, newBundle, context) {
5885
- await runWithMutationPlugin(async (plugin) => {
5886
- const current = await plugin.getBundleById(bundleId, context);
5887
- if (!current) throw new Error("targetBundleId not found");
5888
- assertBundlePersistenceConstraints({
5889
- ...current,
5890
- ...newBundle
5891
- });
5892
- await plugin.updateBundle(bundleId, newBundle, context);
5893
- 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
5894
6057
  });
5895
- },
5896
- async deleteBundleById(bundleId, context) {
5897
- await runWithMutationPlugin(async (plugin) => {
5898
- const bundle = await plugin.getBundleById(bundleId, context);
5899
- if (!bundle) return;
5900
- await plugin.deleteBundle(bundle, context);
5901
- 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
5902
6081
  });
6082
+ return;
5903
6083
  }
5904
- },
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,
5905
6099
  adapterName: getPlugin().name,
5906
6100
  createMigrator: () => {
5907
6101
  throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
@@ -7316,7 +7510,7 @@ function findRoute(router, method, path) {
7316
7510
  }
7317
7511
  //#endregion
7318
7512
  //#region ../../packages/server/src/version.ts
7319
- const HOT_UPDATER_SERVER_VERSION = "0.32.0";
7513
+ const HOT_UPDATER_SERVER_VERSION = "0.33.0";
7320
7514
  //#endregion
7321
7515
  //#region ../../packages/server/src/handler.ts
7322
7516
  var HandlerBadRequestError = class extends Error {
@@ -7608,7 +7802,7 @@ function createHandler(api, options = {}) {
7608
7802
  }
7609
7803
  //#endregion
7610
7804
  //#region ../../packages/server/src/route.ts
7611
- const normalizeBasePath = (basePath) => {
7805
+ const normalizeBasePath$1 = (basePath) => {
7612
7806
  if (!basePath || basePath === "/") return "/";
7613
7807
  return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
7614
7808
  };
@@ -7659,7 +7853,7 @@ const createStorageAccess = (storagePlugins) => {
7659
7853
  //#region ../../packages/server/src/runtime.ts
7660
7854
  function createHotUpdater(options) {
7661
7855
  const database = options.database;
7662
- const basePath = normalizeBasePath(options.basePath ?? "/api");
7856
+ const basePath = normalizeBasePath$1(options.basePath ?? "/api");
7663
7857
  const { readStorageText, resolveFileUrl } = createStorageAccess((options.storages ?? options.storagePlugins ?? []).map((plugin) => {
7664
7858
  const storagePlugin = typeof plugin === "function" ? plugin() : plugin;
7665
7859
  assertRuntimeStoragePlugin(storagePlugin);
@@ -7671,23 +7865,21 @@ function createHotUpdater(options) {
7671
7865
  createMutationPlugin: () => database(),
7672
7866
  readStorageText
7673
7867
  } : { readStorageText });
7674
- const api = {
7675
- ...core.api,
7676
- handler: createHandler(core.api, {
7677
- basePath,
7678
- routes: options.routes
7679
- }),
7680
- adapterName: core.adapterName
7681
- };
7868
+ const internalHandler = createHandler(core.api, {
7869
+ basePath,
7870
+ routes: options.routes
7871
+ });
7682
7872
  const handler = (request, context, ...extraArgs) => {
7683
- if (extraArgs.length > 0) return api.handler(request);
7684
- return api.handler(request, context);
7873
+ if (extraArgs.length > 0) return internalHandler(request);
7874
+ return internalHandler(request, context);
7685
7875
  };
7686
- return {
7687
- ...api,
7876
+ const api = {
7688
7877
  basePath,
7878
+ adapterName: core.adapterName,
7689
7879
  handler
7690
7880
  };
7881
+ Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
7882
+ return api;
7691
7883
  }
7692
7884
  //#endregion
7693
7885
  //#region ../../node_modules/.pnpm/hono@4.12.9/node_modules/hono/dist/compose.js
@@ -9582,6 +9774,37 @@ const streamToString = (stream) => {
9582
9774
  const DEFAULT_INVALIDATION_POLL_INTERVAL_MS = 2e3;
9583
9775
  const DEFAULT_INVALIDATION_TIMEOUT_MS = 300 * 1e3;
9584
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
+ }
9585
9808
  /**
9586
9809
  * Loads JSON data from S3.
9587
9810
  * Returns null if NoSuchKey error occurs.
@@ -9597,6 +9820,11 @@ async function loadJsonFromS3(client, bucket, key) {
9597
9820
  return JSON.parse(bodyContents);
9598
9821
  } catch (e) {
9599
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
+ });
9600
9828
  throw e;
9601
9829
  }
9602
9830
  }
@@ -9680,18 +9908,20 @@ async function invalidateCloudFront(client, distributionId, paths, options) {
9680
9908
  const s3Database = createBlobDatabasePlugin({
9681
9909
  name: "s3Database",
9682
9910
  factory: (config) => {
9683
- const { bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
9911
+ const { basePath, bucketName, cloudfrontDistributionId, apiBasePath = "/api/check-update", shouldWaitForInvalidation = false, ...s3Config } = config;
9684
9912
  const client = new _aws_sdk_client_s3.S3Client(applyS3RuntimeAwsConfig(s3Config));
9913
+ const { fromStorageKey, toStorageKey } = createDatabaseKeyBuilder(basePath);
9685
9914
  const cloudfrontClient = cloudfrontDistributionId ? new _aws_sdk_client_cloudfront.CloudFrontClient({
9686
9915
  credentials: s3Config.credentials,
9687
9916
  region: s3Config.region
9688
9917
  }) : void 0;
9689
9918
  return {
9690
9919
  apiBasePath,
9691
- listObjects: (prefix) => listObjectsInS3(client, bucketName, prefix),
9692
- loadObject: (key) => loadJsonFromS3(client, bucketName, key),
9693
- uploadObject: (key, data) => uploadJsonToS3(client, bucketName, key, data),
9694
- 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",
9695
9925
  invalidatePaths: (pathsToInvalidate) => {
9696
9926
  if (cloudfrontClient && cloudfrontDistributionId && pathsToInvalidate.length > 0) return invalidateCloudFront(cloudfrontClient, cloudfrontDistributionId, pathsToInvalidate, { shouldWaitForInvalidation });
9697
9927
  return Promise.resolve();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/aws",
3
3
  "type": "module",
4
- "version": "0.32.0",
4
+ "version": "0.33.0",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -42,10 +42,10 @@
42
42
  "es-toolkit": "^1.32.0",
43
43
  "execa": "9.5.2",
44
44
  "mime": "^4.0.4",
45
- "@hot-updater/core": "0.32.0",
46
- "@hot-updater/js": "0.32.0",
47
- "@hot-updater/mock": "0.32.0",
48
- "@hot-updater/test-utils": "0.32.0"
45
+ "@hot-updater/core": "0.33.0",
46
+ "@hot-updater/js": "0.33.0",
47
+ "@hot-updater/test-utils": "0.33.0",
48
+ "@hot-updater/mock": "0.33.0"
49
49
  },
50
50
  "dependencies": {
51
51
  "@aws-sdk/client-cloudfront": "3.1008.0",
@@ -60,9 +60,9 @@
60
60
  "@aws-sdk/lib-storage": "3.1008.0",
61
61
  "hono": "4.12.9",
62
62
  "aws-lambda": "1.0.7",
63
- "@hot-updater/cli-tools": "0.32.0",
64
- "@hot-updater/server": "0.32.0",
65
- "@hot-updater/plugin-core": "0.32.0"
63
+ "@hot-updater/cli-tools": "0.33.0",
64
+ "@hot-updater/plugin-core": "0.33.0",
65
+ "@hot-updater/server": "0.33.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsdown",