@hot-updater/plugin-core 0.32.0 → 0.33.1

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.
@@ -1,12 +1,29 @@
1
- import { calculatePagination } from "./calculatePagination.mjs";
1
+ import { bundleMatchesQueryWhere, sortBundles } from "./queryBundles.mjs";
2
2
  import { createDatabasePlugin } from "./createDatabasePlugin.mjs";
3
3
  import { filterCompatibleAppVersions } from "./filterCompatibleAppVersions.mjs";
4
- import { bundleMatchesQueryWhere, sortBundles } from "./queryBundles.mjs";
5
4
  import { paginateBundles } from "./paginateBundles.mjs";
6
- import { getUpdateInfo } from "@hot-updater/js";
5
+ import { resolveUpdateInfoFromBundles } from "./resolveUpdateInfoFromBundles.mjs";
7
6
  import { orderBy } from "es-toolkit";
8
7
  import semver from "semver";
9
8
  //#region src/createBlobDatabasePlugin.ts
9
+ const STORAGE_OPERATION_CONCURRENCY = 8;
10
+ async function mapWithConcurrency(items, concurrency, mapper) {
11
+ const results = [];
12
+ let nextIndex = 0;
13
+ const workerCount = Math.min(concurrency, items.length);
14
+ await Promise.all(Array.from({ length: workerCount }, async () => {
15
+ while (true) {
16
+ const index = nextIndex;
17
+ nextIndex += 1;
18
+ if (index >= items.length) break;
19
+ results[index] = await mapper(items[index], index);
20
+ }
21
+ }));
22
+ return results;
23
+ }
24
+ async function forEachWithConcurrency(items, concurrency, mapper) {
25
+ await mapWithConcurrency(items, concurrency, mapper);
26
+ }
10
27
  function removeBundleInternalKeys(bundle) {
11
28
  const { _updateJsonKey, _oldUpdateJsonKey, ...pureBundle } = bundle;
12
29
  return pureBundle;
@@ -47,134 +64,43 @@ function resolveStorageTarget({ targetAppVersion, fingerprintHash }) {
47
64
  if (!target) throw new Error("target not found");
48
65
  return target;
49
66
  }
50
- const DEFAULT_DESC_ORDER = {
51
- field: "id",
52
- direction: "desc"
53
- };
54
- const MANAGEMENT_INDEX_PREFIX = "_index";
55
- const MANAGEMENT_INDEX_VERSION = 1;
56
- const DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE = 128;
57
- const ALL_SCOPE_CACHE_KEY = "*|*";
58
- function resolveManagementIndexPageSize(config) {
59
- const pageSize = config.managementIndexPageSize ?? DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE;
60
- if (!Number.isInteger(pageSize) || pageSize < 1) throw new Error("managementIndexPageSize must be a positive integer.");
61
- return pageSize;
62
- }
63
- function sortManagedBundles(bundles, orderBy = DEFAULT_DESC_ORDER) {
64
- return sortBundles(bundles, orderBy);
65
- }
66
- function isDefaultManagementOrder(orderBy) {
67
- return orderBy === void 0 || orderBy.field === DEFAULT_DESC_ORDER.field && orderBy.direction === DEFAULT_DESC_ORDER.direction;
68
- }
69
- function hasUnsupportedManagementFilters(where) {
70
- if (!where) return false;
71
- 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);
67
+ function targetVersionMutationKey(bundle) {
68
+ return `${bundle.channel}/${bundle.platform}`;
72
69
  }
73
- function getSupportedManagementScope(where, orderBy) {
74
- if (!isDefaultManagementOrder(orderBy) || hasUnsupportedManagementFilters(where)) return null;
75
- return {
76
- channel: where?.channel,
77
- platform: where?.platform
70
+ function getTargetVersionMutation(mutations, bundle) {
71
+ const key = targetVersionMutationKey(bundle);
72
+ const existingMutation = mutations.get(key);
73
+ if (existingMutation) return existingMutation;
74
+ const mutation = {
75
+ additions: /* @__PURE__ */ new Set(),
76
+ channel: bundle.channel,
77
+ platform: bundle.platform,
78
+ removals: /* @__PURE__ */ new Set()
78
79
  };
80
+ mutations.set(key, mutation);
81
+ return mutation;
79
82
  }
80
- function encodeScopePart(value) {
81
- return encodeURIComponent(value);
82
- }
83
- function getManagementScopeCacheKey({ channel, platform }) {
84
- return `${channel ?? "*"}|${platform ?? "*"}`;
85
- }
86
- function getManagementScopePrefix({ channel, platform }) {
87
- if (channel && platform) return `${MANAGEMENT_INDEX_PREFIX}/channel/${encodeScopePart(channel)}/platform/${platform}`;
88
- if (channel) return `${MANAGEMENT_INDEX_PREFIX}/channel/${encodeScopePart(channel)}`;
89
- if (platform) return `${MANAGEMENT_INDEX_PREFIX}/platform/${platform}`;
90
- return `${MANAGEMENT_INDEX_PREFIX}/all`;
91
- }
92
- function getManagementRootKey(scope) {
93
- return `${getManagementScopePrefix(scope)}/root.json`;
94
- }
95
- function getManagementPageKey(scope, pageIndex) {
96
- return `${getManagementScopePrefix(scope)}/pages/${String(pageIndex).padStart(4, "0")}.json`;
83
+ function addTargetVersionAddition(mutations, bundle) {
84
+ const targetAppVersion = normalizeTargetAppVersion(bundle.targetAppVersion);
85
+ if (targetAppVersion == null) return;
86
+ getTargetVersionMutation(mutations, bundle).additions.add(targetAppVersion);
97
87
  }
98
- function createBundleWithUpdateJsonKey(bundle) {
99
- const target = resolveStorageTarget(bundle);
100
- return {
101
- ...bundle,
102
- _updateJsonKey: `${bundle.channel}/${bundle.platform}/${target}/update.json`
103
- };
104
- }
105
- function getPageStartOffsets(pages) {
106
- const startOffsets = [];
107
- let offset = 0;
108
- for (const page of pages) {
109
- startOffsets.push(offset);
110
- offset += page.count;
111
- }
112
- return startOffsets;
88
+ function addTargetVersionRemoval(mutations, bundle) {
89
+ const targetAppVersion = normalizeTargetAppVersion(bundle.targetAppVersion);
90
+ if (targetAppVersion == null) return;
91
+ getTargetVersionMutation(mutations, bundle).removals.add(targetAppVersion);
113
92
  }
114
- function createEmptyManagementResult(limit) {
115
- return {
116
- data: [],
117
- pagination: calculatePagination(0, {
118
- limit,
119
- offset: 0
120
- })
121
- };
93
+ function getManagementListPrefixes(where) {
94
+ if (where?.channel && where.platform) return [`${where.channel}/${where.platform}/`];
95
+ if (where?.channel) return [`${where.channel}/`];
96
+ return [""];
122
97
  }
123
- function buildManagementIndexArtifacts(allBundles, pageSize) {
124
- const sortedAllBundles = sortManagedBundles(allBundles);
125
- const pages = /* @__PURE__ */ new Map();
126
- const scopes = [];
127
- const channels = [...new Set(sortedAllBundles.map((bundle) => bundle.channel))].sort();
128
- const addScope = (scope, scopeBundles, options) => {
129
- if (!options?.includeChannels && scopeBundles.length === 0) return;
130
- const pageKeys = [];
131
- const pageDescriptors = [];
132
- for (let pageIndex = 0; pageIndex * pageSize < scopeBundles.length; pageIndex++) {
133
- const page = scopeBundles.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
134
- const key = getManagementPageKey(scope, pageIndex);
135
- pages.set(key, page);
136
- pageKeys.push(key);
137
- pageDescriptors.push({
138
- key,
139
- count: page.length,
140
- firstId: page[0].id,
141
- lastId: page.at(-1).id
142
- });
143
- }
144
- const root = {
145
- version: MANAGEMENT_INDEX_VERSION,
146
- pageSize,
147
- total: scopeBundles.length,
148
- pages: pageDescriptors,
149
- ...options?.includeChannels ? { channels } : {}
150
- };
151
- scopes.push({
152
- cacheKey: getManagementScopeCacheKey(scope),
153
- rootKey: getManagementRootKey(scope),
154
- root,
155
- pageKeys
156
- });
157
- };
158
- addScope({}, sortedAllBundles, { includeChannels: true });
159
- for (const channel of channels) {
160
- const channelBundles = sortedAllBundles.filter((bundle) => bundle.channel === channel);
161
- addScope({ channel }, channelBundles);
162
- for (const platform of ["ios", "android"]) {
163
- const scopedBundles = channelBundles.filter((bundle) => bundle.platform === platform);
164
- addScope({
165
- channel,
166
- platform
167
- }, scopedBundles);
168
- }
169
- }
170
- for (const platform of ["ios", "android"]) {
171
- const platformBundles = sortedAllBundles.filter((bundle) => bundle.platform === platform);
172
- addScope({ platform }, platformBundles);
173
- }
174
- return {
175
- pages,
176
- scopes
177
- };
98
+ const DEFAULT_DESC_ORDER = {
99
+ field: "id",
100
+ direction: "desc"
101
+ };
102
+ function sortManagedBundles(bundles, orderBy = DEFAULT_DESC_ORDER) {
103
+ return sortBundles(bundles, orderBy);
178
104
  }
179
105
  /**
180
106
  * Creates a blob storage-based database plugin with lazy initialization.
@@ -185,339 +111,90 @@ function buildManagementIndexArtifacts(allBundles, pageSize) {
185
111
  */
186
112
  const createBlobDatabasePlugin = ({ name, factory }) => {
187
113
  return (config, hooks) => {
188
- const managementIndexPageSize = resolveManagementIndexPageSize(config);
189
- const { listObjects, loadObject, uploadObject, deleteObject, invalidatePaths, apiBasePath } = factory(config);
114
+ const { listObjects, loadObject, uploadObject, deleteObject, shouldSkipLoadObjectError, invalidatePaths, apiBasePath } = factory(config);
190
115
  const bundlesMap = /* @__PURE__ */ new Map();
191
116
  const pendingBundlesMap = /* @__PURE__ */ new Map();
192
- const managementRootCache = /* @__PURE__ */ new Map();
193
- const PLATFORMS = ["ios", "android"];
194
- const getAllManagementArtifact = (artifacts) => {
195
- const allArtifact = artifacts.scopes.find((scope) => scope.cacheKey === ALL_SCOPE_CACHE_KEY);
196
- if (!allArtifact) throw new Error("all-bundles management index artifact not found");
197
- return allArtifact;
198
- };
199
- const replaceManagementRootCache = (artifacts) => {
200
- managementRootCache.clear();
201
- for (const scope of artifacts.scopes) managementRootCache.set(scope.cacheKey, scope.root);
202
- };
203
- const createHydratedBundle = (bundle) => {
204
- const hydratedBundle = createBundleWithUpdateJsonKey(bundle);
205
- bundlesMap.set(hydratedBundle.id, hydratedBundle);
206
- return hydratedBundle;
207
- };
208
- const loadStoredManagementRoot = async (scope) => {
209
- const cacheKey = getManagementScopeCacheKey(scope);
210
- const storedRoot = await loadObject(getManagementRootKey(scope));
211
- if (storedRoot) {
212
- managementRootCache.set(cacheKey, storedRoot);
213
- return storedRoot;
214
- }
215
- managementRootCache.delete(cacheKey);
216
- return null;
217
- };
218
- const loadManagementPage = async (descriptor, pageCache) => {
219
- if (pageCache?.has(descriptor.key)) return pageCache.get(descriptor.key) ?? null;
220
- const page = await loadObject(descriptor.key);
221
- pageCache?.set(descriptor.key, page);
222
- return page;
223
- };
224
- const loadBundleFromManagementRoot = async (root, bundleId) => {
225
- const pageIndex = findPageIndexContainingId(root.pages, bundleId);
226
- if (pageIndex < 0) return null;
227
- const descriptor = root.pages[pageIndex];
228
- const page = await loadManagementPage(descriptor);
229
- if (!page) return null;
230
- return page.find((item) => item.id === bundleId) ?? null;
231
- };
232
- const loadAllBundlesFromRoot = async (root) => {
233
- const allBundles = [];
234
- const pageCache = /* @__PURE__ */ new Map();
235
- for (const descriptor of root.pages) {
236
- const page = await loadManagementPage(descriptor, pageCache);
237
- if (!page) return null;
238
- allBundles.push(...page);
117
+ const locallyDeletedBundleIds = /* @__PURE__ */ new Set();
118
+ const loadOptionalObject = async (key) => {
119
+ try {
120
+ return await loadObject(key);
121
+ } catch (error) {
122
+ if (shouldSkipLoadObjectError?.(error, key)) return null;
123
+ throw error;
239
124
  }
240
- return allBundles;
241
- };
242
- const persistManagementIndexArtifacts = async (nextArtifacts, previousArtifacts) => {
243
- for (const [key, page] of nextArtifacts.pages.entries()) await uploadObject(key, page);
244
- for (const scope of nextArtifacts.scopes) await uploadObject(scope.rootKey, scope.root);
245
- if (!previousArtifacts) return;
246
- const nextPageKeys = new Set(nextArtifacts.pages.keys());
247
- const nextRootKeys = new Set(nextArtifacts.scopes.map((scope) => scope.rootKey));
248
- for (const [key] of previousArtifacts.pages.entries()) if (!nextPageKeys.has(key)) await deleteObject(key).catch(() => {});
249
- for (const scope of previousArtifacts.scopes) if (!nextRootKeys.has(scope.rootKey)) await deleteObject(scope.rootKey).catch(() => {});
250
- };
251
- const ensureAllManagementRoot = async () => {
252
- const storedAllRoot = await loadStoredManagementRoot({});
253
- if (storedAllRoot && storedAllRoot.pageSize === managementIndexPageSize) return storedAllRoot;
254
- const rebuiltBundles = sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
255
- const nextArtifacts = buildManagementIndexArtifacts(rebuiltBundles, managementIndexPageSize);
256
- await persistManagementIndexArtifacts(nextArtifacts, storedAllRoot ? buildManagementIndexArtifacts(rebuiltBundles, storedAllRoot.pageSize) : void 0);
257
- replaceManagementRootCache(nextArtifacts);
258
- return getAllManagementArtifact(nextArtifacts).root;
259
- };
260
- const loadManagementScopeRoot = async (scope) => {
261
- const cacheKey = getManagementScopeCacheKey(scope);
262
- if (cacheKey === ALL_SCOPE_CACHE_KEY) return ensureAllManagementRoot();
263
- const storedRoot = await loadStoredManagementRoot(scope);
264
- if (storedRoot) return storedRoot;
265
- await ensureAllManagementRoot();
266
- const storedScopedRoot = await loadStoredManagementRoot(scope);
267
- if (storedScopedRoot) return storedScopedRoot;
268
- managementRootCache.set(cacheKey, null);
269
- return null;
270
- };
271
- const loadAllBundlesForManagementFallback = async () => {
272
- const allRoot = await loadManagementScopeRoot({});
273
- if (allRoot) {
274
- const pagedBundles = await loadAllBundlesFromRoot(allRoot);
275
- if (pagedBundles) return pagedBundles;
276
- }
277
- return sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
278
- };
279
- const loadCurrentBundlesForIndexRebuild = async () => {
280
- return loadAllBundlesForManagementFallback();
281
125
  };
282
- const findPageIndexContainingId = (pages, id) => {
283
- return pages.findIndex((page) => id.localeCompare(page.firstId) <= 0 && id.localeCompare(page.lastId) >= 0);
126
+ const loadAllBundlesForManagementFallback = async (where) => {
127
+ return sortManagedBundles((await reloadBundles(getManagementListPrefixes(where))).map((bundle) => removeBundleInternalKeys(bundle)));
284
128
  };
285
- const readPagedBundles = async ({ root, limit, offset, cursor }) => {
286
- if (root.total === 0 || root.pages.length === 0) return createEmptyManagementResult(limit);
287
- const pageStartOffsets = getPageStartOffsets(root.pages);
288
- const pageCache = /* @__PURE__ */ new Map();
289
- if (offset !== void 0) {
290
- const normalizedOffset = Math.max(0, offset);
291
- if (normalizedOffset >= root.total) return {
292
- data: [],
293
- pagination: calculatePagination(root.total, {
294
- limit,
295
- offset: normalizedOffset
296
- })
297
- };
298
- let pageIndex = 0;
299
- for (let index = pageStartOffsets.length - 1; index >= 0; index--) if ((pageStartOffsets[index] ?? 0) <= normalizedOffset) {
300
- pageIndex = index;
301
- break;
302
- }
303
- const startInPage = normalizedOffset - (pageStartOffsets[pageIndex] ?? 0);
304
- const data = [];
305
- for (let currentPageIndex = pageIndex; currentPageIndex < root.pages.length && (limit <= 0 || data.length < limit); currentPageIndex++) {
306
- const descriptor = root.pages[currentPageIndex];
307
- const page = await loadManagementPage(descriptor, pageCache);
308
- if (!page) return paginateBundles({
309
- bundles: await loadAllBundlesForManagementFallback(),
310
- limit,
311
- offset: normalizedOffset
312
- });
313
- data.push(...currentPageIndex === pageIndex ? page.slice(startInPage) : page);
314
- }
315
- const paginatedData = limit > 0 ? data.slice(0, limit) : data;
316
- return {
317
- data: paginatedData,
318
- pagination: {
319
- ...calculatePagination(root.total, {
320
- limit,
321
- offset: normalizedOffset
322
- }),
323
- ...paginatedData.length > 0 && normalizedOffset + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
324
- ...paginatedData.length > 0 && normalizedOffset > 0 ? { previousCursor: paginatedData[0]?.id } : {}
325
- }
326
- };
327
- }
328
- if (cursor?.after) {
329
- let pageIndex = root.pages.findIndex((page) => {
330
- const containsCursor = cursor.after.localeCompare(page.firstId) <= 0 && cursor.after.localeCompare(page.lastId) >= 0;
331
- const wholePageEligible = cursor.after.localeCompare(page.firstId) > 0;
332
- return containsCursor || wholePageEligible;
333
- });
334
- if (pageIndex < 0) return {
335
- data: [],
336
- pagination: {
337
- ...calculatePagination(root.total, {
338
- limit,
339
- offset: root.total
340
- }),
341
- previousCursor: cursor.after
342
- }
343
- };
344
- const data = [];
345
- let startIndex = null;
346
- while (pageIndex < root.pages.length && (limit <= 0 || data.length < limit)) {
347
- const descriptor = root.pages[pageIndex];
348
- const page = await loadManagementPage(descriptor, pageCache);
349
- if (!page) return paginateBundles({
350
- bundles: await loadAllBundlesForManagementFallback(),
351
- limit,
352
- cursor
353
- });
354
- const containsCursor = cursor.after.localeCompare(descriptor.firstId) <= 0 && cursor.after.localeCompare(descriptor.lastId) >= 0;
355
- let eligiblePageBundles = page;
356
- if (containsCursor) {
357
- const startInPage = page.findIndex((bundle) => bundle.id.localeCompare(cursor.after) < 0);
358
- if (startInPage < 0) eligiblePageBundles = [];
359
- else {
360
- eligiblePageBundles = page.slice(startInPage);
361
- startIndex ??= (pageStartOffsets[pageIndex] ?? 0) + startInPage;
362
- }
363
- } else if (eligiblePageBundles.length > 0) startIndex ??= pageStartOffsets[pageIndex] ?? 0;
364
- data.push(...eligiblePageBundles);
365
- if (limit > 0 && data.length >= limit) break;
366
- pageIndex += 1;
367
- }
368
- const paginatedData = limit > 0 ? data.slice(0, limit) : data;
369
- const resolvedStartIndex = startIndex ?? root.total;
370
- return {
371
- data: paginatedData,
372
- pagination: {
373
- ...calculatePagination(root.total, {
374
- limit,
375
- offset: resolvedStartIndex
376
- }),
377
- ...paginatedData.length > 0 && resolvedStartIndex + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
378
- ...paginatedData.length > 0 && resolvedStartIndex > 0 ? { previousCursor: paginatedData[0]?.id } : {}
379
- }
380
- };
381
- }
382
- if (cursor?.before) {
383
- let pageIndex = -1;
384
- for (let index = root.pages.length - 1; index >= 0; index--) {
385
- const page = root.pages[index];
386
- const containsCursor = cursor.before.localeCompare(page.firstId) <= 0 && cursor.before.localeCompare(page.lastId) >= 0;
387
- const wholePageEligible = cursor.before.localeCompare(page.lastId) < 0;
388
- if (containsCursor || wholePageEligible) {
389
- pageIndex = index;
390
- break;
391
- }
392
- }
393
- if (pageIndex < 0) return createEmptyManagementResult(limit);
394
- let startIndex = null;
395
- let collected = [];
396
- while (pageIndex >= 0 && (limit <= 0 || collected.length < limit)) {
397
- const descriptor = root.pages[pageIndex];
398
- const page = await loadManagementPage(descriptor, pageCache);
399
- if (!page) return paginateBundles({
400
- bundles: await loadAllBundlesForManagementFallback(),
401
- limit,
402
- cursor
403
- });
404
- const eligiblePageBundles = cursor.before.localeCompare(descriptor.firstId) <= 0 && cursor.before.localeCompare(descriptor.lastId) >= 0 ? page.filter((bundle) => bundle.id.localeCompare(cursor.before) > 0) : page;
405
- collected = [...eligiblePageBundles, ...collected];
406
- if (eligiblePageBundles.length > 0) startIndex = pageStartOffsets[pageIndex] ?? 0;
407
- if (limit > 0 && collected.length >= limit) break;
408
- pageIndex -= 1;
409
- }
410
- if (startIndex === null || collected.length === 0) return createEmptyManagementResult(limit);
411
- let paginatedData = collected;
412
- if (limit > 0 && collected.length > limit) {
413
- const dropCount = collected.length - limit;
414
- paginatedData = collected.slice(dropCount);
415
- startIndex += dropCount;
416
- }
417
- const pagination = calculatePagination(root.total, {
418
- limit,
419
- offset: startIndex
420
- });
421
- return {
422
- data: paginatedData,
423
- pagination: {
424
- ...pagination,
425
- ...paginatedData.length > 0 && startIndex + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
426
- ...paginatedData.length > 0 && startIndex > 0 ? { previousCursor: paginatedData[0]?.id } : {}
427
- }
428
- };
429
- }
430
- const pageIndex = 0;
431
- const startInPage = 0;
432
- const data = [];
433
- for (let currentPageIndex = pageIndex; currentPageIndex < root.pages.length && (limit <= 0 || data.length < limit); currentPageIndex++) {
434
- const descriptor = root.pages[currentPageIndex];
435
- const page = await loadManagementPage(descriptor, pageCache);
436
- if (!page) return paginateBundles({
437
- bundles: await loadAllBundlesForManagementFallback(),
438
- limit,
439
- cursor
129
+ const cacheBundlesFromObject = (key, bundles) => {
130
+ for (const bundle of bundles) {
131
+ if (locallyDeletedBundleIds.has(bundle.id) || pendingBundlesMap.has(bundle.id)) continue;
132
+ bundlesMap.set(bundle.id, {
133
+ ...bundle,
134
+ _updateJsonKey: key
440
135
  });
441
- data.push(...currentPageIndex === pageIndex ? page.slice(startInPage) : page);
442
136
  }
443
- const paginatedData = limit > 0 ? data.slice(0, limit) : data;
444
- return {
445
- data: paginatedData,
446
- pagination: {
447
- ...calculatePagination(root.total, {
448
- limit,
449
- offset: 0
450
- }),
451
- ...paginatedData.length > 0 && paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {}
452
- }
453
- };
454
137
  };
455
- async function reloadBundles() {
138
+ const loadBundleObject = async (key) => {
139
+ const bundles = await loadOptionalObject(key) ?? [];
140
+ cacheBundlesFromObject(key, bundles);
141
+ return bundles;
142
+ };
143
+ async function reloadBundles(prefixes = [""]) {
456
144
  bundlesMap.clear();
457
- const filePromises = (await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)).map(async (key) => {
458
- return (await loadObject(key) ?? []).map((bundle) => ({
145
+ pendingBundlesMap.clear();
146
+ locallyDeletedBundleIds.clear();
147
+ const allBundles = (await mapWithConcurrency((await mapWithConcurrency(prefixes, STORAGE_OPERATION_CONCURRENCY, (prefix) => listObjects(prefix))).flat().filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)), STORAGE_OPERATION_CONCURRENCY, async (key) => {
148
+ return (await loadBundleObject(key)).map((bundle) => ({
459
149
  ...bundle,
460
150
  _updateJsonKey: key
461
151
  }));
462
- });
463
- const allBundles = (await Promise.all(filePromises)).flat();
152
+ })).flat();
464
153
  for (const bundle of allBundles) bundlesMap.set(bundle.id, bundle);
465
154
  for (const [id, bundle] of pendingBundlesMap.entries()) bundlesMap.set(id, bundle);
466
- return orderBy(allBundles, [(v) => v.id], ["desc"]);
155
+ return orderBy(Array.from(bundlesMap.values()), [(v) => v.id], ["desc"]);
467
156
  }
468
- /**
469
- * Updates target-app-versions.json for each channel on the given platform.
470
- * Returns true if the file was updated, false if no changes were made.
471
- */
472
- async function updateTargetVersionsForPlatform(platform) {
473
- const updateJsonPattern = new RegExp(`^[^/]+/${platform}/[^/]+/update\\.json$`);
474
- const targetVersionsPattern = new RegExp(`^[^/]+/${platform}/target-app-versions\\.json$`);
475
- const allKeys = await listObjects("");
476
- const updateJsonKeys = allKeys.filter((key) => updateJsonPattern.test(key));
477
- const targetVersionsKeys = allKeys.filter((key) => targetVersionsPattern.test(key));
478
- const keysByChannel = updateJsonKeys.reduce((acc, key) => {
479
- const channel = key.split("/")[0];
480
- acc[channel] = acc[channel] || [];
481
- acc[channel].push(key);
482
- return acc;
483
- }, {});
484
- for (const key of targetVersionsKeys) {
485
- const channel = key.split("/")[0];
486
- if (!keysByChannel[channel]) keysByChannel[channel] = [];
487
- }
488
- for (const channel of Object.keys(keysByChannel)) {
489
- const updateKeys = keysByChannel[channel];
157
+ async function applyTargetVersionMutations(mutations) {
158
+ await Promise.all(Array.from(mutations.values()).map(async ({ additions, channel, platform, removals }) => {
490
159
  const targetKey = `${channel}/${platform}/target-app-versions.json`;
491
- const currentVersions = updateKeys.map((key) => key.split("/")[2]);
492
- const oldTargetVersions = await loadObject(targetKey) ?? [];
493
- const newTargetVersions = oldTargetVersions.filter((v) => currentVersions.includes(v));
494
- for (const v of currentVersions) if (!newTargetVersions.includes(v)) newTargetVersions.push(v);
160
+ const oldTargetVersions = await loadOptionalObject(targetKey) ?? [];
161
+ const newTargetVersions = oldTargetVersions.filter((version) => !removals.has(version) || additions.has(version));
162
+ for (const version of additions) if (!newTargetVersions.includes(version)) newTargetVersions.push(version);
495
163
  if (JSON.stringify(oldTargetVersions) !== JSON.stringify(newTargetVersions)) await uploadObject(targetKey, newTargetVersions);
496
- }
164
+ }));
497
165
  }
498
- const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }) => {
499
- const matchingVersions = filterCompatibleAppVersions(await loadObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion);
500
- return getUpdateInfo((await Promise.allSettled(matchingVersions.map(async (targetAppVersion) => {
501
- return await loadObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`) ?? [];
502
- }))).filter((entry) => entry.status === "fulfilled").flatMap((entry) => entry.value), {
503
- _updateStrategy: "appVersion",
504
- appVersion,
505
- bundleId,
506
- channel,
507
- cohort,
508
- minBundleId,
509
- platform
166
+ const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }, context) => {
167
+ const bundles = (await mapWithConcurrency(filterCompatibleAppVersions(await loadOptionalObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion), STORAGE_OPERATION_CONCURRENCY, async (targetAppVersion) => {
168
+ return loadBundleObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`);
169
+ })).flat();
170
+ return resolveUpdateInfoFromBundles({
171
+ args: {
172
+ _updateStrategy: "appVersion",
173
+ appVersion,
174
+ bundleId,
175
+ channel,
176
+ cohort,
177
+ minBundleId,
178
+ platform
179
+ },
180
+ bundles,
181
+ context
510
182
  });
511
183
  };
512
- const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }) => {
513
- return getUpdateInfo(await loadObject(`${channel}/${platform}/${fingerprintHash}/update.json`) ?? [], {
514
- _updateStrategy: "fingerprint",
515
- bundleId,
516
- channel,
517
- cohort,
518
- fingerprintHash,
519
- minBundleId,
520
- platform
184
+ const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }, context) => {
185
+ const bundles = await loadBundleObject(`${channel}/${platform}/${fingerprintHash}/update.json`);
186
+ return resolveUpdateInfoFromBundles({
187
+ args: {
188
+ _updateStrategy: "fingerprint",
189
+ bundleId,
190
+ channel,
191
+ cohort,
192
+ fingerprintHash,
193
+ minBundleId,
194
+ platform
195
+ },
196
+ bundles,
197
+ context
521
198
  });
522
199
  };
523
200
  const addAppVersionInvalidationPaths = (pathsToInvalidate, { platform, channel, targetAppVersion }) => {
@@ -539,48 +216,27 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
539
216
  targetAppVersion
540
217
  });
541
218
  };
542
- return createDatabasePlugin({
219
+ const createPlugin = createDatabasePlugin({
543
220
  name,
544
221
  factory: () => ({
545
222
  supportsCursorPagination: true,
546
223
  async getBundleById(bundleId) {
224
+ if (locallyDeletedBundleIds.has(bundleId)) return null;
547
225
  const pendingBundle = pendingBundlesMap.get(bundleId);
548
226
  if (pendingBundle) return removeBundleInternalKeys(pendingBundle);
549
227
  const bundle = bundlesMap.get(bundleId);
550
228
  if (bundle) return removeBundleInternalKeys(bundle);
551
- const allRoot = await loadManagementScopeRoot({});
552
- if (allRoot) {
553
- const matchedBundle = await loadBundleFromManagementRoot(allRoot, bundleId);
554
- if (matchedBundle) return removeBundleInternalKeys(createHydratedBundle(matchedBundle));
555
- managementRootCache.delete(ALL_SCOPE_CACHE_KEY);
556
- const refreshedAllRoot = await loadStoredManagementRoot({});
557
- if (refreshedAllRoot) {
558
- const refreshedBundle = await loadBundleFromManagementRoot(refreshedAllRoot, bundleId);
559
- if (refreshedBundle) return removeBundleInternalKeys(createHydratedBundle(refreshedBundle));
560
- }
561
- }
562
229
  const matchedBundle = (await reloadBundles()).find((item) => item.id === bundleId);
563
230
  if (!matchedBundle) return null;
564
231
  return removeBundleInternalKeys(matchedBundle);
565
232
  },
566
- async getUpdateInfo(args) {
567
- if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args);
568
- return getFingerprintUpdateInfo(args);
233
+ async getUpdateInfo(args, context) {
234
+ if (args._updateStrategy === "appVersion") return getAppVersionUpdateInfo(args, context);
235
+ return getFingerprintUpdateInfo(args, context);
569
236
  },
570
237
  async getBundles(options) {
571
238
  const { where, limit, offset, orderBy, cursor } = options;
572
- const scope = getSupportedManagementScope(where, orderBy);
573
- if (scope) {
574
- const root = await loadManagementScopeRoot(scope);
575
- if (!root) return createEmptyManagementResult(limit);
576
- return readPagedBundles({
577
- root,
578
- limit,
579
- offset,
580
- cursor
581
- });
582
- }
583
- let allBundles = await loadAllBundlesForManagementFallback();
239
+ let allBundles = await loadAllBundlesForManagementFallback(where);
584
240
  if (where) allBundles = allBundles.filter((bundle) => bundleMatchesQueryWhere(bundle, where));
585
241
  return paginateBundles({
586
242
  bundles: allBundles,
@@ -591,18 +247,16 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
591
247
  });
592
248
  },
593
249
  async getChannels() {
594
- return (await loadManagementScopeRoot({}))?.channels ?? [];
250
+ return [...new Set((await loadAllBundlesForManagementFallback()).map((bundle) => bundle.channel))].sort();
595
251
  },
596
252
  async commitBundle({ changedSets }) {
597
253
  if (changedSets.length === 0) return;
598
254
  const changedBundlesByKey = {};
599
255
  const removalsByKey = {};
256
+ const targetVersionRemovalsByKey = {};
600
257
  const pathsToInvalidate = /* @__PURE__ */ new Set();
601
- let isTargetAppVersionChanged = false;
602
- let isChannelChanged = false;
258
+ const targetVersionMutations = /* @__PURE__ */ new Map();
603
259
  for (const { operation, data } of changedSets) {
604
- if (data.targetAppVersion !== void 0) isTargetAppVersionChanged = true;
605
- if (operation === "update" && data.channel !== void 0) isChannelChanged = true;
606
260
  if (operation === "insert") {
607
261
  const target = resolveStorageTarget(data);
608
262
  const key = `${data.channel}/${data.platform}/${target}/update.json`;
@@ -610,10 +264,12 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
610
264
  ...data,
611
265
  _updateJsonKey: key
612
266
  };
267
+ locallyDeletedBundleIds.delete(data.id);
613
268
  bundlesMap.set(data.id, bundleWithKey);
614
269
  pendingBundlesMap.set(data.id, bundleWithKey);
615
270
  changedBundlesByKey[key] = changedBundlesByKey[key] || [];
616
271
  changedBundlesByKey[key].push(removeBundleInternalKeys(bundleWithKey));
272
+ addTargetVersionAddition(targetVersionMutations, data);
617
273
  addLookupInvalidationPaths(pathsToInvalidate, data);
618
274
  continue;
619
275
  }
@@ -623,9 +279,12 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
623
279
  if (!bundle) throw new Error("Bundle to delete not found");
624
280
  bundlesMap.delete(data.id);
625
281
  pendingBundlesMap.delete(data.id);
282
+ locallyDeletedBundleIds.add(data.id);
626
283
  const key = bundle._updateJsonKey;
627
284
  removalsByKey[key] = removalsByKey[key] || [];
628
285
  removalsByKey[key].push(bundle.id);
286
+ targetVersionRemovalsByKey[key] = targetVersionRemovalsByKey[key] || [];
287
+ targetVersionRemovalsByKey[key].push(bundle);
629
288
  addLookupInvalidationPaths(pathsToInvalidate, bundle);
630
289
  continue;
631
290
  }
@@ -642,11 +301,14 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
642
301
  const oldKey = bundle._updateJsonKey;
643
302
  removalsByKey[oldKey] = removalsByKey[oldKey] || [];
644
303
  removalsByKey[oldKey].push(bundle.id);
304
+ targetVersionRemovalsByKey[oldKey] = targetVersionRemovalsByKey[oldKey] || [];
305
+ targetVersionRemovalsByKey[oldKey].push(bundle);
645
306
  changedBundlesByKey[newKey] = changedBundlesByKey[newKey] || [];
646
307
  updatedBundle._oldUpdateJsonKey = oldKey;
647
308
  updatedBundle._updateJsonKey = newKey;
648
309
  bundlesMap.set(data.id, updatedBundle);
649
310
  pendingBundlesMap.set(data.id, updatedBundle);
311
+ locallyDeletedBundleIds.delete(data.id);
650
312
  changedBundlesByKey[newKey].push(removeBundleInternalKeys(updatedBundle));
651
313
  const oldChannel = bundle.channel;
652
314
  const nextChannel = updatedBundle.channel;
@@ -657,6 +319,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
657
319
  channel: nextChannel
658
320
  });
659
321
  }
322
+ addTargetVersionAddition(targetVersionMutations, updatedBundle);
660
323
  addLookupInvalidationPaths(pathsToInvalidate, updatedBundle);
661
324
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
662
325
  continue;
@@ -664,20 +327,24 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
664
327
  const currentKey = bundle._updateJsonKey;
665
328
  bundlesMap.set(data.id, updatedBundle);
666
329
  pendingBundlesMap.set(data.id, updatedBundle);
330
+ locallyDeletedBundleIds.delete(data.id);
667
331
  changedBundlesByKey[currentKey] = changedBundlesByKey[currentKey] || [];
668
332
  changedBundlesByKey[currentKey].push(removeBundleInternalKeys(updatedBundle));
669
333
  addLookupInvalidationPaths(pathsToInvalidate, updatedBundle);
334
+ addTargetVersionAddition(targetVersionMutations, updatedBundle);
670
335
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
671
336
  }
672
337
  }
673
- for (const oldKey of Object.keys(removalsByKey)) await (async () => {
674
- const updatedBundles = (await loadObject(oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
338
+ await forEachWithConcurrency(Object.keys(removalsByKey), STORAGE_OPERATION_CONCURRENCY, async (oldKey) => {
339
+ const updatedBundles = (await loadOptionalObject(oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
675
340
  updatedBundles.sort((a, b) => b.id.localeCompare(a.id));
676
- if (updatedBundles.length === 0) await deleteObject(oldKey);
677
- else await uploadObject(oldKey, updatedBundles);
678
- })();
679
- for (const key of Object.keys(changedBundlesByKey)) await (async () => {
680
- const currentBundles = await loadObject(key) ?? [];
341
+ if (updatedBundles.length === 0) {
342
+ await deleteObject(oldKey);
343
+ for (const removedBundle of targetVersionRemovalsByKey[oldKey] ?? []) addTargetVersionRemoval(targetVersionMutations, removedBundle);
344
+ } else await uploadObject(oldKey, updatedBundles);
345
+ });
346
+ await forEachWithConcurrency(Object.keys(changedBundlesByKey), STORAGE_OPERATION_CONCURRENCY, async (key) => {
347
+ const currentBundles = await loadOptionalObject(key) ?? [];
681
348
  const pureBundles = changedBundlesByKey[key].map((bundle) => bundle);
682
349
  for (const changedBundle of pureBundles) {
683
350
  const index = currentBundles.findIndex((b) => b.id === changedBundle.id);
@@ -686,22 +353,8 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
686
353
  }
687
354
  currentBundles.sort((a, b) => b.id.localeCompare(a.id));
688
355
  await uploadObject(key, currentBundles);
689
- })();
690
- if (isTargetAppVersionChanged || isChannelChanged) for (const platform of PLATFORMS) await updateTargetVersionsForPlatform(platform);
691
- const currentIndexBundles = await loadCurrentBundlesForIndexRebuild();
692
- const nextIndexMap = new Map(currentIndexBundles.map((bundle) => [bundle.id, bundle]));
693
- for (const { operation, data } of changedSets) {
694
- if (operation === "delete") {
695
- nextIndexMap.delete(data.id);
696
- continue;
697
- }
698
- nextIndexMap.set(data.id, data);
699
- }
700
- const nextIndexBundles = sortManagedBundles(Array.from(nextIndexMap.values()));
701
- const previousArtifacts = buildManagementIndexArtifacts(currentIndexBundles, managementIndexPageSize);
702
- const nextArtifacts = buildManagementIndexArtifacts(nextIndexBundles, managementIndexPageSize);
703
- await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
704
- replaceManagementRootCache(nextArtifacts);
356
+ });
357
+ if (targetVersionMutations.size > 0) await applyTargetVersionMutations(targetVersionMutations);
705
358
  const encondedPaths = /* @__PURE__ */ new Set();
706
359
  for (const path of pathsToInvalidate) encondedPaths.add(encodeURI(path));
707
360
  await invalidatePaths(Array.from(encondedPaths));
@@ -709,6 +362,9 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
709
362
  }
710
363
  })
711
364
  })({}, hooks);
365
+ return () => {
366
+ return createPlugin();
367
+ };
712
368
  };
713
369
  };
714
370
  //#endregion