@hot-updater/aws 0.29.5 → 0.29.7

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.
@@ -22,8 +22,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  enumerable: true
23
23
  }) : target, mod));
24
24
  //#endregion
25
- let fs = require("fs");
26
- fs = __toESM(fs);
27
25
  let _aws_sdk_credential_providers = require("@aws-sdk/credential-providers");
28
26
  let _hot_updater_cli_tools = require("@hot-updater/cli-tools");
29
27
  let node_url = require("node:url");
@@ -826,7 +824,7 @@ const handleCommand = (filePath, rawArguments, rawOptions) => {
826
824
  var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
827
825
  module.exports = isexe;
828
826
  isexe.sync = sync;
829
- var fs$5 = require("fs");
827
+ var fs$3 = require("fs");
830
828
  function checkPathExt(path, options) {
831
829
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
832
830
  if (!pathext) return true;
@@ -843,12 +841,12 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
843
841
  return checkPathExt(path, options);
844
842
  }
845
843
  function isexe(path, options, cb) {
846
- fs$5.stat(path, function(er, stat) {
844
+ fs$3.stat(path, function(er, stat) {
847
845
  cb(er, er ? false : checkStat(stat, path, options));
848
846
  });
849
847
  }
850
848
  function sync(path, options) {
851
- return checkStat(fs$5.statSync(path), path, options);
849
+ return checkStat(fs$3.statSync(path), path, options);
852
850
  }
853
851
  }));
854
852
  //#endregion
