@hot-updater/plugin-core 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.
Files changed (35) hide show
  1. package/dist/assetStorageLayout.cjs +34 -0
  2. package/dist/assetStorageLayout.d.cts +31 -0
  3. package/dist/assetStorageLayout.d.mts +31 -0
  4. package/dist/assetStorageLayout.mjs +30 -0
  5. package/dist/compressionFormat.cjs +2 -2
  6. package/dist/contentAddressedAssets.cjs +7 -0
  7. package/dist/contentAddressedAssets.d.cts +10 -0
  8. package/dist/contentAddressedAssets.d.mts +10 -0
  9. package/dist/contentAddressedAssets.mjs +7 -0
  10. package/dist/createBlobDatabasePlugin.cjs +155 -55
  11. package/dist/createBlobDatabasePlugin.d.cts +1 -0
  12. package/dist/createBlobDatabasePlugin.d.mts +1 -0
  13. package/dist/createBlobDatabasePlugin.mjs +154 -54
  14. package/dist/createDatabasePluginGetUpdateInfo.cjs +11 -3
  15. package/dist/createDatabasePluginGetUpdateInfo.mjs +11 -3
  16. package/dist/createStoragePlugin.cjs +3 -0
  17. package/dist/createStoragePlugin.mjs +3 -0
  18. package/dist/index.cjs +12 -0
  19. package/dist/index.d.cts +6 -2
  20. package/dist/index.d.mts +6 -2
  21. package/dist/index.mjs +5 -1
  22. package/dist/legacyAssetStorageLayout.cjs +12 -0
  23. package/dist/legacyAssetStorageLayout.mjs +12 -0
  24. package/dist/requestUpdateBundleState.cjs +20 -0
  25. package/dist/requestUpdateBundleState.d.cts +6 -0
  26. package/dist/requestUpdateBundleState.d.mts +6 -0
  27. package/dist/requestUpdateBundleState.mjs +19 -0
  28. package/dist/resolveUpdateInfoFromBundles.cjs +14 -0
  29. package/dist/resolveUpdateInfoFromBundles.d.cts +16 -0
  30. package/dist/resolveUpdateInfoFromBundles.d.mts +16 -0
  31. package/dist/resolveUpdateInfoFromBundles.mjs +13 -0
  32. package/dist/semverSatisfies.cjs +1 -1
  33. package/dist/types/index.d.cts +47 -1
  34. package/dist/types/index.d.mts +47 -1
  35. package/package.json +4 -4
