@hot-updater/plugin-core 0.33.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,7 +1,6 @@
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
5
  import { resolveUpdateInfoFromBundles } from "./resolveUpdateInfoFromBundles.mjs";
7
6
  import { orderBy } from "es-toolkit";
@@ -65,140 +64,43 @@ function resolveStorageTarget({ targetAppVersion, fingerprintHash }) {
65
64
  if (!target) throw new Error("target not found");
66
65
  return target;
67
66
  }
68
- const DEFAULT_DESC_ORDER = {
69
- field: "id",
70
- direction: "desc"
71
- };
72
- const MANAGEMENT_INDEX_PREFIX = "_index";
73
- const MANAGEMENT_INDEX_VERSION = 1;
74
- const DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE = 128;
75
- const ALL_SCOPE_CACHE_KEY = "*|*";
76
- function summarizeManagementIndexArtifacts(artifacts) {
77
- return {
78
- pagesWritten: artifacts.pages.size,
79
- scopesWritten: artifacts.scopes.length
80
- };
81
- }
82
- function resolveManagementIndexPageSize(config) {
83
- const pageSize = config.managementIndexPageSize ?? DEFAULT_MANAGEMENT_INDEX_PAGE_SIZE;
84
- if (!Number.isInteger(pageSize) || pageSize < 1) throw new Error("managementIndexPageSize must be a positive integer.");
85
- return pageSize;
86
- }
87
- function sortManagedBundles(bundles, orderBy = DEFAULT_DESC_ORDER) {
88
- return sortBundles(bundles, orderBy);
89
- }
90
- function isDefaultManagementOrder(orderBy) {
91
- return orderBy === void 0 || orderBy.field === DEFAULT_DESC_ORDER.field && orderBy.direction === DEFAULT_DESC_ORDER.direction;
67
+ function targetVersionMutationKey(bundle) {
68
+ return `${bundle.channel}/${bundle.platform}`;
92
69
  }
93
- function hasUnsupportedManagementFilters(where) {
94
- if (!where) return false;
95
- 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);
96
- }
97
- function getSupportedManagementScope(where, orderBy) {
98
- if (!isDefaultManagementOrder(orderBy) || hasUnsupportedManagementFilters(where)) return null;
99
- return {
100
- channel: where?.channel,
101
- 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()
102
79
  };
80
+ mutations.set(key, mutation);
81
+ return mutation;
103
82
  }
104
- function encodeScopePart(value) {
105
- return encodeURIComponent(value);
106
- }
107
- function getManagementScopeCacheKey({ channel, platform }) {
108
- return `${channel ?? "*"}|${platform ?? "*"}`;
109
- }
110
- function getManagementScopePrefix({ channel, platform }) {
111
- if (channel && platform) return `${MANAGEMENT_INDEX_PREFIX}/channel/${encodeScopePart(channel)}/platform/${platform}`;
112
- if (channel) return `${MANAGEMENT_INDEX_PREFIX}/channel/${encodeScopePart(channel)}`;
113
- if (platform) return `${MANAGEMENT_INDEX_PREFIX}/platform/${platform}`;
114
- return `${MANAGEMENT_INDEX_PREFIX}/all`;
115
- }
116
- function getManagementRootKey(scope) {
117
- return `${getManagementScopePrefix(scope)}/root.json`;
118
- }
119
- function getManagementPageKey(scope, pageIndex) {
120
- return `${getManagementScopePrefix(scope)}/pages/${String(pageIndex).padStart(4, "0")}.json`;
121
- }
122
- function createBundleWithUpdateJsonKey(bundle) {
123
- const target = resolveStorageTarget(bundle);
124
- return {
125
- ...bundle,
126
- _updateJsonKey: `${bundle.channel}/${bundle.platform}/${target}/update.json`
127
- };
83
+ function addTargetVersionAddition(mutations, bundle) {
84
+ const targetAppVersion = normalizeTargetAppVersion(bundle.targetAppVersion);
85
+ if (targetAppVersion == null) return;
86
+ getTargetVersionMutation(mutations, bundle).additions.add(targetAppVersion);
128
87
  }
129
- function getPageStartOffsets(pages) {
130
- const startOffsets = [];
131
- let offset = 0;
132
- for (const page of pages) {
133
- startOffsets.push(offset);
134
- offset += page.count;
135
- }
136
- 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);
137
92
  }
138
- function createEmptyManagementResult(limit) {
139
- return {
140
- data: [],
141
- pagination: calculatePagination(0, {
142
- limit,
143
- offset: 0
144
- })
145
- };
93
+ function getManagementListPrefixes(where) {
94
+ if (where?.channel && where.platform) return [`${where.channel}/${where.platform}/`];
95
+ if (where?.channel) return [`${where.channel}/`];
96
+ return [""];
146
97
  }
147
- function buildManagementIndexArtifacts(allBundles, pageSize) {
148
- const sortedAllBundles = sortManagedBundles(allBundles);
149
- const pages = /* @__PURE__ */ new Map();
150
- const scopes = [];
151
- const channels = [...new Set(sortedAllBundles.map((bundle) => bundle.channel))].sort();
152
- const addScope = (scope, scopeBundles, options) => {
153
- if (!options?.includeChannels && scopeBundles.length === 0) return;
154
- const pageKeys = [];
155
- const pageDescriptors = [];
156
- for (let pageIndex = 0; pageIndex * pageSize < scopeBundles.length; pageIndex++) {
157
- const page = scopeBundles.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
158
- const key = getManagementPageKey(scope, pageIndex);
159
- pages.set(key, page);
160
- pageKeys.push(key);
161
- pageDescriptors.push({
162
- key,
163
- count: page.length,
164
- firstId: page[0].id,
165
- lastId: page.at(-1).id
166
- });
167
- }
168
- const root = {
169
- version: MANAGEMENT_INDEX_VERSION,
170
- pageSize,
171
- total: scopeBundles.length,
172
- pages: pageDescriptors,
173
- ...options?.includeChannels ? { channels } : {}
174
- };
175
- scopes.push({
176
- cacheKey: getManagementScopeCacheKey(scope),
177
- rootKey: getManagementRootKey(scope),
178
- root,
179
- pageKeys
180
- });
181
- };
182
- addScope({}, sortedAllBundles, { includeChannels: true });
183
- for (const channel of channels) {
184
- const channelBundles = sortedAllBundles.filter((bundle) => bundle.channel === channel);
185
- addScope({ channel }, channelBundles);
186
- for (const platform of ["ios", "android"]) {
187
- const scopedBundles = channelBundles.filter((bundle) => bundle.platform === platform);
188
- addScope({
189
- channel,
190
- platform
191
- }, scopedBundles);
192
- }
193
- }
194
- for (const platform of ["ios", "android"]) {
195
- const platformBundles = sortedAllBundles.filter((bundle) => bundle.platform === platform);
196
- addScope({ platform }, platformBundles);
197
- }
198
- return {
199
- pages,
200
- scopes
201
- };
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);
202
104
  }
203
105
  /**
204
106
  * Creates a blob storage-based database plugin with lazy initialization.
@@ -209,11 +111,10 @@ function buildManagementIndexArtifacts(allBundles, pageSize) {
209
111
  */
