@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 +43 -5
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +43 -5
- package/dist/lambda/index.cjs +399 -169
- package/package.json +8 -8
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();
|
package/dist/lambda/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
4957
|
-
|
|
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
|
-
|
|
4962
|
-
|
|
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
|
|
5171
|
-
return (await
|
|
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(
|
|
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
|
|
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
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
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
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5387
|
-
const updatedBundles = (await
|
|
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
|
-
|
|
5393
|
-
const currentBundles = await
|
|
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 (
|
|
5404
|
-
const
|
|
5405
|
-
const
|
|
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(
|
|
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
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
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
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
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
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
return
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
}
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
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
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
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.
|
|
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
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
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
|
|
7684
|
-
return
|
|
7873
|
+
if (extraArgs.length > 0) return internalHandler(request);
|
|
7874
|
+
return internalHandler(request, context);
|
|
7685
7875
|
};
|
|
7686
|
-
|
|
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.
|
|
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.
|
|
46
|
-
"@hot-updater/js": "0.
|
|
47
|
-
"@hot-updater/
|
|
48
|
-
"@hot-updater/
|
|
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.
|
|
64
|
-
"@hot-updater/
|
|
65
|
-
"@hot-updater/
|
|
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",
|