@@ -0,0 +1,34 @@
1
+ const require_contentAddressedAssets = require("./contentAddressedAssets.cjs");
2
+ const require_legacyAssetStorageLayout = require("./legacyAssetStorageLayout.cjs");
3
+ //#region src/assetStorageLayout.ts
4
+ const createStorageUriWithRelativePath = ({ baseStorageUri, relativePath }) => {
5
+ const storageUrl = new URL(baseStorageUri);
6
+ storageUrl.pathname = `${storageUrl.pathname.replace(/\/+$/, "")}/${relativePath.replace(/\\/g, "/").split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
7
+ return storageUrl.toString();
8
+ };
9
+ const getAssetStorageLayout = (assetBaseStorageUri) => {
10
+ const pathname = new URL(assetBaseStorageUri).pathname.replace(/\/+$/, "");
11
+ return pathname.endsWith("/assets") || pathname === "/assets" ? "content-addressed" : "legacy-files";
12
+ };
13
+ const isContentAddressedAssetBaseStorageUri = (assetBaseStorageUri) => getAssetStorageLayout(assetBaseStorageUri) === "content-addressed";
14
+ const getManifestAssetStoragePath = ({ assetBaseStorageUri, assetPath, fileHash }) => {
15
+ if (getAssetStorageLayout(assetBaseStorageUri) === "content-addressed") return require_contentAddressedAssets.getContentAddressedAssetStoragePath({
16
+ assetPath,
17
+ fileHash
18
+ });
19
+ return require_legacyAssetStorageLayout.getLegacyManifestAssetStoragePath({ assetPath });
20
+ };
21
+ const resolveManifestAssetStorageUri = ({ assetBaseStorageUri, assetPath, fileHash }) => createStorageUriWithRelativePath({
22
+ baseStorageUri: assetBaseStorageUri,
23
+ relativePath: getManifestAssetStoragePath({
24
+ assetBaseStorageUri,
25
+ assetPath,
26
+ fileHash
27
+ })
28
+ });
29
+ //#endregion
30
+ exports.createStorageUriWithRelativePath = createStorageUriWithRelativePath;
31
+ exports.getAssetStorageLayout = getAssetStorageLayout;
32
+ exports.getManifestAssetStoragePath = getManifestAssetStoragePath;
33
+ exports.isContentAddressedAssetBaseStorageUri = isContentAddressedAssetBaseStorageUri;
34
+ exports.resolveManifestAssetStorageUri = resolveManifestAssetStorageUri;
@@ -0,0 +1,31 @@
1
+ //#region src/assetStorageLayout.d.ts
2
+ type AssetStorageLayout = "content-addressed" | "legacy-files";
3
+ declare const createStorageUriWithRelativePath: ({
4
+ baseStorageUri,
5
+ relativePath
6
+ }: {
7
+ baseStorageUri: string;
8
+ relativePath: string;
9
+ }) => string;
10
+ declare const getAssetStorageLayout: (assetBaseStorageUri: string) => AssetStorageLayout;
11
+ declare const isContentAddressedAssetBaseStorageUri: (assetBaseStorageUri: string) => boolean;
12
+ declare const getManifestAssetStoragePath: ({
13
+ assetBaseStorageUri,
14
+ assetPath,
15
+ fileHash
16
+ }: {
17
+ assetBaseStorageUri: string;
18
+ assetPath: string;
19
+ fileHash: string;
20
+ }) => string;
21
+ declare const resolveManifestAssetStorageUri: ({
22
+ assetBaseStorageUri,
23
+ assetPath,
24
+ fileHash
25
+ }: {
26
+ assetBaseStorageUri: string;
27
+ assetPath: string;
28
+ fileHash: string;
29
+ }) => string;
30
+ //#endregion
31
+ export { AssetStorageLayout, createStorageUriWithRelativePath, getAssetStorageLayout, getManifestAssetStoragePath, isContentAddressedAssetBaseStorageUri, resolveManifestAssetStorageUri };
@@ -0,0 +1,31 @@
1
+ //#region src/assetStorageLayout.d.ts
2
+ type AssetStorageLayout = "content-addressed" | "legacy-files";
3
+ declare const createStorageUriWithRelativePath: ({
4
+ baseStorageUri,
5
+ relativePath
6
+ }: {
7
+ baseStorageUri: string;
8
+ relativePath: string;
9
+ }) => string;
10
+ declare const getAssetStorageLayout: (assetBaseStorageUri: string) => AssetStorageLayout;
11
+ declare const isContentAddressedAssetBaseStorageUri: (assetBaseStorageUri: string) => boolean;
12
+ declare const getManifestAssetStoragePath: ({
13
+ assetBaseStorageUri,
14
+ assetPath,
15
+ fileHash
16
+ }: {
17
+ assetBaseStorageUri: string;
18
+ assetPath: string;
19
+ fileHash: string;
20
+ }) => string;
21
+ declare const resolveManifestAssetStorageUri: ({
22
+ assetBaseStorageUri,
23
+ assetPath,
24
+ fileHash
25
+ }: {
26
+ assetBaseStorageUri: string;
27
+ assetPath: string;
28
+ fileHash: string;
29
+ }) => string;
30
+ //#endregion
31
+ export { AssetStorageLayout, createStorageUriWithRelativePath, getAssetStorageLayout, getManifestAssetStoragePath, isContentAddressedAssetBaseStorageUri, resolveManifestAssetStorageUri };
@@ -0,0 +1,30 @@
1
+ import { getContentAddressedAssetStoragePath } from "./contentAddressedAssets.mjs";
2
+ import { getLegacyManifestAssetStoragePath } from "./legacyAssetStorageLayout.mjs";
3
+ //#region src/assetStorageLayout.ts
4
+ const createStorageUriWithRelativePath = ({ baseStorageUri, relativePath }) => {
5
+ const storageUrl = new URL(baseStorageUri);
6
+ storageUrl.pathname = `${storageUrl.pathname.replace(/\/+$/, "")}/${relativePath.replace(/\\/g, "/").split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
7
+ return storageUrl.toString();
8
+ };
9
+ const getAssetStorageLayout = (assetBaseStorageUri) => {
10
+ const pathname = new URL(assetBaseStorageUri).pathname.replace(/\/+$/, "");
11
+ return pathname.endsWith("/assets") || pathname === "/assets" ? "content-addressed" : "legacy-files";
12
+ };
13
+ const isContentAddressedAssetBaseStorageUri = (assetBaseStorageUri) => getAssetStorageLayout(assetBaseStorageUri) === "content-addressed";
14
+ const getManifestAssetStoragePath = ({ assetBaseStorageUri, assetPath, fileHash }) => {
15
+ if (getAssetStorageLayout(assetBaseStorageUri) === "content-addressed") return getContentAddressedAssetStoragePath({
16
+ assetPath,
17
+ fileHash
18
+ });
19
+ return getLegacyManifestAssetStoragePath({ assetPath });
20
+ };
21
+ const resolveManifestAssetStorageUri = ({ assetBaseStorageUri, assetPath, fileHash }) => createStorageUriWithRelativePath({
22
+ baseStorageUri: assetBaseStorageUri,
23
+ relativePath: getManifestAssetStoragePath({
24
+ assetBaseStorageUri,
25
+ assetPath,
26
+ fileHash
27
+ })
28
+ });
29
+ //#endregion
30
+ export { createStorageUriWithRelativePath, getAssetStorageLayout, getManifestAssetStoragePath, isContentAddressedAssetBaseStorageUri, resolveManifestAssetStorageUri };
@@ -1,8 +1,8 @@
1
1
  const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
2
  let node_path = require("node:path");
3
- node_path = require_runtime.__toESM(node_path, 1);
3
+ node_path = require_runtime.__toESM(node_path);
4
4
  let mime = require("mime");
5
- mime = require_runtime.__toESM(mime, 1);
5
+ mime = require_runtime.__toESM(mime);
6
6
  //#region src/compressionFormat.ts
7
7
  /**
8
8
  * Compression formats registry
@@ -0,0 +1,7 @@
1
+ //#region src/contentAddressedAssets.ts
2
+ const getContentAddressedAssetStoragePath = ({ assetPath, fileHash }) => {
3
+ const extension = assetPath.endsWith(".br") ? ".br" : assetPath.includes(".") ? `.${assetPath.split(".").pop()}` : "";
4
+ return `sha256/${fileHash.slice(0, 2)}/${fileHash}${extension}`;
5
+ };
6
+ //#endregion
7
+ exports.getContentAddressedAssetStoragePath = getContentAddressedAssetStoragePath;
@@ -0,0 +1,10 @@
1
+ //#region src/contentAddressedAssets.d.ts
2
+ declare const getContentAddressedAssetStoragePath: ({
3
+ assetPath,
4
+ fileHash
5
+ }: {
6
+ assetPath: string;
7
+ fileHash: string;
8
+ }) => string;
9
+ //#endregion
10
+ export { getContentAddressedAssetStoragePath };
@@ -0,0 +1,10 @@
1
+ //#region src/contentAddressedAssets.d.ts
2
+ declare const getContentAddressedAssetStoragePath: ({
3
+ assetPath,
4
+ fileHash
5
+ }: {
6
+ assetPath: string;
7
+ fileHash: string;
8
+ }) => string;
9
+ //#endregion
10
+ export { getContentAddressedAssetStoragePath };
@@ -0,0 +1,7 @@
1
+ //#region src/contentAddressedAssets.ts
2
+ const getContentAddressedAssetStoragePath = ({ assetPath, fileHash }) => {
3
+ const extension = assetPath.endsWith(".br") ? ".br" : assetPath.includes(".") ? `.${assetPath.split(".").pop()}` : "";
4
+ return `sha256/${fileHash.slice(0, 2)}/${fileHash}${extension}`;
5
+ };
6
+ //#endregion
7
+ export { getContentAddressedAssetStoragePath };
@@ -4,11 +4,29 @@ const require_createDatabasePlugin = require("./createDatabasePlugin.cjs");
4
4
  const require_filterCompatibleAppVersions = require("./filterCompatibleAppVersions.cjs");
5
5
  const require_queryBundles = require("./queryBundles.cjs");
6
6
  const require_paginateBundles = require("./paginateBundles.cjs");
7
- let _hot_updater_js = require("@hot-updater/js");
7
+ const require_resolveUpdateInfoFromBundles = require("./resolveUpdateInfoFromBundles.cjs");
8
8
  let es_toolkit = require("es-toolkit");
9
9
  let semver = require("semver");
10
- semver = require_runtime.__toESM(semver, 1);
10
+ semver = require_runtime.__toESM(semver);
11
11
  //#region src/createBlobDatabasePlugin.ts
12
+ const STORAGE_OPERATION_CONCURRENCY = 8;
13
+ async function mapWithConcurrency(items, concurrency, mapper) {
14
+ const results = [];
15
+ let nextIndex = 0;
16
+ const workerCount = Math.min(concurrency, items.length);
17
+ await Promise.all(Array.from({ length: workerCount }, async () => {
18
+ while (true) {
19
+ const index = nextIndex;
20
+ nextIndex += 1;
21
+ if (index >= items.length) break;
22
+ results[index] = await mapper(items[index], index);
23
+ }
24
+ }));
25
+ return results;
26
+ }
27
+ async function forEachWithConcurrency(items, concurrency, mapper) {
28
+ await mapWithConcurrency(items, concurrency, mapper);
29
+ }
12
30
  function removeBundleInternalKeys(bundle) {
13
31
  const { _updateJsonKey, _oldUpdateJsonKey, ...pureBundle } = bundle;
14
32
  return pureBundle;
@@ -57,6 +75,12 @@ const MANAGEMENT_INDEX_PREFIX = "_index";
57
75
  const MANAGEMENT_INDEX_VERSION = 1;
58
76
  const DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE = 128;
59
77
  const ALL_SCOPE_CACHE_KEY = "*|*";
78
+ function summarizeManagementIndexArtifacts(artifacts) {
79
+ return {
80
+ pagesWritten: artifacts.pages.size,
81
+ scopesWritten: artifacts.scopes.length
82
+ };
83
+ }
60
84
  function resolveManagementIndexPageSize(config) {
61
85
  const pageSize = config.managementIndexPageSize ?? DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE;
62
86
  if (!Number.isInteger(pageSize) || pageSize < 1) throw new Error("managementIndexPageSize must be a positive integer.");
@@ -188,11 +212,18 @@ function buildManagementIndexArtifacts(allBundles, pageSize) {
188
212
  const createBlobDatabasePlugin = ({ name, factory }) => {
189
213
  return (config, hooks) => {
190
214
  const managementIndexPageSize = resolveManagementIndexPageSize(config);
191
- const { listObjects, loadObject, uploadObject, deleteObject, invalidatePaths, apiBasePath } = factory(config);
215
+ const { listObjects, loadObject, uploadObject, deleteObject, shouldSkipLoadObjectError, invalidatePaths, apiBasePath } = factory(config);
192
216
  const bundlesMap = /* @__PURE__ */ new Map();
193
217
  const pendingBundlesMap = /* @__PURE__ */ new Map();
194
218
  const managementRootCache = /* @__PURE__ */ new Map();
195
- const PLATFORMS = ["ios", "android"];
219
+ const loadOptionalObject = async (key) => {
220
+ try {
221
+ return await loadObject(key);
222
+ } catch (error) {
223
+ if (shouldSkipLoadObjectError?.(error, key)) return null;
224
+ throw error;
225
+ }
226
+ };
196
227
  const getAllManagementArtifact = (artifacts) => {
197
228
  const allArtifact = artifacts.scopes.find((scope) => scope.cacheKey === ALL_SCOPE_CACHE_KEY);
198
229
  if (!allArtifact) throw new Error("all-bundles management index artifact not found");
@@ -209,7 +240,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
209
240
  };
210
241
  const loadStoredManagementRoot = async (scope) => {
211
242
  const cacheKey = getManagementScopeCacheKey(scope);
212
- const storedRoot = await loadObject(getManagementRootKey(scope));
243
+ const storedRoot = await loadOptionalObject(getManagementRootKey(scope));
213
244
  if (storedRoot) {
214
245
  managementRootCache.set(cacheKey, storedRoot);
215
246
  return storedRoot;
@@ -219,7 +250,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
219
250
  };
220
251
  const loadManagementPage = async (descriptor, pageCache) => {
221
252
  if (pageCache?.has(descriptor.key)) return pageCache.get(descriptor.key) ?? null;
222
- const page = await loadObject(descriptor.key);
253
+ const page = await loadOptionalObject(descriptor.key);
223
254
  pageCache?.set(descriptor.key, page);
224
255
  return page;
225
256
  };
@@ -242,13 +273,13 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
242
273
  return allBundles;
243
274
  };
244
275
  const persistManagementIndexArtifacts = async (nextArtifacts, previousArtifacts) => {
245
- for (const [key, page] of nextArtifacts.pages.entries()) await uploadObject(key, page);
246
- for (const scope of nextArtifacts.scopes) await uploadObject(scope.rootKey, scope.root);
276
+ await forEachWithConcurrency(Array.from(nextArtifacts.pages.entries()), STORAGE_OPERATION_CONCURRENCY, ([key, page]) => uploadObject(key, page));
277
+ await forEachWithConcurrency(nextArtifacts.scopes, STORAGE_OPERATION_CONCURRENCY, (scope) => uploadObject(scope.rootKey, scope.root));
247
278
  if (!previousArtifacts) return;
248
279
  const nextPageKeys = new Set(nextArtifacts.pages.keys());
249
280
  const nextRootKeys = new Set(nextArtifacts.scopes.map((scope) => scope.rootKey));
250
- for (const [key] of previousArtifacts.pages.entries()) if (!nextPageKeys.has(key)) await deleteObject(key).catch(() => {});
251
- for (const scope of previousArtifacts.scopes) if (!nextRootKeys.has(scope.rootKey)) await deleteObject(scope.rootKey).catch(() => {});
281
+ await forEachWithConcurrency(Array.from(previousArtifacts.pages.keys()).filter((key) => !nextPageKeys.has(key)), STORAGE_OPERATION_CONCURRENCY, (key) => deleteObject(key).catch(() => {}));
282
+ await forEachWithConcurrency(previousArtifacts.scopes.filter((scope) => !nextRootKeys.has(scope.rootKey)), STORAGE_OPERATION_CONCURRENCY, (scope) => deleteObject(scope.rootKey).catch(() => {}));
252
283
  };
253
284
  const ensureAllManagementRoot = async () => {
254
285
  const storedAllRoot = await loadStoredManagementRoot({});
@@ -281,6 +312,26 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
281
312
  const loadCurrentBundlesForIndexRebuild = async () => {
282
313
  return loadAllBundlesForManagementFallback();
283
314
  };
315
+ const loadBundlesFromCanonicalManifests = async () => {
316
+ return sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
317
+ };
318
+ const loadStoredBundlesForIndexRebuild = loadBundlesFromCanonicalManifests;
319
+ const loadCanonicalBundlesForIndexRepair = loadBundlesFromCanonicalManifests;
320
+ const compareBundleIndex = ({ canonicalBundles, indexedBundles, rootMissing }) => {
321
+ const canonicalIds = new Set(canonicalBundles.map((bundle) => bundle.id));
322
+ const indexedIds = new Set(indexedBundles?.map((bundle) => bundle.id) ?? []);
323
+ const missingBundleIds = Array.from(canonicalIds).filter((id) => !indexedIds.has(id)).sort((left, right) => right.localeCompare(left));
324
+ const extraBundleIds = Array.from(indexedIds).filter((id) => !canonicalIds.has(id)).sort((left, right) => right.localeCompare(left));
325
+ return {
326
+ status: missingBundleIds.length === 0 && extraBundleIds.length === 0 && !rootMissing ? "ok" : rootMissing ? "missing" : "stale",
327
+ canonicalBundles: canonicalBundles.length,
328
+ indexedBundles: indexedBundles?.length ?? 0,
329
+ missingBundles: missingBundleIds.length,
330
+ extraBundles: extraBundleIds.length,
331
+ missingBundleIds: missingBundleIds.slice(0, 20),
332
+ extraBundleIds: extraBundleIds.slice(0, 20)
333
+ };
334
+ };
284
335
  const findPageIndexContainingId = (pages, id) => {
285
336
  return pages.findIndex((page) => id.localeCompare(page.firstId) <= 0 && id.localeCompare(page.lastId) >= 0);
286
337
  };
@@ -456,16 +507,15 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
456
507
  };
457
508
  async function reloadBundles() {
458
509
  bundlesMap.clear();
459
- const filePromises = (await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)).map(async (key) => {
460
- return (await loadObject(key) ?? []).map((bundle) => ({
510
+ const allBundles = (await mapWithConcurrency((await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)), STORAGE_OPERATION_CONCURRENCY, async (key) => {
511
+ return (await loadOptionalObject(key) ?? []).map((bundle) => ({
461
512
  ...bundle,
462
513
  _updateJsonKey: key
463
514
  }));
464
- });
465
- const allBundles = (await Promise.all(filePromises)).flat();
515
+ })).flat();
466
516
  for (const bundle of allBundles) bundlesMap.set(bundle.id, bundle);
467
517
  for (const [id, bundle] of pendingBundlesMap.entries()) bundlesMap.set(id, bundle);
468
- return (0, es_toolkit.orderBy)(allBundles, [(v) => v.id], ["desc"]);
518
+ return (0, es_toolkit.orderBy)(Array.from(bundlesMap.values()), [(v) => v.id], ["desc"]);
469
519
  }
470
520
  /**
471
521
  * Updates target-app-versions.json for each channel on the given platform.
@@ -491,35 +541,44 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
491
541
  const updateKeys = keysByChannel[channel];
492
542
  const targetKey = `${channel}/${platform}/target-app-versions.json`;
493
543
  const currentVersions = updateKeys.map((key) => key.split("/")[2]);
494
- const oldTargetVersions = await loadObject(targetKey) ?? [];
544
+ const oldTargetVersions = await loadOptionalObject(targetKey) ?? [];
495
545
  const newTargetVersions = oldTargetVersions.filter((v) => currentVersions.includes(v));
496
546
  for (const v of currentVersions) if (!newTargetVersions.includes(v)) newTargetVersions.push(v);
497
547
  if (JSON.stringify(oldTargetVersions) !== JSON.stringify(newTargetVersions)) await uploadObject(targetKey, newTargetVersions);
498
548
  }
499
549
  }
500
- const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }) => {
501
- const matchingVersions = require_filterCompatibleAppVersions.filterCompatibleAppVersions(await loadObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion);
502
- return (0, _hot_updater_js.getUpdateInfo)((await Promise.allSettled(matchingVersions.map(async (targetAppVersion) => {
503
- return await loadObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`) ?? [];
504
- }))).filter((entry) => entry.status === "fulfilled").flatMap((entry) => entry.value), {
505
- _updateStrategy: "appVersion",
506
- appVersion,
507
- bundleId,
508
- channel,
509
- cohort,
510
- minBundleId,
511
- platform
550
+ const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }, context) => {
551
+ const bundles = (await mapWithConcurrency(require_filterCompatibleAppVersions.filterCompatibleAppVersions(await loadOptionalObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion), STORAGE_OPERATION_CONCURRENCY, async (targetAppVersion) => {
552
+ return await loadOptionalObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`) ?? [];
553
+ })).flat();
554
+ return require_resolveUpdateInfoFromBundles.resolveUpdateInfoFromBundles({
555
+ args: {
556
+ _updateStrategy: "appVersion",
557
+ appVersion,
558
+ bundleId,
559
+ channel,
560
+ cohort,
561
+ minBundleId,
562
+ platform
563
+ },
564
+ bundles,
565
+ context
512
566
  });
513
567
  };
514
- const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }) => {
515
- return (0, _hot_updater_js.getUpdateInfo)(await loadObject(`${channel}/${platform}/${fingerprintHash}/update.json`) ?? [], {
516
- _updateStrategy: "fingerprint",
517
- bundleId,
518
- channel,
519
- cohort,
520
- fingerprintHash,
521
- minBundleId,
522
- platform
568
+ const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }, context) => {
569
+ const bundles = await loadOptionalObject(`${channel}/${platform}/${fingerprintHash}/update.json`) ?? [];
570
+ return require_resolveUpdateInfoFromBundles.resolveUpdateInfoFromBundles({
571
+ args: {
572
+ _updateStrategy: "fingerprint",
573
+ bundleId,
574
+ channel,
575
+ cohort,
576
+ fingerprintHash,
577
+ minBundleId,
578
+ platform
579
+ },
580
+ bundles,
581
+ context
523
582
  });
524
583
  };
525
584
  const addAppVersionInvalidationPaths = (pathsToInvalidate, { platform, channel, targetAppVersion }) => {
@@ -541,7 +600,35 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
541
600
  targetAppVersion
542
601
  });
543
602
  };
544
- return require_createDatabasePlugin.createDatabasePlugin({
603
+ const bundleIndexDiagnostics = {
604
+ async check() {
605
+ const canonicalBundles = await loadCanonicalBundlesForIndexRepair();
606
+ const allRoot = await loadStoredManagementRoot({});
607
+ return compareBundleIndex({
608
+ canonicalBundles,
609
+ indexedBundles: allRoot ? await loadAllBundlesFromRoot(allRoot) : null,
610
+ rootMissing: !allRoot
611
+ });
612
+ },
613
+ async repair() {
614
+ const canonicalBundles = await loadCanonicalBundlesForIndexRepair();
615
+ const previousRoot = await loadStoredManagementRoot({});
616
+ const previousBundles = previousRoot ? await loadAllBundlesFromRoot(previousRoot) : null;
617
+ const previousArtifacts = previousRoot && previousBundles ? buildManagementIndexArtifacts(previousBundles, previousRoot.pageSize) : void 0;
618
+ const nextArtifacts = buildManagementIndexArtifacts(canonicalBundles, managementIndexPageSize);
619
+ const indexedObjectKeys = await listObjects(`${MANAGEMENT_INDEX_PREFIX}/`);
620
+ const nextObjectKeys = new Set([...nextArtifacts.pages.keys(), ...nextArtifacts.scopes.map((scope) => scope.rootKey)]);
621
+ await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
622
+ await forEachWithConcurrency(indexedObjectKeys.filter((key) => !nextObjectKeys.has(key)), STORAGE_OPERATION_CONCURRENCY, (key) => deleteObject(key).catch(() => {}));
623
+ replaceManagementRootCache(nextArtifacts);
624
+ return {
625
+ scannedBundles: canonicalBundles.length,
626
+ indexedBundles: canonicalBundles.length,
627
+ ...summarizeManagementIndexArtifacts(nextArtifacts)
628
+ };
629
+ }
630
+ };
631
+ const createPlugin = require_createDatabasePlugin.createDatabasePlugin({
545
632
  name,
546
633
  factory: () => ({
547
634
  supportsCursorPagination: true,
@@ -565,9 +652,9 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
565
652
  if (!matchedBundle) return null;
566
653
  return removeBundleInternalKeys(matchedBundle);
567
654
  },
568
- async getUpdateInfo(args) {
569
- if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args);
570
- return getFingerprintUpdateInfo(args);
655
+ async getUpdateInfo(args, context) {
656
+ if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args, context);
657
+ return getFingerprintUpdateInfo(args, context);
571
658
  },
572
659
  async getBundles(options) {
573
660
  const { where, limit, offset, orderBy, cursor } = options;
@@ -600,11 +687,8 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
600
687
  const changedBundlesByKey = {};
601
688
  const removalsByKey = {};
602
689
  const pathsToInvalidate = /* @__PURE__ */ new Set();
603
- let isTargetAppVersionChanged = false;
604
- let isChannelChanged = false;
690
+ const targetVersionPlatforms = /* @__PURE__ */ new Set();
605
691
  for (const { operation, data } of changedSets) {
606
- if (data.targetAppVersion !== void 0) isTargetAppVersionChanged = true;
607
- if (operation === "update" && data.channel !== void 0) isChannelChanged = true;
608
692
  if (operation === "insert") {
609
693
  const target = resolveStorageTarget(data);
610
694
  const key = `${data.channel}/${data.platform}/${target}/update.json`;
@@ -616,6 +700,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
616
700
  pendingBundlesMap.set(data.id, bundleWithKey);
617
701
  changedBundlesByKey[key] = changedBundlesByKey[key] || [];
618
702
  changedBundlesByKey[key].push(removeBundleInternalKeys(bundleWithKey));
703
+ if (data.targetAppVersion !== void 0) targetVersionPlatforms.add(data.platform);
619
704
  addLookupInvalidationPaths(pathsToInvalidate, data);
620
705
  continue;
621
706
  }
@@ -628,6 +713,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
628
713
  const key = bundle._updateJsonKey;
629
714
  removalsByKey[key] = removalsByKey[key] || [];
630
715
  removalsByKey[key].push(bundle.id);
716
+ if (bundle.targetAppVersion !== void 0) targetVersionPlatforms.add(bundle.platform);
631
717
  addLookupInvalidationPaths(pathsToInvalidate, bundle);
632
718
  continue;
633
719
  }
@@ -659,6 +745,10 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
659
745
  channel: nextChannel
660
746
  });