@@ -856,14 +854,14 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
856
854
  var require_mode = /* @__PURE__ */ __commonJSMin(((exports, module) => {
857
855
  module.exports = isexe;
858
856
  isexe.sync = sync;
859
- var fs$4 = require("fs");
857
+ var fs$2 = require("fs");
860
858
  function isexe(path, options, cb) {
861
- fs$4.stat(path, function(er, stat) {
859
+ fs$2.stat(path, function(er, stat) {
862
860
  cb(er, er ? false : checkStat(stat, options));
863
861
  });
864
862
  }
865
863
  function sync(path, options) {
866
- return checkStat(fs$4.statSync(path), options);
864
+ return checkStat(fs$2.statSync(path), options);
867
865
  }
868
866
  function checkStat(stat, options) {
869
867
  return stat.isFile() && checkMode(stat, options);
@@ -1078,16 +1076,16 @@ var require_shebang_command = /* @__PURE__ */ __commonJSMin(((exports, module) =
1078
1076
  //#endregion
1079
1077
  //#region ../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/readShebang.js
1080
1078
  var require_readShebang = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1081
- const fs$3 = require("fs");
1079
+ const fs$1 = require("fs");
1082
1080
  const shebangCommand = require_shebang_command();
1083
1081
  function readShebang(command) {
1084
1082
  const size = 150;
1085
1083
  const buffer = Buffer.alloc(size);
1086
1084
  let fd;
1087
1085
  try {
1088
- fd = fs$3.openSync(command, "r");
1089
- fs$3.readSync(fd, buffer, 0, size, 0);
1090
- fs$3.closeSync(fd);
1086
+ fd = fs$1.openSync(command, "r");
1087
+ fs$1.readSync(fd, buffer, 0, size, 0);
1088
+ fs$1.closeSync(fd);
1091
1089
  } catch (e) {}
1092
1090
  return shebangCommand(buffer.toString());
1093
1091
  }
@@ -7327,7 +7325,7 @@ var SSMKeyPairManager = class {
7327
7325
  };
7328
7326
  //#endregion
7329
7327
  //#region iac/templates.ts
7330
- const getConfigTemplate = (build, { profile }) => {
7328
+ const getConfigScaffold = (build, { profile }) => {
7331
7329
  const storageConfig = {
7332
7330
  imports: [{
7333
7331
  pkg: "@hot-updater/aws",
@@ -7345,14 +7343,21 @@ const getConfigTemplate = (build, { profile }) => {
7345
7343
  cloudfrontDistributionId: process.env.HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID!,
7346
7344
  })`
7347
7345
  };
7348
- let intermediate = "";
7349
- if (profile) intermediate = `
7346
+ let helperStatements = [];
7347
+ if (profile) helperStatements = [{
7348
+ name: "commonOptions",
7349
+ strategy: "merge-object",
7350
+ code: `
7350
7351
  const commonOptions = {
7351
7352
  bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
7352
7353
  region: process.env.HOT_UPDATER_S3_REGION!,
7353
7354
  credentials: fromSSO({ profile: process.env.HOT_UPDATER_AWS_PROFILE! }),
7354
- };`.trim();
7355
- else intermediate = `
7355
+ };`.trim()
7356
+ }];
7357
+ else helperStatements = [{
7358
+ name: "commonOptions",
7359
+ strategy: "merge-object",
7360
+ code: `
7356
7361
  const commonOptions = {
7357
7362
  bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
7358
7363
  region: process.env.HOT_UPDATER_S3_REGION!,
@@ -7360,13 +7365,14 @@ const commonOptions = {
7360
7365
  accessKeyId: process.env.HOT_UPDATER_S3_ACCESS_KEY_ID!,
7361
7366
  secretAccessKey: process.env.HOT_UPDATER_S3_SECRET_ACCESS_KEY!,
7362
7367
  },
7363
- };`.trim();
7364
- const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig);
7368
+ };`.trim()
7369
+ }];
7370
+ const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setIntermediateCode(helperStatements.map((statement) => statement.code.trim()).join("\n\n"));
7365
7371
  if (profile) builder.addImport({
7366
7372
  pkg: "@aws-sdk/credential-provider-sso",
7367
7373
  named: ["fromSSO"]
7368
7374
  });
7369
- return builder.setIntermediateCode(intermediate).getResult();
7375
+ return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(builder, { helperStatements });
7370
7376
  };
7371
7377
  const SOURCE_TEMPLATE = `// Add this to your App.tsx
7372
7378
  import { HotUpdater } from "@hot-updater/react-native";
@@ -7536,7 +7542,7 @@ const runInit = async ({ build }) => {
7536
7542
  distributionId,
7537
7543
  accountId
7538
7544
  });
7539
- await fs.default.promises.writeFile("hot-updater.config.ts", getConfigTemplate(build, { profile: ssoProfile }));
7545
+ const configWriteResult = await (0, _hot_updater_cli_tools.writeHotUpdaterConfig)(getConfigScaffold(build, { profile: ssoProfile }));
7540
7546
  await (0, _hot_updater_cli_tools.makeEnv)({
7541
7547
  HOT_UPDATER_S3_BUCKET_NAME: bucketName,
7542
7548
  HOT_UPDATER_S3_REGION: bucketRegion,
@@ -7555,7 +7561,9 @@ const runInit = async ({ build }) => {
7555
7561
  });
7556
7562
  if (mode === "sso") await (0, _hot_updater_cli_tools.ensureInstallPackages)({ devDependencies: ["@aws-sdk/credential-provider-sso"] });
7557
7563
  _hot_updater_cli_tools.p.log.success("Generated '.env.hotupdater' file with AWS settings.");
7558
- _hot_updater_cli_tools.p.log.success("Generated 'hot-updater.config.ts' file with AWS settings.");
7564
+ if (configWriteResult.status === "created") _hot_updater_cli_tools.p.log.success("Generated 'hot-updater.config.ts' file with AWS settings.");
7565
+ else if (configWriteResult.status === "merged") _hot_updater_cli_tools.p.log.success("Updated 'hot-updater.config.ts' file with AWS settings.");
7566
+ else _hot_updater_cli_tools.p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
7559
7567
  const sourceUrl = `https://${distributionDomain}/api/check-update`;
7560
7568
  _hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.transformTemplate)(SOURCE_TEMPLATE, { source: sourceUrl }));
7561
7569
  _hot_updater_cli_tools.p.log.message(`Next step: ${(0, _hot_updater_cli_tools.link)("https://hot-updater.dev/docs/managed/aws#step-4-changeenv-file-optional")}`);
@@ -1,7 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
- import fs from "fs";
3
2
  import { fromSSO } from "@aws-sdk/credential-providers";
4
- import { ConfigBuilder, colors, copyDirToTmp, createZip, ensureInstallPackages, getCwd, link, makeEnv, p, transformEnv, transformTemplate } from "@hot-updater/cli-tools";
3
+ import { ConfigBuilder, colors, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, getCwd, link, makeEnv, p, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
7
6
  import { StringDecoder } from "node:string_decoder";
@@ -22,7 +21,7 @@ import crypto from "crypto";
22
21
  import { CloudFront } from "@aws-sdk/client-cloudfront";
23
22
  import { IAM } from "@aws-sdk/client-iam";
24
23
  import { STS } from "@aws-sdk/client-sts";
25
- import fs$1 from "fs/promises";
24
+ import fs from "fs/promises";
26
25
  import { Lambda } from "@aws-sdk/client-lambda";
27
26
  import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, S3 } from "@aws-sdk/client-s3";
28
27
  import { Upload } from "@aws-sdk/lib-storage";
@@ -820,7 +819,7 @@ const handleCommand = (filePath, rawArguments, rawOptions) => {
820
819
  var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
821
820
  module.exports = isexe;
822
821
  isexe.sync = sync;
823
- var fs$4 = __require("fs");
822
+ var fs$3 = __require("fs");
824
823
  function checkPathExt(path, options) {
825
824
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
826
825
  if (!pathext) return true;
@@ -837,12 +836,12 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
837
836
  return checkPathExt(path, options);
838
837
  }
839
838
  function isexe(path, options, cb) {
840
- fs$4.stat(path, function(er, stat) {
839
+ fs$3.stat(path, function(er, stat) {
841
840
  cb(er, er ? false : checkStat(stat, path, options));
842
841
  });
843
842
  }
844
843
  function sync(path, options) {
845
- return checkStat(fs$4.statSync(path), path, options);
844
+ return checkStat(fs$3.statSync(path), path, options);
846
845
  }
847
846
  }));
848
847
  //#endregion
@@ -850,14 +849,14 @@ var require_windows = /* @__PURE__ */ __commonJSMin(((exports, module) => {
850
849
  var require_mode = /* @__PURE__ */ __commonJSMin(((exports, module) => {
851
850
  module.exports = isexe;
852
851
  isexe.sync = sync;
853
- var fs$3 = __require("fs");
852
+ var fs$2 = __require("fs");
854
853
  function isexe(path, options, cb) {
855
- fs$3.stat(path, function(er, stat) {
854
+ fs$2.stat(path, function(er, stat) {
856
855
  cb(er, er ? false : checkStat(stat, options));
857
856
  });
858
857
  }
859
858
  function sync(path, options) {
860
- return checkStat(fs$3.statSync(path), options);
859
+ return checkStat(fs$2.statSync(path), options);
861
860
  }
862
861
  function checkStat(stat, options) {
863
862
  return stat.isFile() && checkMode(stat, options);
@@ -1072,16 +1071,16 @@ var require_shebang_command = /* @__PURE__ */ __commonJSMin(((exports, module) =
1072
1071
  //#endregion
1073
1072
  //#region ../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/readShebang.js
1074
1073
  var require_readShebang = /* @__PURE__ */ __commonJSMin(((exports, module) => {
1075
- const fs$2 = __require("fs");
1074
+ const fs$1 = __require("fs");
1076
1075
  const shebangCommand = require_shebang_command();
1077
1076
  function readShebang(command) {
1078
1077
  const size = 150;
1079
1078
  const buffer = Buffer.alloc(size);
1080
1079
  let fd;
1081
1080
  try {
1082
- fd = fs$2.openSync(command, "r");
1083
- fs$2.readSync(fd, buffer, 0, size, 0);
1084
- fs$2.closeSync(fd);
1081
+ fd = fs$1.openSync(command, "r");
1082
+ fs$1.readSync(fd, buffer, 0, size, 0);
1083
+ fs$1.closeSync(fd);
1085
1084
  } catch (e) {}
1086
1085
  return shebangCommand(buffer.toString());
1087
1086
  }
@@ -6750,7 +6749,7 @@ var LambdaEdgeDeployer = class {
6750
6749
  SSM_REGION: config.ssmRegion,
6751
6750
  S3_BUCKET_NAME: config.bucketName
6752
6751
  });
6753
- await fs$1.writeFile(indexPath, code);
6752
+ await fs.writeFile(indexPath, code);
6754
6753
  const lambdaClient = new Lambda({
6755
6754
  region: "us-east-1",
6756
6755
  credentials: this.credentials
@@ -6784,7 +6783,7 @@ var LambdaEdgeDeployer = class {
6784
6783
  Runtime: "nodejs22.x",
6785
6784
  Role: lambdaRoleArn,
6786
6785
  Handler: "index.handler",
6787
- Code: { ZipFile: await fs$1.readFile(zipFilePath) },
6786
+ Code: { ZipFile: await fs.readFile(zipFilePath) },
6788
6787
  Description: "Hot Updater Lambda@Edge function",
6789
6788
  Publish: true,
6790
6789
  Timeout: 10
@@ -6797,7 +6796,7 @@ var LambdaEdgeDeployer = class {
6797
6796
  message(`Function "${lambdaName}" already exists. Updating function code...`);
6798
6797
  const updateResp = await lambdaClient.updateFunctionCode({
6799
6798
  FunctionName: lambdaName,
6800
- ZipFile: await fs$1.readFile(zipFilePath),
6799
+ ZipFile: await fs.readFile(zipFilePath),
6801
6800
  Publish: true
6802
6801
  });
6803
6802
  message("Waiting for Lambda function update to complete...");
@@ -6829,7 +6828,7 @@ var LambdaEdgeDeployer = class {
6829
6828
  return `Updated Lambda "${lambdaName}" function`;
6830
6829
  } finally {
6831
6830
  removeTmpDir();
6832
- fs$1.rm(zipFilePath, { force: true });
6831
+ fs.rm(zipFilePath, { force: true });
6833
6832
  }
6834
6833
  }
6835
6834
  },
@@ -7321,7 +7320,7 @@ var SSMKeyPairManager = class {
7321
7320
  };
7322
7321
  //#endregion
7323
7322
  //#region iac/templates.ts
7324
- const getConfigTemplate = (build, { profile }) => {
7323
+ const getConfigScaffold = (build, { profile }) => {
7325
7324
  const storageConfig = {
7326
7325
  imports: [{
7327
7326
  pkg: "@hot-updater/aws",
@@ -7339,14 +7338,21 @@ const getConfigTemplate = (build, { profile }) => {
7339
7338
  cloudfrontDistributionId: process.env.HOT_UPDATER_CLOUDFRONT_DISTRIBUTION_ID!,
7340
7339
  })`
7341
7340
  };
7342
- let intermediate = "";
7343
- if (profile) intermediate = `
7341
+ let helperStatements = [];
7342
+ if (profile) helperStatements = [{
7343
+ name: "commonOptions",
7344
+ strategy: "merge-object",
7345
+ code: `
7344
7346
  const commonOptions = {
7345
7347
  bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
7346
7348
  region: process.env.HOT_UPDATER_S3_REGION!,
7347
7349
  credentials: fromSSO({ profile: process.env.HOT_UPDATER_AWS_PROFILE! }),
7348
- };`.trim();
7349
- else intermediate = `
7350
+ };`.trim()
7351
+ }];
7352
+ else helperStatements = [{
7353
+ name: "commonOptions",
7354
+ strategy: "merge-object",
7355
+ code: `
7350
7356
  const commonOptions = {
7351
7357
  bucketName: process.env.HOT_UPDATER_S3_BUCKET_NAME!,
7352
7358
  region: process.env.HOT_UPDATER_S3_REGION!,
@@ -7354,13 +7360,14 @@ const commonOptions = {
7354
7360
  accessKeyId: process.env.HOT_UPDATER_S3_ACCESS_KEY_ID!,
7355
7361
  secretAccessKey: process.env.HOT_UPDATER_S3_SECRET_ACCESS_KEY!,
7356
7362
  },
7357
- };`.trim();
7358
- const builder = new ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig);
7363
+ };`.trim()
7364
+ }];
7365
+ const builder = new ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setIntermediateCode(helperStatements.map((statement) => statement.code.trim()).join("\n\n"));
7359
7366
  if (profile) builder.addImport({
7360
7367
  pkg: "@aws-sdk/credential-provider-sso",
7361
7368
  named: ["fromSSO"]
7362
7369
  });
7363
- return builder.setIntermediateCode(intermediate).getResult();
7370
+ return createHotUpdaterConfigScaffoldFromBuilder(builder, { helperStatements });
7364
7371
  };
7365
7372
  const SOURCE_TEMPLATE = `// Add this to your App.tsx
7366
7373
  import { HotUpdater } from "@hot-updater/react-native";
@@ -7530,7 +7537,7 @@ const runInit = async ({ build }) => {
7530
7537
  distributionId,
7531
7538
  accountId
7532
7539
  });
7533
- await fs.promises.writeFile("hot-updater.config.ts", getConfigTemplate(build, { profile: ssoProfile }));
7540
+ const configWriteResult = await writeHotUpdaterConfig(getConfigScaffold(build, { profile: ssoProfile }));
7534
7541
  await makeEnv({
7535
7542
  HOT_UPDATER_S3_BUCKET_NAME: bucketName,
7536
7543
  HOT_UPDATER_S3_REGION: bucketRegion,
@@ -7549,7 +7556,9 @@ const runInit = async ({ build }) => {
7549
7556
  });
7550
7557
  if (mode === "sso") await ensureInstallPackages({ devDependencies: ["@aws-sdk/credential-provider-sso"] });
7551
7558
  p.log.success("Generated '.env.hotupdater' file with AWS settings.");
7552
- p.log.success("Generated 'hot-updater.config.ts' file with AWS settings.");
7559
+ if (configWriteResult.status === "created") p.log.success("Generated 'hot-updater.config.ts' file with AWS settings.");
7560
+ else if (configWriteResult.status === "merged") p.log.success("Updated 'hot-updater.config.ts' file with AWS settings.");
7561
+ else p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
7553
7562
  const sourceUrl = `https://${distributionDomain}/api/check-update`;
7554
7563
  p.note(transformTemplate(SOURCE_TEMPLATE, { source: sourceUrl }));
7555
7564
  p.log.message(`Next step: ${link("https://hot-updater.dev/docs/managed/aws#step-4-changeenv-file-optional")}`);
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as _$_hot_updater_plugin_core0 from "@hot-updater/plugin-core";
2
- import { StoragePlugin, StoragePluginHooks, StorageResolveContext } from "@hot-updater/plugin-core";
2
+ import { BlobDatabasePluginConfig, StoragePlugin, StoragePluginHooks, StorageResolveContext } from "@hot-updater/plugin-core";
3
3
  import { S3ClientConfig } from "@aws-sdk/client-s3";
4
4
 
5
5
  //#region src/s3Database.d.ts
6
- interface S3DatabaseConfig extends S3ClientConfig {
6
+ interface S3DatabaseConfig extends S3ClientConfig, BlobDatabasePluginConfig {
7
7
  bucketName: string;
8
8
  /**
9
9
  * CloudFront distribution ID used for cache invalidation.
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { S3ClientConfig } from "@aws-sdk/client-s3";
2
2
  import * as _$_hot_updater_plugin_core0 from "@hot-updater/plugin-core";
3
- import { StoragePlugin, StoragePluginHooks, StorageResolveContext } from "@hot-updater/plugin-core";
3
+ import { BlobDatabasePluginConfig, StoragePlugin, StoragePluginHooks, StorageResolveContext } from "@hot-updater/plugin-core";
4
4
 
5
5
  //#region src/s3Database.d.ts
6
- interface S3DatabaseConfig extends S3ClientConfig {
6
+ interface S3DatabaseConfig extends S3ClientConfig, BlobDatabasePluginConfig {
7
7
  bucketName: string;
8
8
  /**
9
9
  * CloudFront distribution ID used for cache invalidation.
@@ -1577,11 +1577,71 @@ function mergeWith(target, source, merge) {
1577
1577
  //#endregion
1578
1578
  //#region ../plugin-core/dist/createDatabasePlugin.mjs
1579
1579
  const REPLACE_ON_UPDATE_KEYS = ["targetCohorts"];
1580
+ const DEFAULT_DESC_ORDER$1 = {
1581
+ field: "id",
1582
+ direction: "desc"
1583
+ };
1584
+ function normalizePage(value) {
1585
+ if (!Number.isInteger(value) || value === void 0 || value < 1) return;
1586
+ return value;
1587
+ }
1580
1588
  function mergeBundleUpdate(baseBundle, patch) {
1581
1589
  return mergeWith(baseBundle, patch, (_targetValue, sourceValue, key) => {
1582
1590
  if (REPLACE_ON_UPDATE_KEYS.includes(key)) return sourceValue;
1583
1591
  });
1584
1592
  }
1593
+ function mergeIdFilter(base, patch) {
1594
+ return {
1595
+ ...base,
1596
+ ...patch
1597
+ };
1598
+ }
1599
+ function mergeWhereWithIdFilter(where, idFilter) {
1600
+ return {
1601
+ ...where,
1602
+ id: mergeIdFilter(where?.id, idFilter)
1603
+ };
1604
+ }
1605
+ function buildCursorPageQuery(where, cursor, orderBy) {
1606
+ const direction = orderBy.direction;
1607
+ if (cursor.after) return {
1608
+ reverseData: false,
1609
+ where: mergeWhereWithIdFilter(where, { [direction === "desc" ? "lt" : "gt"]: cursor.after }),
1610
+ orderBy
1611
+ };
1612
+ if (cursor.before) return {
1613
+ reverseData: true,
1614
+ where: mergeWhereWithIdFilter(where, { [direction === "desc" ? "gt" : "lt"]: cursor.before }),
1615
+ orderBy: {
1616
+ field: orderBy.field,
1617
+ direction: direction === "desc" ? "asc" : "desc"
1618
+ }
1619
+ };
1620
+ return {
1621
+ reverseData: false,
1622
+ where: where ?? {},
1623
+ orderBy
1624
+ };
1625
+ }
1626
+ function buildCountBeforeWhere(where, firstBundleId, orderBy) {
1627
+ return mergeWhereWithIdFilter(where, { [orderBy.direction === "desc" ? "gt" : "lt"]: firstBundleId });
1628
+ }
1629
+ function createPaginatedResult(total, limit, startIndex, data) {
1630
+ const pagination = calculatePagination(total, {
1631
+ limit,
1632
+ offset: startIndex
1633
+ });
1634
+ const nextCursor = data.length > 0 && startIndex + data.length < total ? data.at(-1)?.id : void 0;
1635
+ const previousCursor = data.length > 0 && startIndex > 0 ? data[0]?.id : void 0;
1636
+ return {
1637
+ data,
1638
+ pagination: {
1639
+ ...pagination,
1640
+ ...nextCursor ? { nextCursor } : {},
1641
+ ...previousCursor ? { previousCursor } : {}
1642
+ }
1643
+ };
1644
+ }
1585
1645
  /**
1586
1646
  * Creates a database plugin with lazy initialization and automatic hook execution.
1587
1647
  *
@@ -1623,6 +1683,59 @@ function createDatabasePlugin(options) {
1623
1683
  data
1624
1684
  });
1625
1685
  };
1686
+ const runGetBundles = async (options, context) => {
1687
+ if (context === void 0) return getMethods().getBundles(options);
1688
+ return getMethods().getBundles(options, context);
1689
+ };
1690
+ const getBundlesWithLegacyCursorFallback = async (options, context) => {
1691
+ const orderBy = options.orderBy ?? DEFAULT_DESC_ORDER$1;
1692
+ const baseWhere = options.where;
1693
+ const total = (await runGetBundles({
1694
+ where: baseWhere,
1695
+ limit: 1,
1696
+ offset: 0,
1697
+ orderBy
1698
+ }, context)).pagination.total;
1699
+ if (!options.cursor?.after && !options.cursor?.before) {
1700
+ const firstPage = await runGetBundles({
1701
+ where: baseWhere,
1702
+ limit: options.limit,
1703
+ offset: 0,
1704
+ orderBy
1705
+ }, context);
1706
+ return createPaginatedResult(total, options.limit, 0, firstPage.data);
1707
+ }
1708
+ const { where, orderBy: queryOrderBy, reverseData } = buildCursorPageQuery(baseWhere, options.cursor, orderBy);
1709
+ const cursorPage = await runGetBundles({
1710
+ where,
1711
+ limit: options.limit,
1712
+ offset: 0,
1713
+ orderBy: queryOrderBy
1714
+ }, context);
1715
+ const data = reverseData ? cursorPage.data.slice().reverse() : cursorPage.data;
1716
+ if (data.length === 0) {
1717
+ const emptyStartIndex = options.cursor.after ? total : 0;
1718
+ return {
1719
+ data,
1720
+ pagination: {
1721
+ ...calculatePagination(total, {
1722
+ limit: options.limit,
1723
+ offset: emptyStartIndex
1724
+ }),
1725
+ ...options.cursor.after ? { previousCursor: options.cursor.after } : {},
1726
+ ...options.cursor.before ? { nextCursor: options.cursor.before } : {}
1727
+ }
1728
+ };
1729
+ }
1730
+ const firstBundleId = data[0].id;
1731
+ const countBeforeResult = await runGetBundles({
1732
+ where: buildCountBeforeWhere(baseWhere, firstBundleId, orderBy),
1733
+ limit: 1,
1734
+ offset: 0,
1735
+ orderBy
1736
+ }, context);
1737
+ return createPaginatedResult(total, options.limit, countBeforeResult.pagination.total, data);
1738
+ };
1626
1739
  const plugin = {
1627
1740
  name: options.name,
1628
1741
  async getBundleById(bundleId, context) {
@@ -1630,8 +1743,35 @@ function createDatabasePlugin(options) {
1630
1743
  return getMethods().getBundleById(bundleId, context);
1631
1744
  },
1632
1745
  async getBundles(options, context) {
1633
- if (context === void 0) return getMethods().getBundles(options);
1634
- return getMethods().getBundles(options, context);
1746
+ if (typeof options === "object" && options !== null && "offset" in options && options.offset !== void 0) throw new Error("Bundle offset pagination has been removed. Use cursor.after or cursor.before instead.");
1747
+ const methods = getMethods();
1748
+ const normalizedOptions = {
1749
+ ...options,
1750
+ page: normalizePage(options.page),
1751
+ orderBy: options.orderBy ?? DEFAULT_DESC_ORDER$1
1752
+ };
1753
+ if (normalizedOptions.page !== void 0) {
1754
+ const { page, ...pageOptions } = normalizedOptions;
1755
+ const requestedOffset = (page - 1) * normalizedOptions.limit;
1756
+ let pageResult = await runGetBundles({
1757
+ ...pageOptions,
1758
+ offset: requestedOffset
1759
+ }, context);
1760
+ const total = pageResult.pagination.total;
1761
+ const totalPages = total === 0 ? 0 : Math.ceil(total / normalizedOptions.limit);
1762
+ const maxOffset = totalPages === 0 ? 0 : (Math.max(1, totalPages) - 1) * normalizedOptions.limit;
1763
+ const resolvedOffset = Math.min(requestedOffset, maxOffset);
1764
+ if (resolvedOffset !== requestedOffset) pageResult = await runGetBundles({
1765
+ ...pageOptions,
1766
+ offset: resolvedOffset
1767
+ }, context);
1768
+ return createPaginatedResult(total, normalizedOptions.limit, resolvedOffset, pageResult.data);
1769
+ }
1770
+ if (methods.supportsCursorPagination) {
1771
+ if (context === void 0) return methods.getBundles(normalizedOptions);
1772
+ return methods.getBundles(normalizedOptions, context);
1773
+ }
1774
+ return getBundlesWithLegacyCursorFallback(normalizedOptions, context);
1635
1775
  },
1636
1776
  async getChannels(context) {
1637
1777
  if (context === void 0) return getMethods().getChannels();
@@ -3084,6 +3224,56 @@ function sortBundles$1(bundles, orderBy) {
3084
3224
  });
3085
3225
  }
3086
3226
  //#endregion
3227
+ //#region ../plugin-core/dist/paginateBundles.mjs
3228
+ function paginateBundles({ bundles, limit, offset, cursor, orderBy }) {
3229
+ const sortedBundles = sortBundles$1(bundles, orderBy);
3230
+ const direction = orderBy?.direction ?? "desc";
3231
+ const total = sortedBundles.length;
3232
+ if (offset !== void 0) {
3233
+ const normalizedOffset = Math.max(0, offset);
3234
+ const data = limit > 0 ? sortedBundles.slice(normalizedOffset, normalizedOffset + limit) : sortedBundles.slice(normalizedOffset);
3235
+ const pagination = calculatePagination(total, {
3236
+ limit,
3237
+ offset: normalizedOffset
3238
+ });
3239
+ const nextCursor = data.length > 0 && normalizedOffset + data.length < total ? data.at(-1)?.id : void 0;
3240
+ const previousCursor = data.length > 0 && normalizedOffset > 0 ? data[0]?.id : void 0;
3241
+ return {
3242
+ data,
3243
+ pagination: {
3244
+ ...pagination,
3245
+ ...nextCursor ? { nextCursor } : {},
3246
+ ...previousCursor ? { previousCursor } : {}
3247
+ }
3248
+ };
3249
+ }
3250
+ let data;
3251
+ if (cursor?.after) {
3252
+ const candidates = sortedBundles.filter((bundle) => direction === "desc" ? bundle.id.localeCompare(cursor.after) < 0 : bundle.id.localeCompare(cursor.after) > 0);
3253
+ data = limit > 0 ? candidates.slice(0, limit) : candidates;
3254
+ } else if (cursor?.before) {
3255
+ const candidates = sortedBundles.filter((bundle) => direction === "desc" ? bundle.id.localeCompare(cursor.before) > 0 : bundle.id.localeCompare(cursor.before) < 0);
3256
+ data = limit > 0 ? candidates.slice(Math.max(0, candidates.length - limit)) : candidates;
3257
+ } else data = limit > 0 ? sortedBundles.slice(0, limit) : sortedBundles;
3258
+ const startIndex = data.length > 0 ? sortedBundles.findIndex((bundle) => bundle.id === data[0].id) : cursor?.after ? total : 0;
3259
+ const pagination = calculatePagination(total, {
3260
+ limit,
3261
+ offset: startIndex
3262
+ });
3263
+ const nextCursor = data.length > 0 && startIndex + data.length < total ? data.at(-1)?.id : void 0;
3264
+ const previousCursor = data.length > 0 && startIndex > 0 ? data[0]?.id : void 0;
3265
+ return {
3266
+ data,
3267
+ pagination: {
3268
+ ...pagination,
3269
+ ...nextCursor ? { nextCursor } : {},
3270
+ ...previousCursor ? { previousCursor } : {},
3271
+ ...data.length === 0 && cursor?.after ? { previousCursor: cursor.after } : {},
3272
+ ...data.length === 0 && cursor?.before ? { nextCursor: cursor.before } : {}
3273
+ }
3274
+ };
3275
+ }
3276
+ //#endregion
3087
3277
  //#region ../js/dist/index.mjs
3088
3278
  var __create = Object.create;
3089
3279
  var __defProp = Object.defineProperty;
@@ -4490,6 +4680,135 @@ function resolveStorageTarget({ targetAppVersion, fingerprintHash }) {
4490
4680
  if (!target) throw new Error("target not found");
4491
4681
  return target;
4492
4682
  }
4683
+ const DEFAULT_DESC_ORDER = {
4684
+ field: "id",
4685
+ direction: "desc"
4686
+ };
4687
+ const MANAGEMENT_INDEX_PREFIX = "_index";
4688
+ const MANAGEMENT_INDEX_VERSION = 1;
4689
+ const DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE = 128;
4690
+ const ALL_SCOPE_CACHE_KEY = "*|*";
4691
+ function resolveManagementIndexPageSize(config) {
4692
+ const pageSize = config.managementIndexPageSize ?? DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE;
4693
+ if (!Number.isInteger(pageSize) || pageSize < 1) throw new Error("managementIndexPageSize must be a positive integer.");
4694
+ return pageSize;
4695
+ }
4696
+ function sortManagedBundles(bundles, orderBy = DEFAULT_DESC_ORDER) {
4697
+ return sortBundles$1(bundles, orderBy);
4698
+ }
4699
+ function isDefaultManagementOrder(orderBy) {
4700
+ return orderBy === void 0 || orderBy.field === DEFAULT_DESC_ORDER.field && orderBy.direction === DEFAULT_DESC_ORDER.direction;
4701
+ }
4702
+ function hasUnsupportedManagementFilters(where) {
4703
+ if (!where) return false;
4704
+ return Boolean(where.enabled !== void 0 || where.id !== void 0 || where.targetAppVersion !== void 0 || where.targetAppVersionIn !== void 0 || where.targetAppVersionNotNull !== void 0 || where.fingerprintHash !== void 0);
4705
+ }
4706
+ function getSupportedManagementScope(where, orderBy) {
4707
+ if (!isDefaultManagementOrder(orderBy) || hasUnsupportedManagementFilters(where)) return null;
4708
+ return {
4709
+ channel: where?.channel,
4710
+ platform: where?.platform
4711
+ };
4712
+ }
4713
+ function encodeScopePart(value) {
4714
+ return encodeURIComponent(value);
4715
+ }
4716
+ function getManagementScopeCacheKey({ channel, platform }) {
4717
+ return `${channel ?? "*"}|${platform ?? "*"}`;
4718
+ }
4719
+ function getManagementScopePrefix({ channel, platform }) {
4720
+ if (channel && platform) return `${MANAGEMENT_INDEX_PREFIX}/channel/${encodeScopePart(channel)}/platform/${platform}`;
4721
+ if (channel) return `${MANAGEMENT_INDEX_PREFIX}/channel/${encodeScopePart(channel)}`;
4722
+ if (platform) return `${MANAGEMENT_INDEX_PREFIX}/platform/${platform}`;
4723
+ return `${MANAGEMENT_INDEX_PREFIX}/all`;
4724
+ }
4725
+ function getManagementRootKey(scope) {
4726
+ return `${getManagementScopePrefix(scope)}/root.json`;
4727
+ }
4728
+ function getManagementPageKey(scope, pageIndex) {
4729
+ return `${getManagementScopePrefix(scope)}/pages/${String(pageIndex).padStart(4, "0")}.json`;
4730
+ }
4731
+ function createBundleWithUpdateJsonKey(bundle) {
4732
+ const target = resolveStorageTarget(bundle);
4733
+ return {
4734
+ ...bundle,
4735
+ _updateJsonKey: `${bundle.channel}/${bundle.platform}/${target}/update.json`
4736
+ };
4737
+ }
4738
+ function getPageStartOffsets(pages) {
4739
+ const startOffsets = [];
4740
+ let offset = 0;
4741
+ for (const page of pages) {
4742
+ startOffsets.push(offset);
4743
+ offset += page.count;
4744
+ }
4745
+ return startOffsets;
4746
+ }
4747
+ function createEmptyManagementResult(limit) {
4748
+ return {
4749
+ data: [],
4750
+ pagination: calculatePagination(0, {
4751
+ limit,
4752
+ offset: 0
4753
+ })
4754
+ };
4755
+ }
4756
+ function buildManagementIndexArtifacts(allBundles, pageSize) {
4757
+ const sortedAllBundles = sortManagedBundles(allBundles);
4758
+ const pages = /* @__PURE__ */ new Map();
4759
+ const scopes = [];
4760
+ const channels = [...new Set(sortedAllBundles.map((bundle) => bundle.channel))].sort();
4761
+ const addScope = (scope, scopeBundles, options) => {
4762
+ if (!options?.includeChannels && scopeBundles.length === 0) return;
4763
+ const pageKeys = [];
4764
+ const pageDescriptors = [];
4765
+ for (let pageIndex = 0; pageIndex * pageSize < scopeBundles.length; pageIndex++) {
4766
+ const page = scopeBundles.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
4767
+ const key = getManagementPageKey(scope, pageIndex);
4768
+ pages.set(key, page);
4769
+ pageKeys.push(key);
4770
+ pageDescriptors.push({
4771
+ key,
4772
+ count: page.length,
4773
+ firstId: page[0].id,
4774
+ lastId: page.at(-1).id
4775
+ });
4776
+ }
4777
+ const root = {
4778
+ version: MANAGEMENT_INDEX_VERSION,
4779
+ pageSize,
4780
+ total: scopeBundles.length,
4781
+ pages: pageDescriptors,
4782
+ ...options?.includeChannels ? { channels } : {}
4783
+ };
4784
+ scopes.push({
4785
+ cacheKey: getManagementScopeCacheKey(scope),
4786
+ rootKey: getManagementRootKey(scope),
4787
+ root,
4788
+ pageKeys
4789
+ });
4790
+ };
4791
+ addScope({}, sortedAllBundles, { includeChannels: true });
4792
+ for (const channel of channels) {
4793
+ const channelBundles = sortedAllBundles.filter((bundle) => bundle.channel === channel);
4794
+ addScope({ channel }, channelBundles);
4795
+ for (const platform of ["ios", "android"]) {
4796
+ const scopedBundles = channelBundles.filter((bundle) => bundle.platform === platform);
4797
+ addScope({
4798
+ channel,
4799
+ platform
4800
+ }, scopedBundles);
4801
+ }
4802
+ }
4803
+ for (const platform of ["ios", "android"]) {
4804
+ const platformBundles = sortedAllBundles.filter((bundle) => bundle.platform === platform);
4805
+ addScope({ platform }, platformBundles);
4806
+ }
4807
+ return {
4808
+ pages,
4809
+ scopes
4810
+ };
4811
+ }
4493
4812
  /**
4494
4813
  * Creates a blob storage-based database plugin with lazy initialization.
4495
4814
  *
@@ -4499,22 +4818,282 @@ function resolveStorageTarget({ targetAppVersion, fingerprintHash }) {
4499
4818
  */
4500
4819
  const createBlobDatabasePlugin = ({ name, factory }) => {
4501
4820
  return (config, hooks) => {
4821
+ const managementIndexPageSize = resolveManagementIndexPageSize(config);
4502
4822
  const { listObjects, loadObject, uploadObject, deleteObject, invalidatePaths, apiBasePath } = factory(config);
4503
4823
  const bundlesMap = /* @__PURE__ */ new Map();
4504
4824
  const pendingBundlesMap = /* @__PURE__ */ new Map();
4825
+ const managementRootCache = /* @__PURE__ */ new Map();
4505
4826
  const PLATFORMS = ["ios", "android"];
4827
+ const getAllManagementArtifact = (artifacts) => {
4828
+ const allArtifact = artifacts.scopes.find((scope) => scope.cacheKey === ALL_SCOPE_CACHE_KEY);
4829
+ if (!allArtifact) throw new Error("all-bundles management index artifact not found");
4830
+ return allArtifact;
4831
+ };
4832
+ const replaceManagementRootCache = (artifacts) => {
4833
+ managementRootCache.clear();
4834
+ for (const scope of artifacts.scopes) managementRootCache.set(scope.cacheKey, scope.root);
4835
+ };
4836
+ const createHydratedBundle = (bundle) => {
4837
+ const hydratedBundle = createBundleWithUpdateJsonKey(bundle);
4838
+ bundlesMap.set(hydratedBundle.id, hydratedBundle);
4839
+ return hydratedBundle;
4840
+ };
4841
+ const loadStoredManagementRoot = async (scope) => {
4842
+ const cacheKey = getManagementScopeCacheKey(scope);
4843
+ const storedRoot = await loadObject(getManagementRootKey(scope));
4844
+ if (storedRoot) {
4845
+ managementRootCache.set(cacheKey, storedRoot);
4846
+ return storedRoot;
4847
+ }
4848
+ managementRootCache.delete(cacheKey);
4849
+ return null;
4850
+ };
4851
+ const loadManagementPage = async (descriptor, pageCache) => {
4852
+ if (pageCache?.has(descriptor.key)) return pageCache.get(descriptor.key) ?? null;
4853
+ const page = await loadObject(descriptor.key);
4854
+ pageCache?.set(descriptor.key, page);
4855
+ return page;
4856
+ };
4857
+ const loadBundleFromManagementRoot = async (root, bundleId) => {
4858
+ const pageIndex = findPageIndexContainingId(root.pages, bundleId);
4859
+ if (pageIndex < 0) return null;
4860
+ const descriptor = root.pages[pageIndex];
4861
+ const page = await loadManagementPage(descriptor);
4862
+ if (!page) return null;
4863
+ return page.find((item) => item.id === bundleId) ?? null;
4864
+ };
4865
+ const loadAllBundlesFromRoot = async (root) => {
4866
+ const allBundles = [];
4867
+ const pageCache = /* @__PURE__ */ new Map();
4868
+ for (const descriptor of root.pages) {
4869
+ const page = await loadManagementPage(descriptor, pageCache);
4870
+ if (!page) return null;
4871
+ allBundles.push(...page);
4872
+ }
4873
+ return allBundles;
4874
+ };
4875
+ const persistManagementIndexArtifacts = async (nextArtifacts, previousArtifacts) => {
4876
+ for (const [key, page] of nextArtifacts.pages.entries()) await uploadObject(key, page);
4877
+ for (const scope of nextArtifacts.scopes) await uploadObject(scope.rootKey, scope.root);
4878
+ if (!previousArtifacts) return;
4879
+ const nextPageKeys = new Set(nextArtifacts.pages.keys());
4880
+ const nextRootKeys = new Set(nextArtifacts.scopes.map((scope) => scope.rootKey));
4881
+ for (const [key] of previousArtifacts.pages.entries()) if (!nextPageKeys.has(key)) await deleteObject(key).catch(() => {});
4882
+ for (const scope of previousArtifacts.scopes) if (!nextRootKeys.has(scope.rootKey)) await deleteObject(scope.rootKey).catch(() => {});
4883
+ };
4884
+ const ensureAllManagementRoot = async () => {
4885
+ const storedAllRoot = await loadStoredManagementRoot({});
4886
+ if (storedAllRoot && storedAllRoot.pageSize === managementIndexPageSize) return storedAllRoot;
4887
+ const rebuiltBundles = sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
4888
+ const nextArtifacts = buildManagementIndexArtifacts(rebuiltBundles, managementIndexPageSize);
4889
+ await persistManagementIndexArtifacts(nextArtifacts, storedAllRoot ? buildManagementIndexArtifacts(rebuiltBundles, storedAllRoot.pageSize) : void 0);
4890
+ replaceManagementRootCache(nextArtifacts);
4891
+ return getAllManagementArtifact(nextArtifacts).root;
4892
+ };
4893
+ const loadManagementScopeRoot = async (scope) => {
4894
+ const cacheKey = getManagementScopeCacheKey(scope);
4895
+ if (cacheKey === ALL_SCOPE_CACHE_KEY) return ensureAllManagementRoot();
4896
+ const storedRoot = await loadStoredManagementRoot(scope);
4897
+ if (storedRoot) return storedRoot;
4898
+ await ensureAllManagementRoot();
4899
+ const storedScopedRoot = await loadStoredManagementRoot(scope);
4900
+ if (storedScopedRoot) return storedScopedRoot;
4901
+ managementRootCache.set(cacheKey, null);
4902
+ return null;
4903
+ };
4904
+ const loadAllBundlesForManagementFallback = async () => {
4905
+ const allRoot = await loadManagementScopeRoot({});
4906
+ if (allRoot) {
4907
+ const pagedBundles = await loadAllBundlesFromRoot(allRoot);
4908
+ if (pagedBundles) return pagedBundles;
4909
+ }
4910
+ return sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
4911
+ };
4912
+ const loadCurrentBundlesForIndexRebuild = async () => {
4913
+ return loadAllBundlesForManagementFallback();
4914
+ };
4915
+ const findPageIndexContainingId = (pages, id) => {
4916
+ return pages.findIndex((page) => id.localeCompare(page.firstId) <= 0 && id.localeCompare(page.lastId) >= 0);
4917
+ };
4918
+ const readPagedBundles = async ({ root, limit, offset, cursor }) => {
4919
+ if (root.total === 0 || root.pages.length === 0) return createEmptyManagementResult(limit);
4920
+ const pageStartOffsets = getPageStartOffsets(root.pages);
4921
+ const pageCache = /* @__PURE__ */ new Map();
4922
+ if (offset !== void 0) {
4923
+ const normalizedOffset = Math.max(0, offset);
4924
+ if (normalizedOffset >= root.total) return {
4925
+ data: [],
4926
+ pagination: calculatePagination(root.total, {
4927
+ limit,
4928
+ offset: normalizedOffset
4929
+ })
4930
+ };
4931
+ let pageIndex = 0;
4932
+ for (let index = pageStartOffsets.length - 1; index >= 0; index--) if ((pageStartOffsets[index] ?? 0) <= normalizedOffset) {
4933
+ pageIndex = index;
4934
+ break;
4935
+ }
4936
+ const startInPage = normalizedOffset - (pageStartOffsets[pageIndex] ?? 0);
4937
+ const data = [];
4938
+ for (let currentPageIndex = pageIndex; currentPageIndex < root.pages.length && (limit <= 0 || data.length < limit); currentPageIndex++) {
4939
+ const descriptor = root.pages[currentPageIndex];
4940
+ const page = await loadManagementPage(descriptor, pageCache);
4941
+ if (!page) return paginateBundles({
4942
+ bundles: await loadAllBundlesForManagementFallback(),
4943
+ limit,
4944
+ offset: normalizedOffset
4945
+ });
4946
+ data.push(...currentPageIndex === pageIndex ? page.slice(startInPage) : page);
4947
+ }
4948
+ const paginatedData = limit > 0 ? data.slice(0, limit) : data;
4949
+ return {
4950
+ data: paginatedData,
4951
+ pagination: {
4952
+ ...calculatePagination(root.total, {
4953
+ limit,
4954
+ offset: normalizedOffset
4955
+ }),
4956
+ ...paginatedData.length > 0 && normalizedOffset + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
4957
+ ...paginatedData.length > 0 && normalizedOffset > 0 ? { previousCursor: paginatedData[0]?.id } : {}
4958
+ }
4959
+ };
4960
+ }
4961
+ if (cursor?.after) {
4962
+ let pageIndex = root.pages.findIndex((page) => {
4963
+ const containsCursor = cursor.after.localeCompare(page.firstId) <= 0 && cursor.after.localeCompare(page.lastId) >= 0;
4964
+ const wholePageEligible = cursor.after.localeCompare(page.firstId) > 0;
4965
+ return containsCursor || wholePageEligible;
4966
+ });
4967
+ if (pageIndex < 0) return {
4968
+ data: [],
4969
+ pagination: {
4970
+ ...calculatePagination(root.total, {
4971
+ limit,
4972
+ offset: root.total
4973
+ }),
4974
+ previousCursor: cursor.after
4975
+ }
4976
+ };
4977
+ const data = [];
4978
+ let startIndex = null;
4979
+ while (pageIndex < root.pages.length && (limit <= 0 || data.length < limit)) {
4980
+ const descriptor = root.pages[pageIndex];
4981
+ const page = await loadManagementPage(descriptor, pageCache);
4982
+ if (!page) return paginateBundles({
4983
+ bundles: await loadAllBundlesForManagementFallback(),
4984
+ limit,
4985
+ cursor
4986
+ });
4987
+ const containsCursor = cursor.after.localeCompare(descriptor.firstId) <= 0 && cursor.after.localeCompare(descriptor.lastId) >= 0;
4988
+ let eligiblePageBundles = page;
4989
+ if (containsCursor) {
4990
+ const startInPage = page.findIndex((bundle) => bundle.id.localeCompare(cursor.after) < 0);
4991
+ if (startInPage < 0) eligiblePageBundles = [];
4992
+ else {
4993
+ eligiblePageBundles = page.slice(startInPage);
4994
+ startIndex ??= (pageStartOffsets[pageIndex] ?? 0) + startInPage;
4995
+ }
4996
+ } else if (eligiblePageBundles.length > 0) startIndex ??= pageStartOffsets[pageIndex] ?? 0;
4997
+ data.push(...eligiblePageBundles);
4998
+ if (limit > 0 && data.length >= limit) break;
4999
+ pageIndex += 1;
5000
+ }
5001
+ const paginatedData = limit > 0 ? data.slice(0, limit) : data;
5002
+ const resolvedStartIndex = startIndex ?? root.total;
5003
+ return {
5004
+ data: paginatedData,
5005
+ pagination: {
5006
+ ...calculatePagination(root.total, {
5007
+ limit,
5008
+ offset: resolvedStartIndex
5009
+ }),
5010
+ ...paginatedData.length > 0 && resolvedStartIndex + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
5011
+ ...paginatedData.length > 0 && resolvedStartIndex > 0 ? { previousCursor: paginatedData[0]?.id } : {}
5012
+ }
5013
+ };
5014
+ }
5015
+ if (cursor?.before) {
5016
+ let pageIndex = -1;
5017
+ for (let index = root.pages.length - 1; index >= 0; index--) {
5018
+ const page = root.pages[index];
5019
+ const containsCursor = cursor.before.localeCompare(page.firstId) <= 0 && cursor.before.localeCompare(page.lastId) >= 0;
5020
+ const wholePageEligible = cursor.before.localeCompare(page.lastId) < 0;
5021
+ if (containsCursor || wholePageEligible) {
5022
+ pageIndex = index;
5023
+ break;
5024
+ }
5025
+ }
5026
+ if (pageIndex < 0) return createEmptyManagementResult(limit);
5027
+ let startIndex = null;
5028
+ let collected = [];
5029
+ while (pageIndex >= 0 && (limit <= 0 || collected.length < limit)) {
5030
+ const descriptor = root.pages[pageIndex];
5031
+ const page = await loadManagementPage(descriptor, pageCache);
5032
+ if (!page) return paginateBundles({
5033
+ bundles: await loadAllBundlesForManagementFallback(),
5034
+ limit,
5035
+ cursor
5036
+ });
5037
+ const eligiblePageBundles = cursor.before.localeCompare(descriptor.firstId) <= 0 && cursor.before.localeCompare(descriptor.lastId) >= 0 ? page.filter((bundle) => bundle.id.localeCompare(cursor.before) > 0) : page;
5038
+ collected = [...eligiblePageBundles, ...collected];
5039
+ if (eligiblePageBundles.length > 0) startIndex = pageStartOffsets[pageIndex] ?? 0;
5040
+ if (limit > 0 && collected.length >= limit) break;
5041
+ pageIndex -= 1;
5042
+ }
5043
+ if (startIndex === null || collected.length === 0) return createEmptyManagementResult(limit);
5044
+ let paginatedData = collected;
5045
+ if (limit > 0 && collected.length > limit) {
5046
+ const dropCount = collected.length - limit;
5047
+ paginatedData = collected.slice(dropCount);
5048
+ startIndex += dropCount;
5049
+ }
5050
+ const pagination = calculatePagination(root.total, {
5051
+ limit,
5052
+ offset: startIndex
5053
+ });
5054
+ return {
5055
+ data: paginatedData,
5056
+ pagination: {
5057
+ ...pagination,
5058
+ ...paginatedData.length > 0 && startIndex + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
5059
+ ...paginatedData.length > 0 && startIndex > 0 ? { previousCursor: paginatedData[0]?.id } : {}
5060
+ }
5061
+ };
5062
+ }
5063
+ const pageIndex = 0;
5064
+ const startInPage = 0;
5065
+ const data = [];
5066
+ for (let currentPageIndex = pageIndex; currentPageIndex < root.pages.length && (limit <= 0 || data.length < limit); currentPageIndex++) {
5067
+ const descriptor = root.pages[currentPageIndex];
5068
+ const page = await loadManagementPage(descriptor, pageCache);
5069
+ if (!page) return paginateBundles({
5070
+ bundles: await loadAllBundlesForManagementFallback(),
5071
+ limit,
5072
+ cursor
5073
+ });
5074
+ data.push(...currentPageIndex === pageIndex ? page.slice(startInPage) : page);
5075
+ }
5076
+ const paginatedData = limit > 0 ? data.slice(0, limit) : data;
5077
+ return {
5078
+ data: paginatedData,
5079
+ pagination: {
5080
+ ...calculatePagination(root.total, {
5081
+ limit,
5082
+ offset: 0
5083
+ }),
5084
+ ...paginatedData.length > 0 && paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {}
5085
+ }
5086
+ };
5087
+ };
4506
5088
  async function reloadBundles() {
4507
5089
  bundlesMap.clear();
4508
- const platformPromises = PLATFORMS.map(async (platform) => {
4509
- const filePromises = (await listUpdateJsonKeys(platform)).map(async (key) => {
4510
- return (await loadObject(key) ?? []).map((bundle) => ({
4511
- ...bundle,
4512
- _updateJsonKey: key
4513
- }));
4514
- });
4515
- return (await Promise.all(filePromises)).flat();
5090
+ const filePromises = (await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)).map(async (key) => {
5091
+ return (await loadObject(key) ?? []).map((bundle) => ({
5092
+ ...bundle,
5093
+ _updateJsonKey: key
5094
+ }));
4516
5095
  });
4517
- const allBundles = (await Promise.all(platformPromises)).flat();
5096
+ const allBundles = (await Promise.all(filePromises)).flat();
4518
5097
  for (const bundle of allBundles) bundlesMap.set(bundle.id, bundle);
4519
5098
  for (const [id, bundle] of pendingBundlesMap.entries()) bundlesMap.set(id, bundle);
4520
5099
  return orderBy(allBundles, [(v) => v.id], ["desc"]);
@@ -4549,17 +5128,6 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4549
5128
  if (JSON.stringify(oldTargetVersions) !== JSON.stringify(newTargetVersions)) await uploadObject(targetKey, newTargetVersions);
4550
5129
  }
4551
5130
  }
4552
- /**
4553
- * Lists update.json keys for a given platform.
4554
- *
4555
- * - If a channel is provided, only that channel's update.json files are listed.
4556
- * - Otherwise, all channels for the given platform are returned.
4557
- */
4558
- async function listUpdateJsonKeys(platform, channel) {
4559
- const prefix = channel ? platform ? `${channel}/${platform}/` : `${channel}/` : "";
4560
- const pattern = channel ? platform ? new RegExp(`^${channel}/${platform}/[^/]+/update\\.json$`) : new RegExp(`^${channel}/[^/]+/[^/]+/update\\.json$`) : platform ? new RegExp(`^[^/]+/${platform}/[^/]+/update\\.json$`) : /^[^/]+\/[^/]+\/[^/]+\/update\.json$/;
4561
- return listObjects(prefix).then((keys) => keys.filter((key) => pattern.test(key)));
4562
- }
4563
5131
  const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }) => {
4564
5132
  const matchingVersions = filterCompatibleAppVersions(await loadObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion);
4565
5133
  return getUpdateInfo((await Promise.allSettled(matchingVersions.map(async (targetAppVersion) => {
@@ -4607,41 +5175,56 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4607
5175
  return createDatabasePlugin({
4608
5176
  name,
4609
5177
  factory: () => ({
5178
+ supportsCursorPagination: true,
4610
5179
  async getBundleById(bundleId) {
4611
5180
  const pendingBundle = pendingBundlesMap.get(bundleId);
4612
5181
  if (pendingBundle) return removeBundleInternalKeys(pendingBundle);
4613
5182
  const bundle = bundlesMap.get(bundleId);
4614
5183
  if (bundle) return removeBundleInternalKeys(bundle);
4615
- return (await reloadBundles()).find((bundle) => bundle.id === bundleId) ?? null;
5184
+ const allRoot = await loadManagementScopeRoot({});
5185
+ if (allRoot) {
5186
+ const matchedBundle = await loadBundleFromManagementRoot(allRoot, bundleId);
5187
+ if (matchedBundle) return removeBundleInternalKeys(createHydratedBundle(matchedBundle));
5188
+ managementRootCache.delete(ALL_SCOPE_CACHE_KEY);
5189
+ const refreshedAllRoot = await loadStoredManagementRoot({});
5190
+ if (refreshedAllRoot) {
5191
+ const refreshedBundle = await loadBundleFromManagementRoot(refreshedAllRoot, bundleId);
5192
+ if (refreshedBundle) return removeBundleInternalKeys(createHydratedBundle(refreshedBundle));
5193
+ }
5194
+ }
5195
+ const matchedBundle = (await reloadBundles()).find((item) => item.id === bundleId);
5196
+ if (!matchedBundle) return null;
5197
+ return removeBundleInternalKeys(matchedBundle);
4616
5198
  },
4617
5199
  async getUpdateInfo(args) {
4618
5200
  if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args);
4619
5201
  return getFingerprintUpdateInfo(args);
4620
5202
  },
4621
5203
  async getBundles(options) {
4622
- let allBundles = await reloadBundles();
4623
- const { where, limit, offset, orderBy } = options;
4624
- if (where) allBundles = allBundles.filter((bundle) => bundleMatchesQueryWhere$1(bundle, where));
4625
- const cleanBundles = sortBundles$1(allBundles.map(removeBundleInternalKeys), orderBy);
4626
- const total = cleanBundles.length;
4627
- let paginatedData = cleanBundles;
4628
- if (offset > 0) paginatedData = paginatedData.slice(offset);
4629
- if (limit) paginatedData = paginatedData.slice(0, limit);
4630
- return {
4631
- data: paginatedData,
4632
- pagination: calculatePagination(total, {
5204
+ const { where, limit, offset, orderBy, cursor } = options;
5205
+ const scope = getSupportedManagementScope(where, orderBy);
5206
+ if (scope) {
5207
+ const root = await loadManagementScopeRoot(scope);
5208
+ if (!root) return createEmptyManagementResult(limit);
5209
+ return readPagedBundles({
5210
+ root,
4633
5211
  limit,
4634
- offset
4635
- })
4636
- };
5212
+ offset,
5213
+ cursor
5214
+ });
5215
+ }
5216
+ let allBundles = await loadAllBundlesForManagementFallback();
5217
+ if (where) allBundles = allBundles.filter((bundle) => bundleMatchesQueryWhere$1(bundle, where));
5218
+ return paginateBundles({
5219
+ bundles: allBundles,
5220
+ limit,
5221
+ offset,
5222
+ cursor,
5223
+ orderBy
5224
+ });
4637
5225
  },
4638
5226
  async getChannels() {
4639
- const total = (await reloadBundles()).length;
4640
- const result = await this.getBundles({
4641
- limit: total,
4642
- offset: 0
4643
- });
4644
- return [...new Set(result.data.map((bundle) => bundle.channel))];
5227
+ return (await loadManagementScopeRoot({}))?.channels ?? [];
4645
5228
  },
4646
5229
  async commitBundle({ changedSets }) {
4647
5230
  if (changedSets.length === 0) return;
@@ -4738,6 +5321,20 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
4738
5321
  await uploadObject(key, currentBundles);
4739
5322
  })();
4740
5323
  if (isTargetAppVersionChanged || isChannelChanged) for (const platform of PLATFORMS) await updateTargetVersionsForPlatform(platform);
5324
+ const currentIndexBundles = await loadCurrentBundlesForIndexRebuild();
5325
+ const nextIndexMap = new Map(currentIndexBundles.map((bundle) => [bundle.id, bundle]));
5326
+ for (const { operation, data } of changedSets) {
5327
+ if (operation === "delete") {
5328
+ nextIndexMap.delete(data.id);
5329
+ continue;
5330
+ }
5331
+ nextIndexMap.set(data.id, data);
5332
+ }
5333
+ const nextIndexBundles = sortManagedBundles(Array.from(nextIndexMap.values()));
5334
+ const previousArtifacts = buildManagementIndexArtifacts(currentIndexBundles, managementIndexPageSize);
5335
+ const nextArtifacts = buildManagementIndexArtifacts(nextIndexBundles, managementIndexPageSize);
5336
+ await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
5337
+ replaceManagementRootCache(nextArtifacts);
4741
5338
  const encondedPaths = /* @__PURE__ */ new Set();
4742
5339
  for (const path of pathsToInvalidate) encondedPaths.add(encodeURI(path));
4743
5340
  await invalidatePaths(Array.from(encondedPaths));
@@ -4908,13 +5505,13 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
4908
5505
  return isCohortEligibleForUpdate(bundle.id, cohort, bundle.rolloutCohortCount, bundle.targetCohorts);
4909
5506
  };
4910
5507
  const findUpdateInfoByScanning = async ({ args, queryWhere, isCandidate, context }) => {
4911
- let offset = 0;
5508
+ let after;
4912
5509
  while (true) {
4913
5510
  const { data, pagination } = await getSortedBundlePage({
4914
5511
  where: queryWhere,
4915
5512
  limit: PAGE_SIZE,
4916
- offset,
4917
- orderBy: DESC_ORDER
5513
+ orderBy: DESC_ORDER,
5514
+ ...after ? { cursor: { after } } : {}
4918
5515
  }, context);
4919
5516
  for (const bundle of data) {
4920
5517
  if (!bundleMatchesQueryWhere(bundle, queryWhere) || !isCandidate(bundle)) continue;
@@ -4934,7 +5531,8 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
4934
5531
  return makeResponse(bundle, "ROLLBACK");
4935
5532
  }
4936
5533
  if (!pagination.hasNextPage) break;
4937
- offset += PAGE_SIZE;
5534
+ after = data.at(-1)?.id;
5535
+ if (!after) break;
4938
5536
  }
4939
5537
  if (args.bundleId === "00000000-0000-0000-0000-000000000000") return null;
4940
5538
  if (args.minBundleId && args.bundleId.localeCompare(args.minBundleId) <= 0) return null;
@@ -5185,7 +5783,13 @@ const handleGetBundles = async (_params, request, api, context) => {
5185
5783
  const channel = url.searchParams.get("channel") ?? void 0;
5186
5784
  const platform = url.searchParams.get("platform");
5187
5785
  const limit = Number(url.searchParams.get("limit")) || 50;
5188
- const offset = Number(url.searchParams.get("offset")) || 0;
5786
+ const pageParam = url.searchParams.get("page");
5787
+ const offset = url.searchParams.get("offset");
5788
+ const after = url.searchParams.get("after") ?? void 0;
5789
+ const before = url.searchParams.get("before") ?? void 0;
5790
+ const page = pageParam === null ? void 0 : Number.isInteger(Number(pageParam)) && Number(pageParam) > 0 ? Number(pageParam) : null;
5791
+ if (offset !== null) throw new HandlerBadRequestError("The 'offset' query parameter has been removed. Use 'after' or 'before' cursor pagination instead.");
5792
+ if (page === null) throw new HandlerBadRequestError("The 'page' query parameter must be a positive integer.");
5189
5793
  if (platform !== null && !isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
5190
5794
  const result = await api.getBundles({
5191
5795
  where: {
@@ -5193,7 +5797,11 @@ const handleGetBundles = async (_params, request, api, context) => {
5193
5797
  ...platform && { platform }
5194
5798
  },
5195
5799
  limit,
5196
- offset
5800
+ page,
5801
+ cursor: after || before ? {
5802
+ after,
5803
+ before
5804
+ } : void 0
5197
5805
  }, context);
5198
5806
  return new Response(JSON.stringify(result), {
5199
5807
  status: 200,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/aws",
3
3
  "type": "module",
4
- "version": "0.29.5",
4
+ "version": "0.29.7",
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.29.5",
46
- "@hot-updater/js": "0.29.5",
47
- "@hot-updater/mock": "0.29.5",
48
- "@hot-updater/test-utils": "0.29.5"
45
+ "@hot-updater/core": "0.29.7",
46
+ "@hot-updater/js": "0.29.7",
47
+ "@hot-updater/mock": "0.29.7",
48
+ "@hot-updater/test-utils": "0.29.7"
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/plugin-core": "0.29.5",
64
- "@hot-updater/cli-tools": "0.29.5",
65
- "@hot-updater/server": "0.29.5"
63
+ "@hot-updater/plugin-core": "0.29.7",
64
+ "@hot-updater/server": "0.29.7",
65
+ "@hot-updater/cli-tools": "0.29.7"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsdown",