@hot-updater/server 0.31.4 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/_virtual/_rolldown/runtime.cjs +1 -1
  2. package/dist/_virtual/_rolldown/runtime.mjs +1 -1
  3. package/dist/db/createBundleDiff.cjs +19 -13
  4. package/dist/db/createBundleDiff.mjs +15 -9
  5. package/dist/db/index.cjs +7 -10
  6. package/dist/db/index.mjs +7 -10
  7. package/dist/db/pluginCore.cjs +136 -96
  8. package/dist/db/pluginCore.mjs +137 -97
  9. package/dist/db/requestBundleIdentityMap.cjs +29 -0
  10. package/dist/db/requestBundleIdentityMap.mjs +29 -0
  11. package/dist/db/schemaEnhancements.cjs +1 -1
  12. package/dist/db/types.d.cts +2 -1
  13. package/dist/db/types.d.mts +2 -1
  14. package/dist/db/updateArtifacts.cjs +6 -6
  15. package/dist/db/updateArtifacts.mjs +6 -6
  16. package/dist/handler.cjs +18 -8
  17. package/dist/handler.d.cts +9 -10
  18. package/dist/handler.d.mts +9 -10
  19. package/dist/handler.mjs +17 -7
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.mts +1 -1
  22. package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/index.d.cts +1 -1
  23. package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/index.d.mts +1 -1
  24. package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/query/index.d.cts +1 -1
  25. package/dist/node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/query/index.d.mts +1 -1
  26. package/dist/packages/server/package.cjs +1 -1
  27. package/dist/packages/server/package.mjs +1 -1
  28. package/dist/runtime.cjs +10 -12
  29. package/dist/runtime.mjs +10 -12
  30. package/package.json +7 -7
  31. package/src/db/createBundleDiff.spec.ts +3 -0
  32. package/src/db/createBundleDiff.ts +27 -21
  33. package/src/db/index.spec.ts +36 -0
  34. package/src/db/index.ts +6 -10
  35. package/src/db/pluginCore.spec.ts +443 -0
  36. package/src/db/pluginCore.ts +63 -7
  37. package/src/db/requestBundleIdentityMap.spec.ts +56 -0
  38. package/src/db/requestBundleIdentityMap.ts +61 -0
  39. package/src/db/types.ts +2 -0
  40. package/src/db/updateArtifacts.ts +8 -19
  41. package/src/handler-standalone.integration.spec.ts +12 -0
  42. package/src/handler.spec.ts +117 -19
  43. package/src/handler.ts +47 -21
  44. package/src/runtime.spec.ts +46 -4
  45. package/src/runtime.ts +10 -12
@@ -1,6 +1,7 @@
1
1
  import { assertBundlePersistenceConstraints } from "./schemaEnhancements.mjs";
2
2
  import { resolveManifestArtifacts } from "./updateArtifacts.mjs";
3
- import { semverSatisfies } from "@hot-updater/plugin-core";
3
+ import { createRequestBundleIdentityMap } from "./requestBundleIdentityMap.mjs";
4
+ import { getRequestUpdateBundleSeeds, semverSatisfies } from "@hot-updater/plugin-core";
4
5
  import { NIL_UUID, isCohortEligibleForUpdate } from "@hot-updater/core";
5
6
  //#region src/db/pluginCore.ts
6
7
  const PAGE_SIZE = 100;
@@ -110,109 +111,148 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
110
111
  enabled: true,
111
112
  id: { gte: minBundleId }
112
113
  });