210
112
  const createBlobDatabasePlugin = ({ name, factory }) => {
211
113
  return (config, hooks) => {
212
- const managementIndexPageSize = resolveManagementIndexPageSize(config);
213
114
  const { listObjects, loadObject, uploadObject, deleteObject, shouldSkipLoadObjectError, invalidatePaths, apiBasePath } = factory(config);
214
115
  const bundlesMap = /* @__PURE__ */ new Map();
215
116
  const pendingBundlesMap = /* @__PURE__ */ new Map();
216
- const managementRootCache = /* @__PURE__ */ new Map();
117
+ const locallyDeletedBundleIds = /* @__PURE__ */ new Set();
217
118
  const loadOptionalObject = async (key) => {
218
119
  try {
219
120
  return await loadObject(key);
@@ -222,291 +123,29 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
222
123
  throw error;
223
124
  }
224
125
  };
225
- const getAllManagementArtifact = (artifacts) => {
226
- const allArtifact = artifacts.scopes.find((scope) => scope.cacheKey === ALL_SCOPE_CACHE_KEY);
227
- if (!allArtifact) throw new Error("all-bundles management index artifact not found");
228
- return allArtifact;
229
- };
230
- const replaceManagementRootCache = (artifacts) => {
231
- managementRootCache.clear();
232
- for (const scope of artifacts.scopes) managementRootCache.set(scope.cacheKey, scope.root);
233
- };
234
- const createHydratedBundle = (bundle) => {
235
- const hydratedBundle = createBundleWithUpdateJsonKey(bundle);
236
- bundlesMap.set(hydratedBundle.id, hydratedBundle);
237
- return hydratedBundle;
238
- };
239
- const loadStoredManagementRoot = async (scope) => {
240
- const cacheKey = getManagementScopeCacheKey(scope);
241
- const storedRoot = await loadOptionalObject(getManagementRootKey(scope));
242
- if (storedRoot) {
243
- managementRootCache.set(cacheKey, storedRoot);
244
- return storedRoot;
245
- }
246
- managementRootCache.delete(cacheKey);
247
- return null;
248
- };
249
- const loadManagementPage = async (descriptor, pageCache) => {
250
- if (pageCache?.has(descriptor.key)) return pageCache.get(descriptor.key) ?? null;
251
- const page = await loadOptionalObject(descriptor.key);
252
- pageCache?.set(descriptor.key, page);
253
- return page;
254
- };
255
- const loadBundleFromManagementRoot = async (root, bundleId) => {
256
- const pageIndex = findPageIndexContainingId(root.pages, bundleId);
257
- if (pageIndex < 0) return null;
258
- const descriptor = root.pages[pageIndex];
259
- const page = await loadManagementPage(descriptor);
260
- if (!page) return null;
261
- return page.find((item) => item.id === bundleId) ?? null;
262
- };
263
- const loadAllBundlesFromRoot = async (root) => {
264
- const allBundles = [];
265
- const pageCache = /* @__PURE__ */ new Map();
266
- for (const descriptor of root.pages) {
267
- const page = await loadManagementPage(descriptor, pageCache);
268
- if (!page) return null;
269
- allBundles.push(...page);
270
- }
271
- return allBundles;
272
- };
273
- const persistManagementIndexArtifacts = async (nextArtifacts, previousArtifacts) => {
274
- await forEachWithConcurrency(Array.from(nextArtifacts.pages.entries()), STORAGE_OPERATION_CONCURRENCY, ([key, page]) => uploadObject(key, page));
275
- await forEachWithConcurrency(nextArtifacts.scopes, STORAGE_OPERATION_CONCURRENCY, (scope) => uploadObject(scope.rootKey, scope.root));
276
- if (!previousArtifacts) return;
277
- const nextPageKeys = new Set(nextArtifacts.pages.keys());
278
- const nextRootKeys = new Set(nextArtifacts.scopes.map((scope) => scope.rootKey));
279
- await forEachWithConcurrency(Array.from(previousArtifacts.pages.keys()).filter((key) => !nextPageKeys.has(key)), STORAGE_OPERATION_CONCURRENCY, (key) => deleteObject(key).catch(() => {}));
280
- await forEachWithConcurrency(previousArtifacts.scopes.filter((scope) => !nextRootKeys.has(scope.rootKey)), STORAGE_OPERATION_CONCURRENCY, (scope) => deleteObject(scope.rootKey).catch(() => {}));
281
- };
282
- const ensureAllManagementRoot = async () => {
283
- const storedAllRoot = await loadStoredManagementRoot({});
284
- if (storedAllRoot && storedAllRoot.pageSize === managementIndexPageSize) return storedAllRoot;
285
- const rebuiltBundles = sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
286
- const nextArtifacts = buildManagementIndexArtifacts(rebuiltBundles, managementIndexPageSize);
287
- await persistManagementIndexArtifacts(nextArtifacts, storedAllRoot ? buildManagementIndexArtifacts(rebuiltBundles, storedAllRoot.pageSize) : void 0);
288
- replaceManagementRootCache(nextArtifacts);
289
- return getAllManagementArtifact(nextArtifacts).root;
290
- };
291
- const loadManagementScopeRoot = async (scope) => {
292
- const cacheKey = getManagementScopeCacheKey(scope);
293
- if (cacheKey === ALL_SCOPE_CACHE_KEY) return ensureAllManagementRoot();
294
- const storedRoot = await loadStoredManagementRoot(scope);
295
- if (storedRoot) return storedRoot;
296
- await ensureAllManagementRoot();
297
- const storedScopedRoot = await loadStoredManagementRoot(scope);
298
- if (storedScopedRoot) return storedScopedRoot;
299
- managementRootCache.set(cacheKey, null);
300
- return null;
301
- };
302
- const loadAllBundlesForManagementFallback = async () => {
303
- const allRoot = await loadManagementScopeRoot({});
304
- if (allRoot) {
305
- const pagedBundles = await loadAllBundlesFromRoot(allRoot);
306
- if (pagedBundles) return pagedBundles;
307
- }
308
- return sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
309
- };
310
- const loadCurrentBundlesForIndexRebuild = async () => {
311
- return loadAllBundlesForManagementFallback();
126
+ const loadAllBundlesForManagementFallback = async (where) => {
127
+ return sortManagedBundles((await reloadBundles(getManagementListPrefixes(where))).map((bundle) => removeBundleInternalKeys(bundle)));
312
128
  };
313
- const loadBundlesFromCanonicalManifests = async () => {
314
- return sortManagedBundles((await reloadBundles()).map((bundle) => removeBundleInternalKeys(bundle)));
315
- };
316
- const loadStoredBundlesForIndexRebuild = loadBundlesFromCanonicalManifests;
317
- const loadCanonicalBundlesForIndexRepair = loadBundlesFromCanonicalManifests;
318
- const compareBundleIndex = ({ canonicalBundles, indexedBundles, rootMissing }) => {
319
- const canonicalIds = new Set(canonicalBundles.map((bundle) => bundle.id));
320
- const indexedIds = new Set(indexedBundles?.map((bundle) => bundle.id) ?? []);
321
- const missingBundleIds = Array.from(canonicalIds).filter((id) => !indexedIds.has(id)).sort((left, right) => right.localeCompare(left));
322
- const extraBundleIds = Array.from(indexedIds).filter((id) => !canonicalIds.has(id)).sort((left, right) => right.localeCompare(left));
323
- return {
324
- status: missingBundleIds.length === 0 && extraBundleIds.length === 0 && !rootMissing ? "ok" : rootMissing ? "missing" : "stale",
325
- canonicalBundles: canonicalBundles.length,
326
- indexedBundles: indexedBundles?.length ?? 0,
327
- missingBundles: missingBundleIds.length,
328
- extraBundles: extraBundleIds.length,
329
- missingBundleIds: missingBundleIds.slice(0, 20),
330
- extraBundleIds: extraBundleIds.slice(0, 20)
331
- };
332
- };
333
- const findPageIndexContainingId = (pages, id) => {
334
- return pages.findIndex((page) => id.localeCompare(page.firstId) <= 0 && id.localeCompare(page.lastId) >= 0);
335
- };
336
- const readPagedBundles = async ({ root, limit, offset, cursor }) => {
337
- if (root.total === 0 || root.pages.length === 0) return createEmptyManagementResult(limit);
338
- const pageStartOffsets = getPageStartOffsets(root.pages);
339
- const pageCache = /* @__PURE__ */ new Map();
340
- if (offset !== void 0) {
341
- const normalizedOffset = Math.max(0, offset);
342
- if (normalizedOffset >= root.total) return {
343
- data: [],
344
- pagination: calculatePagination(root.total, {
345
- limit,
346
- offset: normalizedOffset
347
- })
348
- };
349
- let pageIndex = 0;
350
- for (let index = pageStartOffsets.length - 1; index >= 0; index--) if ((pageStartOffsets[index] ?? 0) <= normalizedOffset) {
351
- pageIndex = index;
352
- break;
353
- }
354
- const startInPage = normalizedOffset - (pageStartOffsets[pageIndex] ?? 0);
355
- const data = [];
356
- for (let currentPageIndex = pageIndex; currentPageIndex < root.pages.length && (limit <= 0 || data.length < limit); currentPageIndex++) {
357
- const descriptor = root.pages[currentPageIndex];
358
- const page = await loadManagementPage(descriptor, pageCache);
359
- if (!page) return paginateBundles({
360
- bundles: await loadAllBundlesForManagementFallback(),
361
- limit,
362
- offset: normalizedOffset
363
- });
364
- data.push(...currentPageIndex === pageIndex ? page.slice(startInPage) : page);
365
- }
366
- const paginatedData = limit > 0 ? data.slice(0, limit) : data;
367
- return {
368
- data: paginatedData,
369
- pagination: {
370
- ...calculatePagination(root.total, {
371
- limit,
372
- offset: normalizedOffset
373
- }),
374
- ...paginatedData.length > 0 && normalizedOffset + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
375
- ...paginatedData.length > 0 && normalizedOffset > 0 ? { previousCursor: paginatedData[0]?.id } : {}
376
- }
377
- };
378
- }
379
- if (cursor?.after) {
380
- let pageIndex = root.pages.findIndex((page) => {
381
- const containsCursor = cursor.after.localeCompare(page.firstId) <= 0 && cursor.after.localeCompare(page.lastId) >= 0;
382
- const wholePageEligible = cursor.after.localeCompare(page.firstId) > 0;
383
- return containsCursor || wholePageEligible;
384
- });
385
- if (pageIndex < 0) return {
386
- data: [],
387
- pagination: {
388
- ...calculatePagination(root.total, {
389
- limit,
390
- offset: root.total
391
- }),
392
- previousCursor: cursor.after
393
- }
394
- };
395
- const data = [];
396
- let startIndex = null;
397
- while (pageIndex < root.pages.length && (limit <= 0 || data.length < limit)) {
398
- const descriptor = root.pages[pageIndex];
399
- const page = await loadManagementPage(descriptor, pageCache);
400
- if (!page) return paginateBundles({
401
- bundles: await loadAllBundlesForManagementFallback(),
402
- limit,
403
- cursor
404
- });
405
- const containsCursor = cursor.after.localeCompare(descriptor.firstId) <= 0 && cursor.after.localeCompare(descriptor.lastId) >= 0;
406
- let eligiblePageBundles = page;
407
- if (containsCursor) {
408
- const startInPage = page.findIndex((bundle) => bundle.id.localeCompare(cursor.after) < 0);
409
- if (startInPage < 0) eligiblePageBundles = [];
410
- else {
411
- eligiblePageBundles = page.slice(startInPage);
412
- startIndex ??= (pageStartOffsets[pageIndex] ?? 0) + startInPage;
413
- }
414
- } else if (eligiblePageBundles.length > 0) startIndex ??= pageStartOffsets[pageIndex] ?? 0;
415
- data.push(...eligiblePageBundles);
416
- if (limit > 0 && data.length >= limit) break;
417
- pageIndex += 1;
418
- }
419
- const paginatedData = limit > 0 ? data.slice(0, limit) : data;
420
- const resolvedStartIndex = startIndex ?? root.total;
421
- return {
422
- data: paginatedData,
423
- pagination: {
424
- ...calculatePagination(root.total, {
425
- limit,
426
- offset: resolvedStartIndex
427
- }),
428
- ...paginatedData.length > 0 && resolvedStartIndex + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
429
- ...paginatedData.length > 0 && resolvedStartIndex > 0 ? { previousCursor: paginatedData[0]?.id } : {}
430
- }
431
- };
432
- }
433
- if (cursor?.before) {
434
- let pageIndex = -1;
435
- for (let index = root.pages.length - 1; index >= 0; index--) {
436
- const page = root.pages[index];
437
- const containsCursor = cursor.before.localeCompare(page.firstId) <= 0 && cursor.before.localeCompare(page.lastId) >= 0;
438
- const wholePageEligible = cursor.before.localeCompare(page.lastId) < 0;
439
- if (containsCursor || wholePageEligible) {
440
- pageIndex = index;
441
- break;
442
- }
443
- }
444
- if (pageIndex < 0) return createEmptyManagementResult(limit);
445
- let startIndex = null;
446
- let collected = [];
447
- while (pageIndex >= 0 && (limit <= 0 || collected.length < limit)) {
448
- const descriptor = root.pages[pageIndex];
449
- const page = await loadManagementPage(descriptor, pageCache);
450
- if (!page) return paginateBundles({
451
- bundles: await loadAllBundlesForManagementFallback(),
452
- limit,
453
- cursor
454
- });
455
- const eligiblePageBundles = cursor.before.localeCompare(descriptor.firstId) <= 0 && cursor.before.localeCompare(descriptor.lastId) >= 0 ? page.filter((bundle) => bundle.id.localeCompare(cursor.before) > 0) : page;
456
- collected = [...eligiblePageBundles, ...collected];
457
- if (eligiblePageBundles.length > 0) startIndex = pageStartOffsets[pageIndex] ?? 0;
458
- if (limit > 0 && collected.length >= limit) break;
459
- pageIndex -= 1;
460
- }
461
- if (startIndex === null || collected.length === 0) return createEmptyManagementResult(limit);
462
- let paginatedData = collected;
463
- if (limit > 0 && collected.length > limit) {
464
- const dropCount = collected.length - limit;
465
- paginatedData = collected.slice(dropCount);
466
- startIndex += dropCount;
467
- }
468
- const pagination = calculatePagination(root.total, {
469
- limit,
470
- offset: startIndex
471
- });
472
- return {
473
- data: paginatedData,
474
- pagination: {
475
- ...pagination,
476
- ...paginatedData.length > 0 && startIndex + paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {},
477
- ...paginatedData.length > 0 && startIndex > 0 ? { previousCursor: paginatedData[0]?.id } : {}
478
- }
479
- };
480
- }
481
- const pageIndex = 0;
482
- const startInPage = 0;
483
- const data = [];
484
- for (let currentPageIndex = pageIndex; currentPageIndex < root.pages.length && (limit <= 0 || data.length < limit); currentPageIndex++) {
485
- const descriptor = root.pages[currentPageIndex];
486
- const page = await loadManagementPage(descriptor, pageCache);
487
- if (!page) return paginateBundles({
488
- bundles: await loadAllBundlesForManagementFallback(),
489
- limit,
490
- 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
491
135
  });
492
- data.push(...currentPageIndex === pageIndex ? page.slice(startInPage) : page);
493
136
  }
494
- const paginatedData = limit > 0 ? data.slice(0, limit) : data;
495
- return {
496
- data: paginatedData,
497
- pagination: {
498
- ...calculatePagination(root.total, {
499
- limit,
500
- offset: 0
501
- }),
502
- ...paginatedData.length > 0 && paginatedData.length < root.total ? { nextCursor: paginatedData.at(-1)?.id } : {}
503
- }
504
- };
505
137
  };
506
- 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 = [""]) {
507
144
  bundlesMap.clear();
508
- const allBundles = (await mapWithConcurrency((await listObjects("")).filter((key) => /^[^/]+\/(?:ios|android)\/[^/]+\/update\.json$/.test(key)), STORAGE_OPERATION_CONCURRENCY, async (key) => {
509
- return (await loadOptionalObject(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) => ({
510
149
  ...bundle,
511
150
  _updateJsonKey: key
512
151
  }));
@@ -515,39 +154,18 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
515
154
  for (const [id, bundle] of pendingBundlesMap.entries()) bundlesMap.set(id, bundle);
516
155
  return orderBy(Array.from(bundlesMap.values()), [(v) => v.id], ["desc"]);
517
156
  }
518
- /**
519
- * Updates target-app-versions.json for each channel on the given platform.
520
- * Returns true if the file was updated, false if no changes were made.
521
- */
522
- async function updateTargetVersionsForPlatform(platform) {
523
- const updateJsonPattern = new RegExp(`^[^/]+/${platform}/[^/]+/update\\.json$`);
524
- const targetVersionsPattern = new RegExp(`^[^/]+/${platform}/target-app-versions\\.json$`);
525
- const allKeys = await listObjects("");
526
- const updateJsonKeys = allKeys.filter((key) => updateJsonPattern.test(key));
527
- const targetVersionsKeys = allKeys.filter((key) => targetVersionsPattern.test(key));
528
- const keysByChannel = updateJsonKeys.reduce((acc, key) => {
529
- const channel = key.split("/")[0];
530
- acc[channel] = acc[channel] || [];
531
- acc[channel].push(key);
532
- return acc;
533
- }, {});
534
- for (const key of targetVersionsKeys) {
535
- const channel = key.split("/")[0];
536
- if (!keysByChannel[channel]) keysByChannel[channel] = [];
537
- }
538
- for (const channel of Object.keys(keysByChannel)) {
539
- const updateKeys = keysByChannel[channel];
157
+ async function applyTargetVersionMutations(mutations) {
158
+ await Promise.all(Array.from(mutations.values()).map(async ({ additions, channel, platform, removals }) => {
540
159
  const targetKey = `${channel}/${platform}/target-app-versions.json`;
541
- const currentVersions = updateKeys.map((key) => key.split("/")[2]);
542
160
  const oldTargetVersions = await loadOptionalObject(targetKey) ?? [];
543
- const newTargetVersions = oldTargetVersions.filter((v) => currentVersions.includes(v));
544
- for (const v of currentVersions) if (!newTargetVersions.includes(v)) newTargetVersions.push(v);
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);
545
163
  if (JSON.stringify(oldTargetVersions) !== JSON.stringify(newTargetVersions)) await uploadObject(targetKey, newTargetVersions);
546
- }
164
+ }));
547
165
  }
548
166
  const getAppVersionUpdateInfo = async ({ appVersion, bundleId, channel = "production", cohort, minBundleId, platform }, context) => {
549
167
  const bundles = (await mapWithConcurrency(filterCompatibleAppVersions(await loadOptionalObject(`${channel}/${platform}/target-app-versions.json`) ?? [], appVersion), STORAGE_OPERATION_CONCURRENCY, async (targetAppVersion) => {
550
- return await loadOptionalObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`) ?? [];
168
+ return loadBundleObject(`${channel}/${platform}/${normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion}/update.json`);
551
169
  })).flat();
552
170
  return resolveUpdateInfoFromBundles({
553
171
  args: {
@@ -564,7 +182,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
564
182
  });
565
183
  };
566
184
  const getFingerprintUpdateInfo = async ({ bundleId, channel = "production", cohort, fingerprintHash, minBundleId, platform }, context) => {
567
- const bundles = await loadOptionalObject(`${channel}/${platform}/${fingerprintHash}/update.json`) ?? [];
185
+ const bundles = await loadBundleObject(`${channel}/${platform}/${fingerprintHash}/update.json`);
568
186
  return resolveUpdateInfoFromBundles({
569
187
  args: {
570
188
  _updateStrategy: "fingerprint",
@@ -598,54 +216,16 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
598
216
  targetAppVersion
599
217
  });
600
218
  };
601
- const bundleIndexDiagnostics = {
602
- async check() {
603
- const canonicalBundles = await loadCanonicalBundlesForIndexRepair();
604
- const allRoot = await loadStoredManagementRoot({});
605
- return compareBundleIndex({
606
- canonicalBundles,
607
- indexedBundles: allRoot ? await loadAllBundlesFromRoot(allRoot) : null,
608
- rootMissing: !allRoot
609
- });
610
- },
611
- async repair() {
612
- const canonicalBundles = await loadCanonicalBundlesForIndexRepair();
613
- const previousRoot = await loadStoredManagementRoot({});
614
- const previousBundles = previousRoot ? await loadAllBundlesFromRoot(previousRoot) : null;
615
- const previousArtifacts = previousRoot && previousBundles ? buildManagementIndexArtifacts(previousBundles, previousRoot.pageSize) : void 0;
616
- const nextArtifacts = buildManagementIndexArtifacts(canonicalBundles, managementIndexPageSize);
617
- const indexedObjectKeys = await listObjects(`${MANAGEMENT_INDEX_PREFIX}/`);
618
- const nextObjectKeys = new Set([...nextArtifacts.pages.keys(), ...nextArtifacts.scopes.map((scope) => scope.rootKey)]);
619
- await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
620
- await forEachWithConcurrency(indexedObjectKeys.filter((key) => !nextObjectKeys.has(key)), STORAGE_OPERATION_CONCURRENCY, (key) => deleteObject(key).catch(() => {}));
621
- replaceManagementRootCache(nextArtifacts);
622
- return {
623
- scannedBundles: canonicalBundles.length,
624
- indexedBundles: canonicalBundles.length,
625
- ...summarizeManagementIndexArtifacts(nextArtifacts)
626
- };
627
- }
628
- };
629
219
  const createPlugin = createDatabasePlugin({
630
220
  name,
631
221
  factory: () => ({
632
222
  supportsCursorPagination: true,
633
223
  async getBundleById(bundleId) {
224
+ if (locallyDeletedBundleIds.has(bundleId)) return null;
634
225
  const pendingBundle = pendingBundlesMap.get(bundleId);
635
226
  if (pendingBundle) return removeBundleInternalKeys(pendingBundle);
636
227
  const bundle = bundlesMap.get(bundleId);
637
228
  if (bundle) return removeBundleInternalKeys(bundle);
638
- const allRoot = await loadManagementScopeRoot({});
639
- if (allRoot) {
640
- const matchedBundle = await loadBundleFromManagementRoot(allRoot, bundleId);
641
- if (matchedBundle) return removeBundleInternalKeys(createHydratedBundle(matchedBundle));
642
- managementRootCache.delete(ALL_SCOPE_CACHE_KEY);
643
- const refreshedAllRoot = await loadStoredManagementRoot({});
644
- if (refreshedAllRoot) {
645
- const refreshedBundle = await loadBundleFromManagementRoot(refreshedAllRoot, bundleId);
646
- if (refreshedBundle) return removeBundleInternalKeys(createHydratedBundle(refreshedBundle));
647
- }
648
- }
649
229
  const matchedBundle = (await reloadBundles()).find((item) => item.id === bundleId);
650
230
  if (!matchedBundle) return null;
651
231
  return removeBundleInternalKeys(matchedBundle);
@@ -656,18 +236,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
656
236
  },
657
237
  async getBundles(options) {
658
238
  const { where, limit, offset, orderBy, cursor } = options;
659
- const scope = getSupportedManagementScope(where, orderBy);
660
- if (scope) {
661
- const root = await loadManagementScopeRoot(scope);
662
- if (!root) return createEmptyManagementResult(limit);
663
- return readPagedBundles({
664
- root,
665
- limit,
666
- offset,
667
- cursor
668
- });
669
- }
670
- let allBundles = await loadAllBundlesForManagementFallback();
239
+ let allBundles = await loadAllBundlesForManagementFallback(where);
671
240
  if (where) allBundles = allBundles.filter((bundle) => bundleMatchesQueryWhere(bundle, where));
672
241
  return paginateBundles({
673
242
  bundles: allBundles,
@@ -678,14 +247,15 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
678
247
  });
679
248
  },
680
249
  async getChannels() {
681
- return (await loadManagementScopeRoot({}))?.channels ?? [];
250
+ return [...new Set((await loadAllBundlesForManagementFallback()).map((bundle) => bundle.channel))].sort();
682
251
  },
683
252
  async commitBundle({ changedSets }) {
684
253
  if (changedSets.length === 0) return;
685
254
  const changedBundlesByKey = {};
686
255
  const removalsByKey = {};
256
+ const targetVersionRemovalsByKey = {};
687
257
  const pathsToInvalidate = /* @__PURE__ */ new Set();
688
- const targetVersionPlatforms = /* @__PURE__ */ new Set();
258
+ const targetVersionMutations = /* @__PURE__ */ new Map();
689
259
  for (const { operation, data } of changedSets) {
690
260
  if (operation === "insert") {
691
261
  const target = resolveStorageTarget(data);
@@ -694,11 +264,12 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
694
264
  ...data,
695
265
  _updateJsonKey: key
696
266
  };
267
+ locallyDeletedBundleIds.delete(data.id);
697
268
  bundlesMap.set(data.id, bundleWithKey);
698
269
  pendingBundlesMap.set(data.id, bundleWithKey);
699
270
  changedBundlesByKey[key] = changedBundlesByKey[key] || [];
700
271
  changedBundlesByKey[key].push(removeBundleInternalKeys(bundleWithKey));
701
- if (data.targetAppVersion !== void 0) targetVersionPlatforms.add(data.platform);
272
+ addTargetVersionAddition(targetVersionMutations, data);
702
273
  addLookupInvalidationPaths(pathsToInvalidate, data);
703
274
  continue;
704
275
  }
@@ -708,10 +279,12 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
708
279
  if (!bundle) throw new Error("Bundle to delete not found");
709
280
  bundlesMap.delete(data.id);
710
281
  pendingBundlesMap.delete(data.id);
282
+ locallyDeletedBundleIds.add(data.id);
711
283
  const key = bundle._updateJsonKey;
712
284
  removalsByKey[key] = removalsByKey[key] || [];
713
285
  removalsByKey[key].push(bundle.id);
714
- if (bundle.targetAppVersion !== void 0) targetVersionPlatforms.add(bundle.platform);
286
+ targetVersionRemovalsByKey[key] = targetVersionRemovalsByKey[key] || [];
287
+ targetVersionRemovalsByKey[key].push(bundle);
715
288
  addLookupInvalidationPaths(pathsToInvalidate, bundle);
716
289
  continue;
717
290
  }
@@ -728,11 +301,14 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
728
301
  const oldKey = bundle._updateJsonKey;
729
302
  removalsByKey[oldKey] = removalsByKey[oldKey] || [];
730
303
  removalsByKey[oldKey].push(bundle.id);
304
+ targetVersionRemovalsByKey[oldKey] = targetVersionRemovalsByKey[oldKey] || [];
305
+ targetVersionRemovalsByKey[oldKey].push(bundle);
731
306
  changedBundlesByKey[newKey] = changedBundlesByKey[newKey] || [];
732
307
  updatedBundle._oldUpdateJsonKey = oldKey;
733
308
  updatedBundle._updateJsonKey = newKey;
734
309
  bundlesMap.set(data.id, updatedBundle);
735
310
  pendingBundlesMap.set(data.id, updatedBundle);
311
+ locallyDeletedBundleIds.delete(data.id);
736
312
  changedBundlesByKey[newKey].push(removeBundleInternalKeys(updatedBundle));
737
313
  const oldChannel = bundle.channel;
738
314
  const nextChannel = updatedBundle.channel;
@@ -743,10 +319,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
743
319
  channel: nextChannel
744
320
  });
745
321
  }
746
- if (bundle.targetAppVersion !== void 0 || updatedBundle.targetAppVersion !== void 0) {
747
- targetVersionPlatforms.add(bundle.platform);
748
- targetVersionPlatforms.add(updatedBundle.platform);
749
- }
322
+ addTargetVersionAddition(targetVersionMutations, updatedBundle);
750
323
  addLookupInvalidationPaths(pathsToInvalidate, updatedBundle);
751
324
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
752
325
  continue;
@@ -754,17 +327,21 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
754
327
  const currentKey = bundle._updateJsonKey;
755
328
  bundlesMap.set(data.id, updatedBundle);
756
329
  pendingBundlesMap.set(data.id, updatedBundle);
330
+ locallyDeletedBundleIds.delete(data.id);
757
331
  changedBundlesByKey[currentKey] = changedBundlesByKey[currentKey] || [];
758
332
  changedBundlesByKey[currentKey].push(removeBundleInternalKeys(updatedBundle));
759
333
  addLookupInvalidationPaths(pathsToInvalidate, updatedBundle);
334
+ addTargetVersionAddition(targetVersionMutations, updatedBundle);
760
335
  if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) addLookupInvalidationPaths(pathsToInvalidate, bundle);
761
336
  }
762
337
  }
763
338
  await forEachWithConcurrency(Object.keys(removalsByKey), STORAGE_OPERATION_CONCURRENCY, async (oldKey) => {
764
339
  const updatedBundles = (await loadOptionalObject(oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
765
340
  updatedBundles.sort((a, b) => b.id.localeCompare(a.id));
766
- if (updatedBundles.length === 0) await deleteObject(oldKey);
767
- else await uploadObject(oldKey, updatedBundles);
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);
768
345
  });
769
346
  await forEachWithConcurrency(Object.keys(changedBundlesByKey), STORAGE_OPERATION_CONCURRENCY, async (key) => {
770
347
  const currentBundles = await loadOptionalObject(key) ?? [];
@@ -777,22 +354,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
777
354
  currentBundles.sort((a, b) => b.id.localeCompare(a.id));
778
355
  await uploadObject(key, currentBundles);
779
356
  });
780
- if (targetVersionPlatforms.size > 0) await Promise.all(Array.from(targetVersionPlatforms).map((platform) => updateTargetVersionsForPlatform(platform)));
781
- const previousIndexBundles = await loadCurrentBundlesForIndexRebuild();
782
- const storedIndexBundles = await loadStoredBundlesForIndexRebuild();
783
- const nextIndexMap = new Map(storedIndexBundles.map((bundle) => [bundle.id, bundle]));
784
- for (const { operation, data } of changedSets) {
785
- if (operation === "delete") {
786
- nextIndexMap.delete(data.id);
787
- continue;
788
- }
789
- nextIndexMap.set(data.id, data);
790
- }
791
- const nextIndexBundles = sortManagedBundles(Array.from(nextIndexMap.values()));
792
- const previousArtifacts = buildManagementIndexArtifacts(previousIndexBundles, managementIndexPageSize);
793
- const nextArtifacts = buildManagementIndexArtifacts(nextIndexBundles, managementIndexPageSize);
794
- await persistManagementIndexArtifacts(nextArtifacts, previousArtifacts);
795
- replaceManagementRootCache(nextArtifacts);
357
+ if (targetVersionMutations.size > 0) await applyTargetVersionMutations(targetVersionMutations);
796
358
  const encondedPaths = /* @__PURE__ */ new Set();
797
359
  for (const path of pathsToInvalidate) encondedPaths.add(encodeURI(path));
798
360
  await invalidatePaths(Array.from(encondedPaths));
@@ -801,13 +363,7 @@ const createBlobDatabasePlugin = ({ name, factory }) => {
801
363
  })
802
364
  })({}, hooks);
803
365
  return () => {
804
- const plugin = createPlugin();
805
- Object.defineProperty(plugin, "diagnostics", {
806
- configurable: true,
807
- enumerable: true,
808
- value: { bundleIndex: bundleIndexDiagnostics }
809
- });
810
- return plugin;
366
+ return createPlugin();
811
367
  };
812
368
  };
813
369
  };