661
747
  }
748
+ if (bundle.targetAppVersion !== void 0 || updatedBundle.targetAppVersion !== void 0) {
749
+ targetVersionPlatforms.add(bundle.platform);
750
+ targetVersionPlatforms.add(updatedBundle.platform);
751
+ }
662
752
  addLookupInvalidationPaths(pathsToInvalidate, updatedBundle);
663
753
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
664
754
  continue;
@@ -672,14 +762,14 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
672
762
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
673
763
  }
674
764
  }
675
- for (const oldKey of Object.keys(removalsByKey)) await (async () => {
676
- const updatedBundles = (await loadObject(oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
765
+ await forEachWithConcurrency(Object.keys(removalsByKey), STORAGE_OPERATION_CONCURRENCY, async (oldKey) => {
766
+ const updatedBundles = (await loadOptionalObject(oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
677
767
  updatedBundles.sort((a, b) => b.id.localeCompare(a.id));
678
768
  if (updatedBundles.length === 0) await deleteObject(oldKey);
679
769
  else await uploadObject(oldKey, updatedBundles);
680
- })();
681
- for (const key of Object.keys(changedBundlesByKey)) await (async () => {
682
- const currentBundles = await loadObject(key) ?? [];
770
+ });
771
+ await forEachWithConcurrency(Object.keys(changedBundlesByKey), STORAGE_OPERATION_CONCURRENCY, async (key) => {
772
+ const currentBundles = await loadOptionalObject(key) ?? [];
683
773
  const pureBundles = changedBundlesByKey[key].map((bundle) => bundle);
684
774
  for (const changedBundle of pureBundles) {
685
775
  const index = currentBundles.findIndex((b) => b.id === changedBundle.id);
@@ -688,10 +778,11 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
688
778
  }
689
779
  currentBundles.sort((a, b) => b.id.localeCompare(a.id));
690
780
  await uploadObject(key, currentBundles);
691
- })();
692
- if (isTargetAppVersionChanged || isChannelChanged) for (const platform of PLATFORMS) await updateTargetVersionsForPlatform(platform);
693
- const currentIndexBundles = await loadCurrentBundlesForIndexRebuild();
694
- const nextIndexMap = new Map(currentIndexBundles.map((bundle) => [bundle.id, bundle]));
781
+ });
782
+ if (targetVersionPlatforms.size > 0) await Promise.all(Array.from(targetVersionPlatforms).map((platform) => updateTargetVersionsForPlatform(platform)));
783
+ const previousIndexBundles = await loadCurrentBundlesForIndexRebuild();
784
+ const storedIndexBundles = await loadStoredBundlesForIndexRebuild();
785
+ const nextIndexMap = new Map(storedIndexBundles.map((bundle) => [bundle.id, bundle]));
695
786
  for (const { operation, data } of changedSets) {
696
787
  if (operation === "delete") {
697
788
  nextIndexMap.delete(data.id);
@@ -700,7 +791,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
700
791
  nextIndexMap.set(data.id, data);
701
792
  }
702
793
  const nextIndexBundles = sortManagedBundles(Array.from(nextIndexMap.values()));
703
- const previousArtifacts = buildManagementIndexArtifacts(currentIndexBundles, managementIndexPageSize);
794
+ const previousArtifacts = buildManagementIndexArtifacts(previousIndexBundles, managementIndexPageSize);
704
795
  const nextArtifacts = buildManagementIndexArtifacts(nextIndexBundles, managementIndexPageSize);
705
796
  await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
706
797
  replaceManagementRootCache(nextArtifacts);
@@ -711,6 +802,15 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
711
802
  }
712
803
  })
713
804
  })({}, hooks);