113
- return {
114
- api: {
115
- async getBundleById(id, context) {
116
- return getPlugin().getBundleById(id, context);
117
- },
118
- async getUpdateInfo(args, context) {
119
- const directGetUpdateInfo = getPlugin().getUpdateInfo;
120
- if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
121
- const channel = args.channel ?? "production";
122
- const minBundleId = args.minBundleId ?? NIL_UUID;
123
- const baseWhere = getBaseWhere({
124
- platform: args.platform,
125
- channel,
126
- minBundleId
127
- });
128
- if (args._updateStrategy === "fingerprint") return findUpdateInfoByScanning({
129
- args,
130
- queryWhere: {
131
- ...baseWhere,
132
- fingerprintHash: args.fingerprintHash
133
- },
134
- context,
135
- isCandidate: (bundle) => {
136
- return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && bundle.fingerprintHash === args.fingerprintHash;
137
- }
138
- });
139
- return findUpdateInfoByScanning({
140
- args,
141
- queryWhere: { ...baseWhere },
142
- context,
143
- isCandidate: (bundle) => {
144
- return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && !!bundle.targetAppVersion && semverSatisfies(bundle.targetAppVersion, args.appVersion);
145
- }
146
- });
147
- },
148
- async getAppUpdateInfo(args, context) {
149
- const info = await this.getUpdateInfo(args, context);
150
- if (!info) return null;
151
- const { storageUri, ...rest } = info;
152
- const readStorageText = options?.readStorageText;
153
- if (info.id === NIL_UUID || !readStorageText) {
154
- const fileUrl = await resolveFileUrl(storageUri ?? null, context);
155
- return {
156
- ...rest,
157
- fileUrl
158
- };
114
+ const api = {
115
+ async getBundleById(id, context) {
116
+ return getPlugin().getBundleById(id, context);
117
+ },
118
+ async getUpdateInfo(args, context) {
119
+ const directGetUpdateInfo = getPlugin().getUpdateInfo;
120
+ if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
121
+ const channel = args.channel ?? "production";
122
+ const minBundleId = args.minBundleId ?? NIL_UUID;
123
+ const baseWhere = getBaseWhere({
124
+ platform: args.platform,
125
+ channel,
126
+ minBundleId
127
+ });
128
+ if (args._updateStrategy === "fingerprint") return findUpdateInfoByScanning({
129
+ args,
130
+ queryWhere: {
131
+ ...baseWhere,
132
+ fingerprintHash: args.fingerprintHash
133
+ },
134
+ context,
135
+ isCandidate: (bundle) => {
136
+ return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && bundle.fingerprintHash === args.fingerprintHash;
137
+ }
138
+ });
139
+ return findUpdateInfoByScanning({
140
+ args,
141
+ queryWhere: { ...baseWhere },
142
+ context,
143
+ isCandidate: (bundle) => {
144
+ return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && !!bundle.targetAppVersion && semverSatisfies(bundle.targetAppVersion, args.appVersion);
159
145
  }
160
- const [fileUrl, targetBundle, currentBundle] = await Promise.all([
161
- resolveFileUrl(storageUri ?? null, context),
162
- getPlugin().getBundleById(info.id, context),
163
- args.bundleId !== NIL_UUID ? getPlugin().getBundleById(args.bundleId, context) : null
164
- ]);
165
- const baseResponse = {
146
+ });
147
+ },
148
+ async getAppUpdateInfo(args, context) {
149
+ const info = await this.getUpdateInfo(args, context);
150
+ if (!info) return null;
151
+ const { storageUri, ...rest } = info;
152
+ const readStorageText = options?.readStorageText;
153
+ if (info.id === NIL_UUID || !readStorageText) {
154
+ const fileUrl = await resolveFileUrl(storageUri ?? null, context);
155
+ return {
166
156
  ...rest,
167
157
  fileUrl
168
158
  };
169
- const manifestArtifacts = await resolveManifestArtifacts({
170
- currentBundle,
171
- resolveFileUrl,
172
- readStorageText,
173
- targetBundle,
174
- context
175
- });
176
- if (!manifestArtifacts) return baseResponse;
177
- return {
178
- ...baseResponse,
179
- ...manifestArtifacts
180
- };
181
- },
182
- async getChannels(context) {
183
- return getPlugin().getChannels(context);
184
- },
185
- async getBundles(options, context) {
186
- return getPlugin().getBundles(options, context);
187
- },
188
- async insertBundle(bundle, context) {
189
- assertBundlePersistenceConstraints(bundle);
190
- await runWithMutationPlugin(async (plugin) => {
191
- await plugin.appendBundle(bundle, context);
192
- await plugin.commitBundle(context);
193
- });
194
- },
195
- async updateBundleById(bundleId, newBundle, context) {
196
- await runWithMutationPlugin(async (plugin) => {
197
- const current = await plugin.getBundleById(bundleId, context);
198
- if (!current) throw new Error("targetBundleId not found");
199
- assertBundlePersistenceConstraints({
200
- ...current,
201
- ...newBundle
202
- });
203
- await plugin.updateBundle(bundleId, newBundle, context);
204
- await plugin.commitBundle(context);
159
+ }
160
+ const requestBundleSeeds = getRequestUpdateBundleSeeds(context);
161
+ const requestBundles = createRequestBundleIdentityMap({
162
+ context,
163
+ loadBundleById: (bundleId, requestContext) => getPlugin().getBundleById(bundleId, requestContext),
164
+ seeds: requestBundleSeeds
165
+ });
166
+ const getCurrentBundle = () => {
167
+ if (args.bundleId === NIL_UUID) return null;
168
+ const seededCurrentBundle = requestBundles.peek(args.bundleId);
169
+ if (seededCurrentBundle || requestBundleSeeds.length > 0) return seededCurrentBundle;
170
+ return requestBundles.get(args.bundleId);
171
+ };
172
+ const [fileUrl, targetBundle, currentBundle] = await Promise.all([
173
+ resolveFileUrl(storageUri ?? null, context),
174
+ requestBundles.get(info.id),
175
+ getCurrentBundle()
176
+ ]);
177
+ const baseResponse = {
178
+ ...rest,
179
+ fileUrl
180
+ };
181
+ const manifestArtifacts = await resolveManifestArtifacts({
182
+ currentBundle,
183
+ resolveFileUrl,
184
+ readStorageText,
185
+ targetBundle,
186
+ context
187
+ });
188
+ if (!manifestArtifacts) return baseResponse;
189
+ return {
190
+ ...baseResponse,
191
+ ...manifestArtifacts
192
+ };
193
+ },
194
+ async getChannels(context) {
195
+ return getPlugin().getChannels(context);
196
+ },
197
+ async getBundles(options, context) {
198
+ return getPlugin().getBundles(options, context);
199
+ },
200
+ async insertBundle(bundle, context) {
201
+ assertBundlePersistenceConstraints(bundle);
202
+ await runWithMutationPlugin(async (plugin) => {
203
+ await plugin.appendBundle(bundle, context);
204
+ await plugin.commitBundle(context);
205
+ });
206
+ },
207
+ async updateBundleById(bundleId, newBundle, context) {
208
+ await runWithMutationPlugin(async (plugin) => {
209
+ const current = await plugin.getBundleById(bundleId, context);
210
+ if (!current) throw new Error("targetBundleId not found");
211
+ assertBundlePersistenceConstraints({
212
+ ...current,
213
+ ...newBundle
205
214
  });
206
- },
207
- async deleteBundleById(bundleId, context) {
208
- await runWithMutationPlugin(async (plugin) => {
209
- const bundle = await plugin.getBundleById(bundleId, context);
210
- if (!bundle) return;
211
- await plugin.deleteBundle(bundle, context);
212
- await plugin.commitBundle(context);
215
+ await plugin.updateBundle(bundleId, newBundle, context);
216
+ await plugin.commitBundle(context);
217
+ });
218
+ },
219
+ async deleteBundleById(bundleId, context) {
220
+ await runWithMutationPlugin(async (plugin) => {
221
+ const bundle = await plugin.getBundleById(bundleId, context);
222
+ if (!bundle) return;
223
+ await plugin.deleteBundle(bundle, context);
224
+ await plugin.commitBundle(context);
225
+ });
226
+ }
227
+ };
228
+ Object.defineProperty(api, "diagnostics", {
229
+ configurable: true,
230
+ enumerable: true,
231
+ get() {
232
+ const diagnostics = getPlugin().diagnostics;
233
+ if (!diagnostics) {
234
+ Object.defineProperty(this, "diagnostics", {
235
+ configurable: true,
236
+ enumerable: true,
237
+ value: void 0
213
238
  });
239
+ return;
214
240
  }
215
- },
241
+ const wrappedDiagnostics = {};
242
+ if (diagnostics.bundleIndex) wrappedDiagnostics.bundleIndex = {
243
+ check: (context) => getPlugin().diagnostics.bundleIndex.check(context),
244
+ ...diagnostics.bundleIndex.repair ? { repair: (context) => getPlugin().diagnostics.bundleIndex.repair(context) } : {}
245
+ };
246
+ Object.defineProperty(this, "diagnostics", {
247
+ configurable: true,
248
+ enumerable: true,
249
+ value: wrappedDiagnostics
250
+ });
251
+ return wrappedDiagnostics;
252
+ }
253
+ });
254
+ return {
255
+ api,
216
256
  adapterName: getPlugin().name,
217
257
  createMigrator: () => {
218
258
  throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
@@ -0,0 +1,29 @@
1
+ //#region src/db/requestBundleIdentityMap.ts
2
+ const createRequestBundleIdentityMap = ({ context, loadBundleById, seeds }) => {
3
+ const bundles = /* @__PURE__ */ new Map();
4
+ const pendingBundles = /* @__PURE__ */ new Map();
5
+ for (const seed of seeds) if (seed) bundles.set(seed.id, seed);
6
+ const get = async (bundleId) => {
7
+ const cachedBundle = bundles.get(bundleId);
8
+ if (cachedBundle) return cachedBundle;
9
+ const pendingBundle = pendingBundles.get(bundleId);
10
+ if (pendingBundle) return pendingBundle;
11
+ const lookup = loadBundleById(bundleId, context).then((bundle) => {
12
+ pendingBundles.delete(bundleId);
13
+ if (bundle) bundles.set(bundle.id, bundle);
14
+ return bundle;
15
+ }, (error) => {
16
+ pendingBundles.delete(bundleId);
17
+ throw error;
18
+ });
19
+ pendingBundles.set(bundleId, lookup);
20
+ return lookup;
21
+ };
22
+ const peek = (bundleId) => bundles.get(bundleId) ?? null;
23
+ return {
24
+ get,
25
+ peek
26
+ };
27
+ };
28
+ //#endregion
29
+ exports.createRequestBundleIdentityMap = createRequestBundleIdentityMap;
@@ -0,0 +1,29 @@
1
+ //#region src/db/requestBundleIdentityMap.ts
2
+ const createRequestBundleIdentityMap = ({ context, loadBundleById, seeds }) => {
3
+ const bundles = /* @__PURE__ */ new Map();
4
+ const pendingBundles = /* @__PURE__ */ new Map();
5
+ for (const seed of seeds) if (seed) bundles.set(seed.id, seed);
6
+ const get = async (bundleId) => {
7
+ const cachedBundle = bundles.get(bundleId);
8
+ if (cachedBundle) return cachedBundle;
9
+ const pendingBundle = pendingBundles.get(bundleId);
10
+ if (pendingBundle) return pendingBundle;
11
+ const lookup = loadBundleById(bundleId, context).then((bundle) => {
12
+ pendingBundles.delete(bundleId);
13
+ if (bundle) bundles.set(bundle.id, bundle);
14
+ return bundle;
15
+ }, (error) => {
16
+ pendingBundles.delete(bundleId);
17
+ throw error;
18
+ });
19
+ pendingBundles.set(bundleId, lookup);
20
+ return lookup;
21
+ };
22
+ const peek = (bundleId) => bundles.get(bundleId) ?? null;
23
+ return {
24
+ get,
25
+ peek
26
+ };
27
+ };
28
+ //#endregion
29
+ export { createRequestBundleIdentityMap };
@@ -1,7 +1,7 @@
1
1
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
2
  let _hot_updater_core = require("@hot-updater/core");
3
3
  let semver = require("semver");
4
- semver = require_runtime.__toESM(semver, 1);
4
+ semver = require_runtime.__toESM(semver);
5
5
  //#region src/db/schemaEnhancements.ts
6
6
  const normalizeNullableString = (value) => {
7
7
  if (value === null || value === void 0) return null;
@@ -1,7 +1,7 @@
1
1
  import { Provider } from "../node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/index-CMqePMTF.cjs";
2
2
  import { PaginatedResult } from "../types/index.cjs";
3
3
  import { AppUpdateAvailableInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
4
- import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, RuntimeStoragePlugin } from "@hot-updater/plugin-core";
4
+ import { DatabaseBundleQueryOptions, DatabaseDiagnostics, DatabasePlugin, HotUpdaterContext, RuntimeStoragePlugin } from "@hot-updater/plugin-core";
5
5
 
6
6
  //#region src/db/types.d.ts
7
7
  type DatabasePluginFactory<TContext = unknown> = () => DatabasePlugin<TContext>;
@@ -27,6 +27,7 @@ interface DatabaseAPI<TContext = unknown> {
27
27
  insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
28
28
  updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
29
29
  deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
30
+ diagnostics?: DatabaseDiagnostics<TContext>;
30
31
  }
31
32
  type StoragePluginFactory<TContext = unknown> = () => RuntimeStoragePlugin<TContext>;
32
33
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { Provider } from "../node_modules/.pnpm/fumadb@0.2.2_drizzle-orm@0.44.7_@cloudflare_workers-types@4.20260313.1_@electric-sql_pg_c72c8c754becd21f6d6662e8fbd28e7f/node_modules/fumadb/dist/index-CMqePMTF.mjs";
2
2
  import { PaginatedResult } from "../types/index.mjs";
3
- import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, RuntimeStoragePlugin } from "@hot-updater/plugin-core";
3
+ import { DatabaseBundleQueryOptions, DatabaseDiagnostics, DatabasePlugin, HotUpdaterContext, RuntimeStoragePlugin } from "@hot-updater/plugin-core";
4
4
  import { AppUpdateAvailableInfo, Bundle as Bundle$1, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
5
5
 
6
6
  //#region src/db/types.d.ts
@@ -27,6 +27,7 @@ interface DatabaseAPI<TContext = unknown> {
27
27
  insertBundle(bundle: Bundle$1, context?: HotUpdaterContext<TContext>): Promise<void>;
28
28
  updateBundleById(bundleId: string, newBundle: Partial<Bundle$1>, context?: HotUpdaterContext<TContext>): Promise<void>;
29
29
  deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
30
+ diagnostics?: DatabaseDiagnostics<TContext>;
30
31
  }
31
32
  type StoragePluginFactory<TContext = unknown> = () => RuntimeStoragePlugin<TContext>;
32
33
  //#endregion
@@ -1,4 +1,5 @@
1
1
  require("../_virtual/_rolldown/runtime.cjs");
2
+ let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
2
3
  let _hot_updater_core = require("@hot-updater/core");
3
4
  //#region src/db/updateArtifacts.ts
4
5
  const HBC_ASSET_PATH_RE = /\.bundle$/;
@@ -18,11 +19,6 @@ const isBundleManifest = (value) => {
18
19
  return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
19
20
  });
20
21
  };
21
- const createChildStorageUri = (baseStorageUri, relativePath) => {
22
- const baseUrl = new URL(baseStorageUri);
23
- baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, "")}/${relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
24
- return baseUrl.toString();
25
- };
26
22
  const parseBundleMetadata = (value) => {
27
23
  if (!value) return;
28
24
  let parsedValue = value;
@@ -61,7 +57,11 @@ async function resolveChangedAssets({ assetBaseStorageUri, currentManifest, curr
61
57
  const changedEntries = await Promise.all(Object.entries(targetManifest.assets).map(async ([assetPath, asset]) => {
62
58
  if ((currentManifest?.assets[assetPath])?.fileHash === asset.fileHash) return null;
63
59
  const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
64
- const storageUri = createChildStorageUri(assetBaseStorageUri, usesBrotliAsset ? `${assetPath}.br` : assetPath);
60
+ const storageUri = (0, _hot_updater_plugin_core.resolveManifestAssetStorageUri)({
61
+ assetBaseStorageUri,
62
+ assetPath: usesBrotliAsset ? `${assetPath}.br` : assetPath,
63
+ fileHash: asset.fileHash
64
+ });
65
65
  const patch = patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
66
66
  let fileUrl = null;
67
67
  try {
@@ -1,3 +1,4 @@
1
+ import { resolveManifestAssetStorageUri } from "@hot-updater/plugin-core";
1
2
  import { getAssetBaseStorageUri, getBundlePatch, getManifestFileHash, getManifestStorageUri, stripBundleArtifactMetadata } from "@hot-updater/core";
2
3
  //#region src/db/updateArtifacts.ts
3
4
  const HBC_ASSET_PATH_RE = /\.bundle$/;
@@ -17,11 +18,6 @@ const isBundleManifest = (value) => {
17
18
  return typeof manifestAsset.fileHash === "string" && (manifestAsset.signature === void 0 || typeof manifestAsset.signature === "string");
18
19
  });
19
20
  };
20
- const createChildStorageUri = (baseStorageUri, relativePath) => {
21
- const baseUrl = new URL(baseStorageUri);
22
- baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, "")}/${relativePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/")}`;
23
- return baseUrl.toString();
24
- };
25
21
  const parseBundleMetadata = (value) => {
26
22
  if (!value) return;
27
23
  let parsedValue = value;
@@ -60,7 +56,11 @@ async function resolveChangedAssets({ assetBaseStorageUri, currentManifest, curr
60
56
  const changedEntries = await Promise.all(Object.entries(targetManifest.assets).map(async ([assetPath, asset]) => {
61
57
  if ((currentManifest?.assets[assetPath])?.fileHash === asset.fileHash) return null;
62
58
  const usesBrotliAsset = BR_COMPRESSED_ASSET_PATH_RE.test(assetPath);
63
- const storageUri = createChildStorageUri(assetBaseStorageUri, usesBrotliAsset ? `${assetPath}.br` : assetPath);
59
+ const storageUri = resolveManifestAssetStorageUri({
60
+ assetBaseStorageUri,
61
+ assetPath: usesBrotliAsset ? `${assetPath}.br` : assetPath,
62
+ fileHash: asset.fileHash
63
+ });
64
64
  const patch = patchDescriptor?.assetPath === assetPath ? patchDescriptor.patch : null;
65
65
  let fileUrl = null;
66
66
  try {
package/dist/handler.cjs CHANGED
@@ -2,7 +2,7 @@ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
2
  const require_internalRouter = require("./internalRouter.cjs");
3
3
  const require_version = require("./version.cjs");
4
4
  let semver = require("semver");
5
- semver = require_runtime.__toESM(semver, 1);
5
+ semver = require_runtime.__toESM(semver);
6
6
  //#region src/handler.ts
7
7
  var HandlerBadRequestError = class extends Error {
8
8
  constructor(message) {
@@ -12,6 +12,8 @@ var HandlerBadRequestError = class extends Error {
12
12
  };
13
13
  const SDK_VERSION_HEADER = "Hot-Updater-SDK-Version";
14
14
  const EXPLICIT_NO_UPDATE_MIN_SDK_VERSION = "0.31.0";
15
+ const DEFAULT_BUNDLE_LIST_LIMIT = 50;
16
+ const MAX_BUNDLE_LIST_LIMIT = 100;
15
17
  const supportsExplicitNoUpdateResponse = (request) => {
16
18
  const sdkVersion = request.headers.get(SDK_VERSION_HEADER)?.trim();
17
19
  if (!sdkVersion) return false;
@@ -61,6 +63,13 @@ const parseStringArraySearchParam = (url, key) => {
61
63
  const values = url.searchParams.getAll(key);
62
64
  return values.length > 0 ? values : void 0;
63
65
  };
66
+ const parsePositiveIntegerSearchParam = (url, key, defaultValue, maxValue) => {
67
+ const value = url.searchParams.get(key);
68
+ if (value === null) return defaultValue;
69
+ const parsed = Number(value);
70
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maxValue) throw new HandlerBadRequestError(`The '${key}' query parameter must be a positive integer between 1 and ${maxValue}.`);
71
+ return parsed;
72
+ };
64
73
  const requirePlatformParam = (params) => {
65
74
  const platform = requireRouteParam(params, "platform");
66
75
  if (!isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
@@ -129,7 +138,7 @@ const handleGetBundles = async (_params, request, api, context) => {
129
138
  const url = new URL(request.url);
130
139
  const channel = url.searchParams.get("channel") ?? void 0;
131
140
  const platform = url.searchParams.get("platform");
132
- const limit = Number(url.searchParams.get("limit")) || 50;
141
+ const limit = parsePositiveIntegerSearchParam(url, "limit", DEFAULT_BUNDLE_LIST_LIMIT, MAX_BUNDLE_LIST_LIMIT);
133
142
  const pageParam = url.searchParams.get("page");
134
143
  const offset = url.searchParams.get("offset");
135
144
  const after = url.searchParams.get("after") ?? void 0;
@@ -231,18 +240,19 @@ const routes = {
231
240
  */
232
241
  function createHandler(api, options = {}) {
233
242
  const basePath = options.basePath ?? "/api";
234
- const updateCheckEnabled = options.routes?.updateCheck ?? true;
235
- const versionEnabled = options.routes?.version ?? true;
236
- const bundlesEnabled = options.routes?.bundles ?? true;
243
+ const routeOptions = {
244
+ updateCheck: options.routes?.updateCheck ?? true,
245
+ bundles: options.routes?.bundles ?? false
246
+ };
237
247
  const router = require_internalRouter.createRouter();
238
- if (versionEnabled) require_internalRouter.addRoute(router, "GET", "/version", "version");
239
- if (updateCheckEnabled) {
248
+ require_internalRouter.addRoute(router, "GET", "/version", "version");
249
+ if (routeOptions.updateCheck) {
240
250
  require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdateWithCohort");
241
251
  require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort", "fingerprintUpdateWithCohort");
242
252
  require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdateWithCohort");
243
253
  require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort", "appVersionUpdateWithCohort");
244
254
  }
245
- if (bundlesEnabled) {
255
+ if (routeOptions.bundles) {
246
256
  require_internalRouter.addRoute(router, "GET", "/api/bundles/channels", "getChannels");
247
257
  require_internalRouter.addRoute(router, "GET", "/api/bundles/:id", "getBundle");
248
258
  require_internalRouter.addRoute(router, "GET", "/api/bundles", "getBundles");
@@ -18,27 +18,26 @@ interface HandlerOptions {
18
18
  * @default "/api"
19
19
  */
20
20
  basePath?: string;
21
+ /**
22
+ * Route groups to mount. Omit this option to use the default route groups.
23
+ * When provided, both route groups must be specified explicitly.
24
+ * The `/version` endpoint is always mounted for diagnostics.
25
+ */
21
26
  routes?: HandlerRoutes;
22
27
  }
23
28
  interface HandlerRoutes {
24
29
  /**
25
30
  * Controls whether update-check routes are mounted.
26
- * @default true
27
- */
28
- updateCheck?: boolean;
29
- /**
30
- * Controls whether the `/version` endpoint is mounted.
31
- * Useful for diagnostics and lightweight health/version checks.
32
- * @default true
31
+ * Defaults to `true` only when `routes` is omitted.
33
32
  */
34
- version?: boolean;
33
+ updateCheck: boolean;
35
34
  /**
36
35
  * Controls whether bundle management routes are mounted.
37
36
  * This includes `/api/bundles*`, which are used by the
38
37
  * CLI `standaloneRepository` plugin.
39
- * @default true
38
+ * Defaults to `false` only when `routes` is omitted.
40
39
  */
41
- bundles?: boolean;
40
+ bundles: boolean;
42
41
  }
43
42
  /**
44
43
  * Creates a Web Standard Request handler for Hot Updater API
@@ -18,27 +18,26 @@ interface HandlerOptions {
18
18
  * @default "/api"
19
19
  */
20
20
  basePath?: string;
21
+ /**
22
+ * Route groups to mount. Omit this option to use the default route groups.
23
+ * When provided, both route groups must be specified explicitly.
24
+ * The `/version` endpoint is always mounted for diagnostics.
25
+ */
21
26
  routes?: HandlerRoutes;
22
27
  }
23
28
  interface HandlerRoutes {
24
29
  /**
25
30
  * Controls whether update-check routes are mounted.
26
- * @default true
27
- */
28
- updateCheck?: boolean;
29
- /**
30
- * Controls whether the `/version` endpoint is mounted.
31
- * Useful for diagnostics and lightweight health/version checks.
32
- * @default true
31
+ * Defaults to `true` only when `routes` is omitted.
33
32
  */
34
- version?: boolean;
33
+ updateCheck: boolean;
35
34
  /**
36
35
  * Controls whether bundle management routes are mounted.
37
36
  * This includes `/api/bundles*`, which are used by the
38
37
  * CLI `standaloneRepository` plugin.
39
- * @default true
38
+ * Defaults to `false` only when `routes` is omitted.
40
39
  */
41
- bundles?: boolean;
40
+ bundles: boolean;
42
41
  }
43
42
  /**
44
43
  * Creates a Web Standard Request handler for Hot Updater API
package/dist/handler.mjs CHANGED
@@ -10,6 +10,8 @@ var HandlerBadRequestError = class extends Error {
10
10
  };
11
11
  const SDK_VERSION_HEADER = "Hot-Updater-SDK-Version";
12
12
  const EXPLICIT_NO_UPDATE_MIN_SDK_VERSION = "0.31.0";
13
+ const DEFAULT_BUNDLE_LIST_LIMIT = 50;
14
+ const MAX_BUNDLE_LIST_LIMIT = 100;
13
15
  const supportsExplicitNoUpdateResponse = (request) => {
14
16
  const sdkVersion = request.headers.get(SDK_VERSION_HEADER)?.trim();
15
17
  if (!sdkVersion) return false;
@@ -59,6 +61,13 @@ const parseStringArraySearchParam = (url, key) => {
59
61
  const values = url.searchParams.getAll(key);
60
62
  return values.length > 0 ? values : void 0;
61
63
  };
64
+ const parsePositiveIntegerSearchParam = (url, key, defaultValue, maxValue) => {
65
+ const value = url.searchParams.get(key);
66
+ if (value === null) return defaultValue;
67
+ const parsed = Number(value);
68
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maxValue) throw new HandlerBadRequestError(`The '${key}' query parameter must be a positive integer between 1 and ${maxValue}.`);
69
+ return parsed;
70
+ };
62
71
  const requirePlatformParam = (params) => {
63
72
  const platform = requireRouteParam(params, "platform");
64
73
  if (!isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
@@ -127,7 +136,7 @@ const handleGetBundles = async (_params, request, api, context) => {
127
136
  const url = new URL(request.url);
128
137
  const channel = url.searchParams.get("channel") ?? void 0;
129
138
  const platform = url.searchParams.get("platform");
130
- const limit = Number(url.searchParams.get("limit")) || 50;
139
+ const limit = parsePositiveIntegerSearchParam(url, "limit", DEFAULT_BUNDLE_LIST_LIMIT, MAX_BUNDLE_LIST_LIMIT);
131
140
  const pageParam = url.searchParams.get("page");
132
141
  const offset = url.searchParams.get("offset");
133
142
  const after = url.searchParams.get("after") ?? void 0;
@@ -229,18 +238,19 @@ const routes = {
229
238
  */
230
239
  function createHandler(api, options = {}) {
231
240
  const basePath = options.basePath ?? "/api";
232
- const updateCheckEnabled = options.routes?.updateCheck ?? true;
233
- const versionEnabled = options.routes?.version ?? true;
234
- const bundlesEnabled = options.routes?.bundles ?? true;
241
+ const routeOptions = {
242
+ updateCheck: options.routes?.updateCheck ?? true,
243
+ bundles: options.routes?.bundles ?? false
244
+ };
235
245
  const router = createRouter();
236
- if (versionEnabled) addRoute(router, "GET", "/version", "version");
237
- if (updateCheckEnabled) {
246
+ addRoute(router, "GET", "/version", "version");
247
+ if (routeOptions.updateCheck) {
238
248
  addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdateWithCohort");
239
249
  addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort", "fingerprintUpdateWithCohort");
240
250
  addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdateWithCohort");
241
251
  addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort", "appVersionUpdateWithCohort");
242
252
  }
243
- if (bundlesEnabled) {
253
+ if (routeOptions.bundles) {
244
254
  addRoute(router, "GET", "/api/bundles/channels", "getChannels");
245
255
  addRoute(router, "GET", "/api/bundles/:id", "getBundle");
246
256
  addRoute(router, "GET", "/api/bundles", "getBundles");
package/dist/index.d.cts CHANGED
@@ -4,4 +4,4 @@ import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.cjs";
4
4
  import { HOT_UPDATER_SERVER_VERSION } from "./version.cjs";
5
5
  import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.cjs";
6
6
  import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.cjs";
7
- export { type Bundle, ChannelsResponse, CreateBundleDiffDependencies, CreateBundleDiffInput, CreateBundleDiffOptions, CreateHotUpdaterOptions, DataResponse, HOT_UPDATER_SERVER_VERSION, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, type HotUpdaterClient, HotUpdaterDB, type Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createBundleDiff, createHandler, createHotUpdater };
7
+ export { Bundle, ChannelsResponse, CreateBundleDiffDependencies, CreateBundleDiffInput, CreateBundleDiffOptions, CreateHotUpdaterOptions, DataResponse, HOT_UPDATER_SERVER_VERSION, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createBundleDiff, createHandler, createHotUpdater };
package/dist/index.d.mts CHANGED
@@ -4,4 +4,4 @@ import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.mjs";
4
4
  import { HOT_UPDATER_SERVER_VERSION } from "./version.mjs";
5
5
  import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.mjs";
6
6
  import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.mjs";
7
- export { type Bundle, ChannelsResponse, CreateBundleDiffDependencies, CreateBundleDiffInput, CreateBundleDiffOptions, CreateHotUpdaterOptions, DataResponse, HOT_UPDATER_SERVER_VERSION, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, type HotUpdaterClient, HotUpdaterDB, type Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createBundleDiff, createHandler, createHotUpdater };
7
+ export { Bundle, ChannelsResponse, CreateBundleDiffDependencies, CreateBundleDiffInput, CreateBundleDiffOptions, CreateHotUpdaterOptions, DataResponse, HOT_UPDATER_SERVER_VERSION, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createBundleDiff, createHandler, createHotUpdater };
@@ -46,4 +46,4 @@ interface FumaDBFactory<Schemas extends AnySchema[]> {
46
46
  }
47
47
  type InferFumaDB<Factory extends FumaDBFactory<any>> = Factory extends FumaDBFactory<infer Schemas> ? FumaDB<Schemas> : never;
48
48
  //#endregion
49
- export type { FumaDBFactory, InferFumaDB };
49
+ export { type FumaDBFactory, type InferFumaDB };