805
+ return () => {
806
+ const plugin = createPlugin();
807
+ Object.defineProperty(plugin, "diagnostics", {
808
+ configurable: true,
809
+ enumerable: true,
810
+ value: { bundleIndex: bundleIndexDiagnostics }
811
+ });
812
+ return plugin;
813
+ };
714
814
  };
715
815
  };
716
816
  //#endregion
@@ -9,6 +9,7 @@ interface BlobOperations {
9
9
  loadObject: <T>(key: string) => Promise<T | null>;
10
10
  uploadObject: <T>(key: string, data: T) => Promise<void>;
11
11
  deleteObject: (key: string) => Promise<void>;
12
+ shouldSkipLoadObjectError?: (error: unknown, key: string) => boolean;
12
13
  invalidatePaths: (paths: string[]) => Promise<void>;
13
14
  apiBasePath: string;
14
15
  }
@@ -9,6 +9,7 @@ interface BlobOperations {
9
9
  loadObject: <T>(key: string) => Promise<T | null>;
10
10
  uploadObject: <T>(key: string, data: T) => Promise<void>;
11
11
  deleteObject: (key: string) => Promise<void>;
12
+ shouldSkipLoadObjectError?: (error: unknown, key: string) => boolean;
12
13
  invalidatePaths: (paths: string[]) => Promise<void>;
13
14
  apiBasePath: string;
14
15
  }