@hot-updater/plugin-core 0.36.7 → 1.0.0-rc.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 (161) hide show
  1. package/dist/assetStorageLayout.cjs +36 -18
  2. package/dist/assetStorageLayout.d.cts +21 -6
  3. package/dist/assetStorageLayout.d.mts +21 -6
  4. package/dist/assetStorageLayout.mjs +36 -18
  5. package/dist/bundleStorageLayout.cjs +2 -3
  6. package/dist/bundleStorageLayout.mjs +2 -3
  7. package/dist/contentAddressedAssets.cjs +5 -0
  8. package/dist/contentAddressedAssets.d.cts +2 -1
  9. package/dist/contentAddressedAssets.d.mts +2 -1
  10. package/dist/contentAddressedAssets.mjs +5 -1
  11. package/dist/createDatabasePlugin.cjs +775 -284
  12. package/dist/createDatabasePlugin.d.cts +20 -64
  13. package/dist/createDatabasePlugin.d.mts +20 -64
  14. package/dist/createDatabasePlugin.mjs +773 -284
  15. package/dist/createStorageKeyBuilder.cjs +9 -2
  16. package/dist/createStorageKeyBuilder.mjs +9 -2
  17. package/dist/createStoragePlugin.cjs +6 -145
  18. package/dist/createStoragePlugin.d.cts +7 -52
  19. package/dist/createStoragePlugin.d.mts +7 -52
  20. package/dist/createStoragePlugin.mjs +5 -143
  21. package/dist/databaseClient.cjs +112 -0
  22. package/dist/databaseClient.d.cts +33 -0
  23. package/dist/databaseClient.d.mts +33 -0
  24. package/dist/databaseClient.mjs +110 -0
  25. package/dist/databaseClientReads.cjs +113 -0
  26. package/dist/databaseClientReads.mjs +112 -0
  27. package/dist/databaseClientUpdates.cjs +39 -0
  28. package/dist/databaseClientUpdates.d.cts +11 -0
  29. package/dist/databaseClientUpdates.d.mts +11 -0
  30. package/dist/databaseClientUpdates.mjs +36 -0
  31. package/dist/databaseJsonValue.cjs +37 -0
  32. package/dist/databaseJsonValue.d.cts +5 -0
  33. package/dist/databaseJsonValue.d.mts +5 -0
  34. package/dist/databaseJsonValue.mjs +37 -0
  35. package/dist/databaseMetadata.cjs +13 -0
  36. package/dist/databaseMetadata.mjs +12 -0
  37. package/dist/databasePluginCrud.cjs +86 -0
  38. package/dist/databasePluginCrud.d.cts +1 -0
  39. package/dist/databasePluginCrud.d.mts +1 -0
  40. package/dist/databasePluginCrud.mjs +86 -0
  41. package/dist/databasePluginCrudValidation.d.cts +1 -0
  42. package/dist/databasePluginCrudValidation.d.mts +1 -0
  43. package/dist/databasePluginCrudValidationErrors.cjs +10 -0
  44. package/dist/databasePluginCrudValidationErrors.d.cts +9 -0
  45. package/dist/databasePluginCrudValidationErrors.d.mts +9 -0
  46. package/dist/databasePluginCrudValidationErrors.mjs +10 -0
  47. package/dist/databasePluginCrudValidationFields.cjs +291 -0
  48. package/dist/databasePluginCrudValidationFields.d.cts +1 -0
  49. package/dist/databasePluginCrudValidationFields.d.mts +1 -0
  50. package/dist/databasePluginCrudValidationFields.mjs +281 -0
  51. package/dist/databasePluginCrudValidationMutations.cjs +88 -0
  52. package/dist/databasePluginCrudValidationMutations.d.cts +1 -0
  53. package/dist/databasePluginCrudValidationMutations.d.mts +1 -0
  54. package/dist/databasePluginCrudValidationMutations.mjs +82 -0
  55. package/dist/databasePluginCrudValidationQueries.cjs +103 -0
  56. package/dist/databasePluginCrudValidationQueries.d.cts +1 -0
  57. package/dist/databasePluginCrudValidationQueries.d.mts +1 -0
  58. package/dist/databasePluginCrudValidationQueries.mjs +97 -0
  59. package/dist/databasePluginCrudValidationRows.cjs +55 -0
  60. package/dist/databasePluginCrudValidationRows.d.cts +1 -0
  61. package/dist/databasePluginCrudValidationRows.d.mts +1 -0
  62. package/dist/databasePluginCrudValidationRows.mjs +53 -0
  63. package/dist/databasePluginTransaction.cjs +7 -0
  64. package/dist/databasePluginTransaction.mjs +7 -0
  65. package/dist/databaseRows.cjs +149 -0
  66. package/dist/databaseRows.d.cts +34 -0
  67. package/dist/databaseRows.d.mts +34 -0
  68. package/dist/databaseRows.mjs +141 -0
  69. package/dist/index.cjs +76 -24
  70. package/dist/index.d.cts +22 -12
  71. package/dist/index.d.mts +22 -12
  72. package/dist/index.mjs +21 -14
  73. package/dist/internal.cjs +6 -0
  74. package/dist/internal.d.cts +9 -0
  75. package/dist/internal.d.mts +9 -0
  76. package/dist/internal.mjs +3 -0
  77. package/dist/node_modules/.pnpm/verkit@0.3.2/node_modules/verkit/dist/index.cjs +345 -0
  78. package/dist/node_modules/.pnpm/verkit@0.3.2/node_modules/verkit/dist/index.mjs +341 -0
  79. package/dist/paginateBundles.d.cts +6 -4
  80. package/dist/paginateBundles.d.mts +6 -4
  81. package/dist/parseStorageUri.cjs +46 -30
  82. package/dist/parseStorageUri.d.cts +20 -17
  83. package/dist/parseStorageUri.d.mts +20 -17
  84. package/dist/parseStorageUri.mjs +46 -30
  85. package/dist/queryBundles.cjs +1 -7
  86. package/dist/queryBundles.d.cts +3 -1
  87. package/dist/queryBundles.d.mts +3 -1
  88. package/dist/queryBundles.mjs +1 -7
  89. package/dist/releaseCatalogCompiler.cjs +391 -0
  90. package/dist/releaseCatalogCompiler.d.cts +55 -0
  91. package/dist/releaseCatalogCompiler.d.mts +55 -0
  92. package/dist/releaseCatalogCompiler.mjs +386 -0
  93. package/dist/releaseCatalogMutation.cjs +303 -0
  94. package/dist/releaseCatalogMutation.d.cts +86 -0
  95. package/dist/releaseCatalogMutation.d.mts +86 -0
  96. package/dist/releaseCatalogMutation.mjs +297 -0
  97. package/dist/releaseManagement.cjs +231 -0
  98. package/dist/releaseManagement.d.cts +48 -0
  99. package/dist/releaseManagement.d.mts +48 -0
  100. package/dist/releaseManagement.mjs +224 -0
  101. package/dist/remoteBundleSigning.cjs +296 -0
  102. package/dist/remoteBundleSigning.d.cts +43 -0
  103. package/dist/remoteBundleSigning.d.mts +43 -0
  104. package/dist/remoteBundleSigning.mjs +291 -0
  105. package/dist/requestBundleCache.cjs +22 -0
  106. package/dist/requestBundleCache.d.cts +9 -0
  107. package/dist/requestBundleCache.d.mts +9 -0
  108. package/dist/requestBundleCache.mjs +22 -0
  109. package/dist/semverSatisfies.cjs +3 -4
  110. package/dist/semverSatisfies.mjs +1 -1
  111. package/dist/storageDownloadPath.cjs +48 -0
  112. package/dist/storageDownloadPath.d.cts +15 -0
  113. package/dist/storageDownloadPath.d.mts +15 -0
  114. package/dist/storageDownloadPath.mjs +46 -0
  115. package/dist/types/database.d.cts +4 -0
  116. package/dist/types/database.d.mts +4 -0
  117. package/dist/types/databaseFields.cjs +92 -0
  118. package/dist/types/databaseFields.d.cts +12 -0
  119. package/dist/types/databaseFields.d.mts +12 -0
  120. package/dist/types/databaseFields.mjs +92 -0
  121. package/dist/types/databaseOperations.d.cts +173 -0
  122. package/dist/types/databaseOperations.d.mts +173 -0
  123. package/dist/types/databasePlugin.d.cts +240 -0
  124. package/dist/types/databasePlugin.d.mts +240 -0
  125. package/dist/types/databaseQuery.d.cts +47 -0
  126. package/dist/types/databaseQuery.d.mts +47 -0
  127. package/dist/types/databaseRows.d.cts +120 -0
  128. package/dist/types/databaseRows.d.mts +120 -0
  129. package/dist/types/index.d.cts +120 -162
  130. package/dist/types/index.d.mts +120 -162
  131. package/dist/types/internal.d.cts +7 -0
  132. package/dist/types/internal.d.mts +7 -0
  133. package/dist/types/public.d.cts +3 -0
  134. package/dist/types/public.d.mts +3 -0
  135. package/dist/uuidv7.cjs +9 -0
  136. package/dist/uuidv7.d.cts +4 -1
  137. package/dist/uuidv7.d.mts +4 -1
  138. package/dist/uuidv7.mjs +8 -1
  139. package/package.json +10 -5
  140. package/dist/bundleUnitOfWork.cjs +0 -158
  141. package/dist/bundleUnitOfWork.mjs +0 -158
  142. package/dist/bundleUnitOfWorkStore.cjs +0 -15
  143. package/dist/bundleUnitOfWorkStore.mjs +0 -15
  144. package/dist/createBlobDatabasePlugin.cjs +0 -382
  145. package/dist/createBlobDatabasePlugin.d.cts +0 -29
  146. package/dist/createBlobDatabasePlugin.d.mts +0 -29
  147. package/dist/createBlobDatabasePlugin.mjs +0 -381
  148. package/dist/legacyAssetStorageLayout.cjs +0 -12
  149. package/dist/legacyAssetStorageLayout.mjs +0 -12
  150. package/dist/requestUpdateBundleState.cjs +0 -26
  151. package/dist/requestUpdateBundleState.d.cts +0 -12
  152. package/dist/requestUpdateBundleState.d.mts +0 -12
  153. package/dist/requestUpdateBundleState.mjs +0 -24
  154. package/dist/resolveUpdateInfoFromBundles.cjs +0 -14
  155. package/dist/resolveUpdateInfoFromBundles.d.cts +0 -16
  156. package/dist/resolveUpdateInfoFromBundles.d.mts +0 -16
  157. package/dist/resolveUpdateInfoFromBundles.mjs +0 -13
  158. package/dist/storageProfile.cjs +0 -15
  159. package/dist/storageProfile.d.cts +0 -9
  160. package/dist/storageProfile.d.mts +0 -9
  161. package/dist/storageProfile.mjs +0 -12
@@ -0,0 +1,386 @@
1
+ import { isUUIDv7 as isUUIDv7$1 } from "./uuidv7.mjs";
2
+ import { coerce, compare, normalize, parseRange, satisfies } from "./node_modules/.pnpm/verkit@0.3.2/node_modules/verkit/dist/index.mjs";
3
+ import { MAX_COMPILED_CATALOG_BYTES, MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE, MAX_TARGET_COHORTS_PER_RELEASE, NUMERIC_COHORT_SIZE, RELEASE_CATALOG_FALLBACK_POLICY, RELEASE_CATALOG_SCHEMA_VERSION, getNumericCohortValue, getRolledOutNumericCohorts, isCustomCohort, normalizeCohortValue, normalizeRolloutCohortCount } from "@hot-updater/core";
4
+ //#region src/releaseCatalogCompiler.ts
5
+ var ReleaseCatalogCompilationError = class extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.name = "ReleaseCatalogCompilationError";
10
+ this.code = code;
11
+ }
12
+ };
13
+ function compareBounds(left, right) {
14
+ return compare(left, right);
15
+ }
16
+ function strongerLower(current, candidate) {
17
+ if (current === null) return candidate;
18
+ const compared = compareBounds(candidate.version, current.version);
19
+ if (compared > 0) return candidate;
20
+ if (compared < 0) return current;
21
+ return {
22
+ version: current.version,
23
+ inclusive: current.inclusive && candidate.inclusive
24
+ };
25
+ }
26
+ function strongerUpper(current, candidate) {
27
+ if (current === null) return candidate;
28
+ const compared = compareBounds(candidate.version, current.version);
29
+ if (compared < 0) return candidate;
30
+ if (compared > 0) return current;
31
+ return {
32
+ version: current.version,
33
+ inclusive: current.inclusive && candidate.inclusive
34
+ };
35
+ }
36
+ function comparatorVersion(comparator) {
37
+ const version = comparator.version && normalize(comparator.version);
38
+ if (version === null) throw new ReleaseCatalogCompilationError("INVALID_TARGET_RANGE", `Invalid comparator: ${comparator.value}`);
39
+ return version;
40
+ }
41
+ function comparatorSetToInterval(comparators) {
42
+ let lower = null;
43
+ let upper = null;
44
+ for (const comparator of comparators) {
45
+ if (comparator.version === null) continue;
46
+ const version = comparatorVersion(comparator);
47
+ if (comparator.operator === "") {
48
+ lower = strongerLower(lower, {
49
+ inclusive: true,
50
+ version
51
+ });
52
+ upper = strongerUpper(upper, {
53
+ inclusive: true,
54
+ version
55
+ });
56
+ } else if (comparator.operator === ">") lower = strongerLower(lower, {
57
+ inclusive: false,
58
+ version
59
+ });
60
+ else if (comparator.operator === ">=") lower = strongerLower(lower, {
61
+ inclusive: true,
62
+ version
63
+ });
64
+ else if (comparator.operator === "<") upper = strongerUpper(upper, {
65
+ inclusive: false,
66
+ version
67
+ });
68
+ else if (comparator.operator === "<=") upper = strongerUpper(upper, {
69
+ inclusive: true,
70
+ version
71
+ });
72
+ }
73
+ if (lower !== null && upper !== null) {
74
+ const compared = compareBounds(lower.version, upper.version);
75
+ if (compared > 0 || compared === 0 && (!lower.inclusive || !upper.inclusive)) return null;
76
+ }
77
+ return {
78
+ lower,
79
+ upper
80
+ };
81
+ }
82
+ function rangeToIntervals(range) {
83
+ try {
84
+ return parseRange(range).sets.flatMap((comparators) => {
85
+ const interval = comparatorSetToInterval(comparators);
86
+ return interval === null ? [] : [interval];
87
+ });
88
+ } catch {
89
+ throw new ReleaseCatalogCompilationError("INVALID_TARGET_RANGE", `Invalid target app-version range: ${range}`);
90
+ }
91
+ }
92
+ function releaseDescriptor(release) {
93
+ return {
94
+ releaseId: release.id,
95
+ kind: release.kind,
96
+ bundleId: release.bundleId,
97
+ rolloutCohortCount: release.rolloutCohortCount,
98
+ targetCohorts: [...new Set(release.targetCohorts)].sort(),
99
+ shouldForceUpdate: release.shouldForceUpdate,
100
+ message: release.message
101
+ };
102
+ }
103
+ function validateReleases(releases, strategy) {
104
+ const enabled = releases.filter((release) => release.enabled).sort((left, right) => right.id.localeCompare(left.id));
105
+ const seenIds = /* @__PURE__ */ new Set();
106
+ for (const release of enabled) {
107
+ if (!isUUIDv7$1(release.id)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} must use a canonical lowercase UUIDv7 ID`);
108
+ if (seenIds.has(release.id)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Duplicate Release ID: ${release.id}`);
109
+ seenIds.add(release.id);
110
+ if (release.strategy !== strategy) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has the wrong strategy`);
111
+ if (release.kind === "BUNDLE" && release.bundleId === null || release.kind === "EMBEDDED" && release.bundleId !== null) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has an invalid kind/Bundle combination`);
112
+ if (release.targetCohorts.length > MAX_TARGET_COHORTS_PER_RELEASE) throw new ReleaseCatalogCompilationError("TARGET_COHORT_LIMIT", `Release ${release.id} exceeds ${MAX_TARGET_COHORTS_PER_RELEASE} target cohorts`);
113
+ if (strategy === "APP_VERSION" && (release.targetAppVersion === null || release.fingerprintHash !== null)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has an invalid app-version target`);
114
+ if (strategy === "FINGERPRINT" && (release.fingerprintHash === null || release.targetAppVersion !== null)) throw new ReleaseCatalogCompilationError("INVALID_RELEASE", `Release ${release.id} has an invalid fingerprint target`);
115
+ }
116
+ return enabled.filter((release) => release.kind === "BUNDLE");
117
+ }
118
+ function collectFrontierReleaseIds(releases) {
119
+ if (releases.every((release) => normalizeRolloutCohortCount(release.rolloutCohortCount) >= NUMERIC_COHORT_SIZE)) {
120
+ const retained = /* @__PURE__ */ new Set();
121
+ const bundleIds = /* @__PURE__ */ new Set();
122
+ let embedded = false;
123
+ for (const release of releases) {
124
+ if (release.kind === "EMBEDDED") {
125
+ if (!embedded) {
126
+ embedded = true;
127
+ retained.add(release.id);
128
+ }
129
+ } else if (release.bundleId !== null && bundleIds.size < 11 && !bundleIds.has(release.bundleId)) {
130
+ bundleIds.add(release.bundleId);
131
+ retained.add(release.id);
132
+ }
133
+ if (embedded && bundleIds.size >= 11) break;
134
+ }
135
+ return retained;
136
+ }
137
+ const explicitTargetCohorts = new Set(releases.flatMap((release) => release.targetCohorts));
138
+ const cohortStates = [
139
+ void 0,
140
+ ...Array.from({ length: 1e3 }, (_, index) => String(index + 1)),
141
+ ...[...explicitTargetCohorts].sort(),
142
+ "release-catalog-non-targeted-cohort"
143
+ ].map((cohort) => {
144
+ const normalized = cohort === void 0 ? void 0 : normalizeCohortValue(cohort);
145
+ return {
146
+ isCustom: normalized !== void 0 && isCustomCohort(normalized),
147
+ normalized,
148
+ numeric: normalized === void 0 ? null : getNumericCohortValue(normalized),
149
+ bundleIds: /* @__PURE__ */ new Set(),
150
+ embedded: false,
151
+ complete: false
152
+ };
153
+ });
154
+ const stateIndexesByCohort = /* @__PURE__ */ new Map();
155
+ const fullRolloutStateIndexes = [];
156
+ for (const [index, state] of cohortStates.entries()) {
157
+ if (state.normalized !== void 0) {
158
+ const indexes = stateIndexesByCohort.get(state.normalized) ?? [];
159
+ indexes.push(index);
160
+ stateIndexesByCohort.set(state.normalized, indexes);
161
+ }
162
+ if (state.normalized === void 0 || state.numeric !== null || state.isCustom) fullRolloutStateIndexes.push(index);
163
+ }
164
+ const retained = /* @__PURE__ */ new Set();
165
+ let incompleteStateCount = cohortStates.length;
166
+ for (const release of releases) {
167
+ const rolloutCohortCount = normalizeRolloutCohortCount(release.rolloutCohortCount);
168
+ const eligibleStateIndexes = /* @__PURE__ */ new Set();
169
+ if (rolloutCohortCount >= NUMERIC_COHORT_SIZE) for (const index of fullRolloutStateIndexes) eligibleStateIndexes.add(index);
170
+ else if (rolloutCohortCount > 0) for (const numericCohort of getRolledOutNumericCohorts(release.id, rolloutCohortCount)) for (const index of stateIndexesByCohort.get(String(numericCohort)) ?? []) eligibleStateIndexes.add(index);
171
+ for (const targetCohort of new Set(release.targetCohorts.map(normalizeCohortValue))) for (const index of stateIndexesByCohort.get(targetCohort) ?? []) eligibleStateIndexes.add(index);
172
+ for (const index of eligibleStateIndexes) {
173
+ const state = cohortStates[index];
174
+ if (state.complete) continue;
175
+ if (release.kind === "EMBEDDED") {
176
+ if (!state.embedded) {
177
+ state.embedded = true;
178
+ retained.add(release.id);
179
+ }
180
+ } else if (release.bundleId !== null && state.bundleIds.size < 11 && !state.bundleIds.has(release.bundleId)) {
181
+ state.bundleIds.add(release.bundleId);
182
+ retained.add(release.id);
183
+ }
184
+ if (state.embedded && state.bundleIds.size >= 11) {
185
+ state.complete = true;
186
+ incompleteStateCount -= 1;
187
+ }
188
+ }
189
+ if (incompleteStateCount === 0) break;
190
+ }
191
+ return retained;
192
+ }
193
+ function intervalContainsSegment(interval, segment) {
194
+ if (interval.lower !== null) {
195
+ if (segment.lower === null) return false;
196
+ const compared = compareBounds(segment.lower.version, interval.lower.version);
197
+ if (compared < 0 || compared === 0 && segment.lower.inclusive && !interval.lower.inclusive) return false;
198
+ }
199
+ if (interval.upper !== null) {
200
+ if (segment.upper === null) return false;
201
+ const compared = compareBounds(segment.upper.version, interval.upper.version);
202
+ if (compared > 0 || compared === 0 && segment.upper.inclusive && !interval.upper.inclusive) return false;
203
+ }
204
+ return true;
205
+ }
206
+ function atomicVersionSegments(intervals) {
207
+ const endpoints = [...new Set(intervals.flatMap((interval) => [interval.lower?.version, interval.upper?.version].filter((version) => version !== void 0)))].sort(compareBounds);
208
+ if (endpoints.length === 0) return [{
209
+ lower: null,
210
+ upper: null
211
+ }];
212
+ const segments = [{
213
+ lower: null,
214
+ upper: {
215
+ inclusive: false,
216
+ version: endpoints[0]
217
+ }
218
+ }];
219
+ for (let index = 0; index < endpoints.length; index += 1) {
220
+ const endpoint = endpoints[index];
221
+ segments.push({
222
+ lower: {
223
+ inclusive: true,
224
+ version: endpoint
225
+ },
226
+ upper: {
227
+ inclusive: true,
228
+ version: endpoint
229
+ }
230
+ });
231
+ const next = endpoints[index + 1];
232
+ if (next !== void 0) segments.push({
233
+ lower: {
234
+ inclusive: false,
235
+ version: endpoint
236
+ },
237
+ upper: {
238
+ inclusive: false,
239
+ version: next
240
+ }
241
+ });
242
+ }
243
+ segments.push({
244
+ lower: {
245
+ inclusive: false,
246
+ version: endpoints.at(-1)
247
+ },
248
+ upper: null
249
+ });
250
+ return segments;
251
+ }
252
+ function sameIndexes(left, right) {
253
+ return left.length === right.length && left.every((value, index) => value === right[index]);
254
+ }
255
+ function mergeSegments(segments) {
256
+ const merged = [];
257
+ for (const segment of segments) {
258
+ const previous = merged.at(-1);
259
+ if (previous && sameIndexes(previous.releaseIndexes, segment.releaseIndexes) && sameIndexes(previous.rollbackReleaseIndexes, segment.rollbackReleaseIndexes)) merged[merged.length - 1] = {
260
+ ...previous,
261
+ upper: segment.upper
262
+ };
263
+ else merged.push(segment);
264
+ }
265
+ return merged;
266
+ }
267
+ function compileAppVersion(releases) {
268
+ const ranges = /* @__PURE__ */ new Map();
269
+ for (const release of releases) ranges.set(release.id, rangeToIntervals(release.targetAppVersion));
270
+ const segmentReleases = atomicVersionSegments([...ranges.values()].flat()).map((segment) => {
271
+ const applicable = releases.filter((release) => ranges.get(release.id).some((range) => intervalContainsSegment(range, segment)));
272
+ return {
273
+ rollbackIds: new Set(applicable.map(({ id }) => id)),
274
+ segment,
275
+ retainedIds: collectFrontierReleaseIds(applicable)
276
+ };
277
+ });
278
+ const retainedIds = new Set(segmentReleases.flatMap(({ retainedIds, rollbackIds }) => [...retainedIds, ...rollbackIds]));
279
+ const retainedReleases = releases.filter((release) => retainedIds.has(release.id));
280
+ const descriptorIndex = new Map(retainedReleases.map((release, index) => [release.id, index]));
281
+ return {
282
+ envelope: {
283
+ fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
284
+ schemaVersion: RELEASE_CATALOG_SCHEMA_VERSION,
285
+ segments: mergeSegments(segmentReleases.map(({ segment, retainedIds, rollbackIds }) => ({
286
+ ...segment,
287
+ releaseIndexes: releases.filter((release) => retainedIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
288
+ rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
289
+ })).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0)),
290
+ strategy: "APP_VERSION"
291
+ },
292
+ retainedReleases
293
+ };
294
+ }
295
+ function compileFingerprint(releases) {
296
+ const retainedIds = collectFrontierReleaseIds(releases);
297
+ const retainedReleases = releases;
298
+ return {
299
+ envelope: {
300
+ fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
301
+ releaseIndexes: retainedReleases.map((release, index) => retainedIds.has(release.id) ? index : -1).filter((index) => index >= 0),
302
+ rollbackReleaseIndexes: retainedReleases.map((_, index) => index),
303
+ schemaVersion: RELEASE_CATALOG_SCHEMA_VERSION,
304
+ strategy: "FINGERPRINT"
305
+ },
306
+ retainedReleases
307
+ };
308
+ }
309
+ async function sha256(value) {
310
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
311
+ return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
312
+ }
313
+ function canonicalStringify(value) {
314
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
315
+ if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(",")}]`;
316
+ const record = value;
317
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`).join(",")}}`;
318
+ }
319
+ async function compileReleaseCatalog(input) {
320
+ const releases = validateReleases(input.releases, input.strategy);
321
+ const targetCohorts = new Set(releases.flatMap((release) => release.targetCohorts));
322
+ if (targetCohorts.size > MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE) throw new ReleaseCatalogCompilationError("DISTINCT_TARGET_COHORT_LIMIT", `Scope exceeds ${MAX_DISTINCT_TARGET_COHORTS_PER_SCOPE} distinct target cohorts`);
323
+ const { envelope, retainedReleases } = input.strategy === "APP_VERSION" ? compileAppVersion(releases) : compileFingerprint(releases);
324
+ const payload = {
325
+ ...envelope,
326
+ releaseDescriptors: retainedReleases.map(releaseDescriptor)
327
+ };
328
+ const canonicalPayload = canonicalStringify(payload);
329
+ const byteSize = new TextEncoder().encode(canonicalPayload).byteLength;
330
+ if (byteSize > MAX_COMPILED_CATALOG_BYTES) throw new ReleaseCatalogCompilationError("CATALOG_OVERSIZE", `Compiled Release catalog is ${byteSize} bytes; maximum is ${MAX_COMPILED_CATALOG_BYTES}`);
331
+ return {
332
+ byteSize,
333
+ canonicalPayload,
334
+ catalogHash: await sha256(canonicalPayload),
335
+ diagnostics: {
336
+ byteSize,
337
+ descriptorCount: payload.releaseDescriptors.length,
338
+ distinctTargetCohortCount: targetCohorts.size,
339
+ releaseCount: releases.length,
340
+ segmentCount: payload.strategy === "APP_VERSION" ? payload.segments.length : 1
341
+ },
342
+ payload
343
+ };
344
+ }
345
+ function versionInSegment(version, segment) {
346
+ if (segment.lower !== null) {
347
+ const compared = compareBounds(version, segment.lower.version);
348
+ if (compared < 0 || compared === 0 && !segment.lower.inclusive) return false;
349
+ }
350
+ if (segment.upper !== null) {
351
+ const compared = compareBounds(version, segment.upper.version);
352
+ if (compared > 0 || compared === 0 && !segment.upper.inclusive) return false;
353
+ }
354
+ return true;
355
+ }
356
+ function canonicalizeAppVersion(appVersion) {
357
+ return coerce(appVersion);
358
+ }
359
+ function projectCompiledCatalog(catalog, appVersion) {
360
+ let indexes;
361
+ if (catalog.strategy === "FINGERPRINT") indexes = catalog.releaseIndexes;
362
+ else {
363
+ const canonicalVersion = appVersion && canonicalizeAppVersion(appVersion);
364
+ if (!canonicalVersion) return [];
365
+ indexes = catalog.segments.find((segment) => versionInSegment(canonicalVersion, segment))?.releaseIndexes ?? [];
366
+ }
367
+ return indexes.map((index) => catalog.releaseDescriptors[index]);
368
+ }
369
+ function projectCompiledRollbackCatalog(catalog, appVersion) {
370
+ let indexes;
371
+ if (catalog.strategy === "FINGERPRINT") indexes = catalog.rollbackReleaseIndexes ?? catalog.releaseIndexes;
372
+ else {
373
+ const canonicalVersion = appVersion && canonicalizeAppVersion(appVersion);
374
+ if (!canonicalVersion) return [];
375
+ const segment = catalog.segments.find((candidate) => versionInSegment(canonicalVersion, candidate));
376
+ indexes = segment?.rollbackReleaseIndexes ?? segment?.releaseIndexes ?? [];
377
+ }
378
+ return indexes.map((index) => catalog.releaseDescriptors[index]);
379
+ }
380
+ function referenceCompatibleReleases(releases, appVersion) {
381
+ const canonicalVersion = canonicalizeAppVersion(appVersion);
382
+ if (!canonicalVersion) return [];
383
+ return releases.filter((release) => release.enabled && release.targetAppVersion !== null && satisfies(canonicalVersion, release.targetAppVersion));
384
+ }
385
+ //#endregion
386
+ export { ReleaseCatalogCompilationError, canonicalizeAppVersion, compileReleaseCatalog, projectCompiledCatalog, projectCompiledRollbackCatalog, referenceCompatibleReleases };
@@ -0,0 +1,303 @@
1
+ require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_uuidv7 = require("./uuidv7.cjs");
3
+ const require_databaseRows = require("./databaseRows.cjs");
4
+ const require_releaseCatalogCompiler = require("./releaseCatalogCompiler.cjs");
5
+ let _hot_updater_core = require("@hot-updater/core");
6
+ //#region src/releaseCatalogMutation.ts
7
+ const RELEASE_PAGE_SIZE = 1e3;
8
+ const DEFAULT_MAX_ATTEMPTS = 3;
9
+ var ReleaseCatalogMutationError = class extends Error {
10
+ name = "ReleaseCatalogMutationError";
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ }
15
+ };
16
+ const readScopeReleases = async (database, scopeKey) => {
17
+ const releases = [];
18
+ let afterReleaseId;
19
+ for (;;) {
20
+ const page = await database.models.releases.findManyByScope({
21
+ scopeKey,
22
+ ...afterReleaseId === void 0 ? {} : { afterReleaseId },
23
+ limit: RELEASE_PAGE_SIZE,
24
+ consistency: "strong"
25
+ });
26
+ for (const row of page) {
27
+ if (row.scope_key !== scopeKey) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${row.id}" was returned for the wrong catalog scope`);
28
+ if (releases.at(-1)?.id === row.id) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${row.id}" was returned more than once`);
29
+ releases.push(row);
30
+ }
31
+ if (page.length < RELEASE_PAGE_SIZE) return releases;
32
+ const next = page.at(-1)?.id;
33
+ if (next === void 0 || next === afterReleaseId) throw new ReleaseCatalogMutationError("INVALID_SCOPE", "Release scope paging did not advance");
34
+ afterReleaseId = next;
35
+ }
36
+ };
37
+ const validateScope = (scope) => {
38
+ const channelKey = (0, _hot_updater_core.encodeChannelKey)(scope.channelName);
39
+ if (scope.strategy === "FINGERPRINT" !== (scope.fingerprintHash !== null)) throw new ReleaseCatalogMutationError("INVALID_SCOPE", "Release catalog strategy and fingerprint are inconsistent");
40
+ const expectedScopeKey = scope.strategy === "APP_VERSION" ? (0, _hot_updater_core.createReleaseCatalogScopeKey)({
41
+ channelKey,
42
+ platform: scope.platform,
43
+ strategy: "APP_VERSION"
44
+ }) : (0, _hot_updater_core.createReleaseCatalogScopeKey)({
45
+ channelKey,
46
+ fingerprintHash: scope.fingerprintHash ?? "",
47
+ platform: scope.platform,
48
+ strategy: "FINGERPRINT"
49
+ });
50
+ if (scope.scopeKey !== expectedScopeKey) throw new ReleaseCatalogMutationError("INVALID_SCOPE", "Release catalog scope metadata is inconsistent");
51
+ };
52
+ const applyMutation = (rows, scope, mutation) => {
53
+ const currentIndex = mutation.operation === "insert" ? -1 : rows.findIndex(({ id }) => id === mutation.id);
54
+ const current = currentIndex < 0 ? void 0 : rows[currentIndex];
55
+ if (mutation.operation === "insert") {
56
+ const latestId = rows.at(-1)?.id;
57
+ if (mutation.row.scope_key !== scope.scopeKey || mutation.row.channel_id !== scope.channelId || mutation.row.platform !== scope.platform || mutation.row.strategy !== scope.strategy || mutation.row.fingerprint_hash !== scope.fingerprintHash || mutation.row.revision !== 1) throw new ReleaseCatalogMutationError("INVALID_SCOPE", "Inserted Release does not match its catalog scope");
58
+ if (latestId !== void 0 && mutation.row.id <= latestId) throw new ReleaseCatalogMutationError("NON_MONOTONIC_RELEASE_ID", `Release "${mutation.row.id}" must sort after "${latestId}"`);
59
+ return {
60
+ change: {
61
+ model: "releases",
62
+ operation: "insert",
63
+ row: mutation.row
64
+ },
65
+ expectedRevision: null,
66
+ nextRows: [...rows, mutation.row],
67
+ release: mutation.row
68
+ };
69
+ }
70
+ if (current === void 0) throw new ReleaseCatalogMutationError("RELEASE_NOT_FOUND", `Release "${mutation.id}" was not found in scope "${scope.scopeKey}"`);
71
+ if (mutation.operation === "delete") return {
72
+ change: {
73
+ model: "releases",
74
+ operation: "delete",
75
+ where: { id: mutation.id }
76
+ },
77
+ expectedRevision: current.revision,
78
+ nextRows: rows.filter(({ id }) => id !== mutation.id),
79
+ release: null
80
+ };
81
+ const release = {
82
+ ...current,
83
+ ...mutation.update,
84
+ revision: current.revision + 1
85
+ };
86
+ return {
87
+ change: {
88
+ model: "releases",
89
+ operation: "update",
90
+ where: { id: mutation.id },
91
+ update: {
92
+ ...mutation.update,
93
+ revision: release.revision
94
+ }
95
+ },
96
+ expectedRevision: current.revision,
97
+ nextRows: rows.map((row) => row.id === release.id ? release : row),
98
+ release
99
+ };
100
+ };
101
+ const catalogIdentity = (current, rows) => {
102
+ if (current !== null) return current.catalog_id;
103
+ if (rows.length > 0) throw new ReleaseCatalogMutationError("CATALOG_IDENTITY_MISSING", "Catalog history is missing. Restore its catalog row from backup before rebuilding or deploying.");
104
+ return require_uuidv7.createUUIDv7();
105
+ };
106
+ const readCatalogScope = async (database, scopeKey) => {
107
+ let currentCatalog = await database.models.releaseCatalogs.findByScopeKey(scopeKey);
108
+ let rows = await readScopeReleases(database, scopeKey);
109
+ if (currentCatalog === null && rows.length > 0) {
110
+ currentCatalog = await database.models.releaseCatalogs.findByScopeKey(scopeKey);
111
+ if (currentCatalog !== null) rows = await readScopeReleases(database, scopeKey);
112
+ }
113
+ return {
114
+ currentCatalog,
115
+ rows
116
+ };
117
+ };
118
+ const prepareReleaseCatalogMutation = async (database, mutationInput) => {
119
+ const { rows, currentCatalog } = await readCatalogScope(database, mutationInput.scope.scopeKey);
120
+ const applied = applyMutation(rows, mutationInput.scope, mutationInput.mutation);
121
+ const compilation = await require_releaseCatalogCompiler.compileReleaseCatalog({
122
+ strategy: mutationInput.scope.strategy,
123
+ releases: applied.nextRows.map(require_databaseRows.releaseRowToRelease)
124
+ });
125
+ const generation = (currentCatalog?.generation ?? 0) + 1;
126
+ if (!Number.isSafeInteger(generation)) throw new ReleaseCatalogMutationError("CATALOG_GENERATION_EXHAUSTED", `Catalog "${mutationInput.scope.scopeKey}" exhausted its generation counter`);
127
+ return {
128
+ applied,
129
+ catalog: {
130
+ scope_key: mutationInput.scope.scopeKey,
131
+ catalog_id: catalogIdentity(currentCatalog, rows),
132
+ strategy: mutationInput.scope.strategy,
133
+ channel_id: mutationInput.scope.channelId,
134
+ channel_key: (0, _hot_updater_core.encodeChannelKey)(mutationInput.scope.channelName),
135
+ platform: mutationInput.scope.platform,
136
+ fingerprint_hash: mutationInput.scope.fingerprintHash,
137
+ generation,
138
+ payload: compilation.canonicalPayload,
139
+ catalog_hash: compilation.catalogHash,
140
+ byte_size: compilation.byteSize,
141
+ is_tombstone: compilation.payload.releaseDescriptors.length === 0,
142
+ updated_at_ms: mutationInput.updatedAtMs ?? Date.now()
143
+ },
144
+ compilation,
145
+ currentCatalog,
146
+ mutationInput
147
+ };
148
+ };
149
+ async function preflightReleaseCatalogMutation(input) {
150
+ validateScope(input.scope);
151
+ const prepared = await prepareReleaseCatalogMutation(input.database, input);
152
+ return {
153
+ catalog: prepared.catalog,
154
+ currentCatalog: prepared.currentCatalog,
155
+ diagnostics: prepared.compilation.diagnostics,
156
+ expectedReleaseRevision: prepared.applied.expectedRevision,
157
+ release: prepared.applied.release
158
+ };
159
+ }
160
+ async function commitReleaseCatalogMutations(input) {
161
+ if (input.mutations.length === 0) return [];
162
+ const scopeKeys = /* @__PURE__ */ new Set();
163
+ for (const mutation of input.mutations) {
164
+ validateScope(mutation.scope);
165
+ if (scopeKeys.has(mutation.scope.scopeKey)) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `A batch cannot mutate catalog scope "${mutation.scope.scopeKey}" more than once`);
166
+ scopeKeys.add(mutation.scope.scopeKey);
167
+ }
168
+ const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
169
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) throw new RangeError("maxAttempts must be a positive safe integer");
170
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
171
+ const prepared = await Promise.all(input.mutations.map((mutationInput) => prepareReleaseCatalogMutation(input.database, mutationInput)));
172
+ const result = await input.database.commit({
173
+ expectations: prepared.flatMap(({ applied, currentCatalog, mutationInput }) => [{
174
+ model: "releases",
175
+ id: mutationInput.mutation.operation === "insert" ? mutationInput.mutation.row.id : mutationInput.mutation.id,
176
+ revision: applied.expectedRevision
177
+ }, {
178
+ model: "releaseCatalogs",
179
+ scopeKey: mutationInput.scope.scopeKey,
180
+ generation: currentCatalog?.generation ?? null
181
+ }]),
182
+ changes: prepared.flatMap(({ applied, catalog, mutationInput }) => [
183
+ ...mutationInput.companionChanges ?? [],
184
+ applied.change,
185
+ {
186
+ model: "releaseCatalogs",
187
+ operation: "put",
188
+ row: catalog
189
+ },
190
+ ...mutationInput.trailingChanges ?? []
191
+ ])
192
+ });
193
+ if (result.committed) return prepared.map(({ applied, catalog }) => ({
194
+ attempts: attempt,
195
+ catalog,
196
+ release: applied.release
197
+ }));
198
+ if (result.conflict.reason !== "version_conflict") throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release mutation failed: ${result.conflict.reason}`);
199
+ }
200
+ throw new ReleaseCatalogMutationError("VERSION_CONFLICT", `Release catalog mutation batch conflicted ${maxAttempts} times`);
201
+ }
202
+ async function commitReleaseCatalogMutation(input) {
203
+ const [result] = await commitReleaseCatalogMutations({
204
+ database: input.database,
205
+ maxAttempts: input.maxAttempts,
206
+ mutations: [input]
207
+ });
208
+ return result;
209
+ }
210
+ const catalogProjectionMatches = (current, scope, compilation) => current.strategy === scope.strategy && current.channel_id === scope.channelId && current.channel_key === (0, _hot_updater_core.encodeChannelKey)(scope.channelName) && current.platform === scope.platform && current.fingerprint_hash === scope.fingerprintHash && current.payload === compilation.canonicalPayload && current.catalog_hash === compilation.catalogHash && current.byte_size === compilation.byteSize && current.is_tombstone === (compilation.payload.releaseDescriptors.length === 0);
211
+ async function preflightReleaseCatalogRebuild(input) {
212
+ validateScope(input.scope);
213
+ const { rows, currentCatalog } = await readCatalogScope(input.database, input.scope.scopeKey);
214
+ const compilation = await require_releaseCatalogCompiler.compileReleaseCatalog({
215
+ releases: rows.map(require_databaseRows.releaseRowToRelease),
216
+ strategy: input.scope.strategy
217
+ });
218
+ const changed = currentCatalog === null || !catalogProjectionMatches(currentCatalog, input.scope, compilation);
219
+ const generation = changed ? (currentCatalog?.generation ?? 0) + 1 : currentCatalog.generation;
220
+ if (!Number.isSafeInteger(generation)) throw new ReleaseCatalogMutationError("CATALOG_GENERATION_EXHAUSTED", `Catalog "${input.scope.scopeKey}" exhausted its generation counter`);
221
+ return {
222
+ changed,
223
+ currentCatalog,
224
+ diagnostics: compilation.diagnostics,
225
+ projectedCatalog: {
226
+ catalog_id: catalogIdentity(currentCatalog, rows),
227
+ byte_size: compilation.byteSize,
228
+ catalog_hash: compilation.catalogHash,
229
+ channel_id: input.scope.channelId,
230
+ channel_key: (0, _hot_updater_core.encodeChannelKey)(input.scope.channelName),
231
+ fingerprint_hash: input.scope.fingerprintHash,
232
+ generation,
233
+ is_tombstone: compilation.payload.releaseDescriptors.length === 0,
234
+ payload: compilation.canonicalPayload,
235
+ platform: input.scope.platform,
236
+ scope_key: input.scope.scopeKey,
237
+ strategy: input.scope.strategy,
238
+ updated_at_ms: changed || currentCatalog === null ? input.updatedAtMs ?? Date.now() : currentCatalog.updated_at_ms
239
+ }
240
+ };
241
+ }
242
+ async function rebuildReleaseCatalog(input) {
243
+ validateScope(input.scope);
244
+ const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
245
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) throw new RangeError("maxAttempts must be a positive safe integer");
246
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
247
+ const { rows, currentCatalog } = await readCatalogScope(input.database, input.scope.scopeKey);
248
+ const compilation = await require_releaseCatalogCompiler.compileReleaseCatalog({
249
+ releases: rows.map(require_databaseRows.releaseRowToRelease),
250
+ strategy: input.scope.strategy
251
+ });
252
+ if (currentCatalog !== null && catalogProjectionMatches(currentCatalog, input.scope, compilation)) return {
253
+ attempts: attempt,
254
+ catalog: currentCatalog,
255
+ changed: false,
256
+ diagnostics: compilation.diagnostics
257
+ };
258
+ const generation = (currentCatalog?.generation ?? 0) + 1;
259
+ if (!Number.isSafeInteger(generation)) throw new ReleaseCatalogMutationError("CATALOG_GENERATION_EXHAUSTED", `Catalog "${input.scope.scopeKey}" exhausted its generation counter`);
260
+ const catalog = {
261
+ catalog_id: catalogIdentity(currentCatalog, rows),
262
+ byte_size: compilation.byteSize,
263
+ catalog_hash: compilation.catalogHash,
264
+ channel_id: input.scope.channelId,
265
+ channel_key: (0, _hot_updater_core.encodeChannelKey)(input.scope.channelName),
266
+ fingerprint_hash: input.scope.fingerprintHash,
267
+ generation,
268
+ is_tombstone: compilation.payload.releaseDescriptors.length === 0,
269
+ payload: compilation.canonicalPayload,
270
+ platform: input.scope.platform,
271
+ scope_key: input.scope.scopeKey,
272
+ strategy: input.scope.strategy,
273
+ updated_at_ms: input.updatedAtMs ?? Date.now()
274
+ };
275
+ const result = await input.database.commit({
276
+ changes: [{
277
+ model: "releaseCatalogs",
278
+ operation: "put",
279
+ row: catalog
280
+ }],
281
+ expectations: [{
282
+ generation: currentCatalog?.generation ?? null,
283
+ model: "releaseCatalogs",
284
+ scopeKey: input.scope.scopeKey
285
+ }]
286
+ });
287
+ if (result.committed) return {
288
+ attempts: attempt,
289
+ catalog,
290
+ changed: true,
291
+ diagnostics: compilation.diagnostics
292
+ };
293
+ if (result.conflict.reason !== "version_conflict") throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release catalog rebuild failed: ${result.conflict.reason}`);
294
+ }
295
+ throw new ReleaseCatalogMutationError("VERSION_CONFLICT", `Release catalog rebuild conflicted ${maxAttempts} times`);
296
+ }
297
+ //#endregion
298
+ exports.ReleaseCatalogMutationError = ReleaseCatalogMutationError;
299
+ exports.commitReleaseCatalogMutation = commitReleaseCatalogMutation;
300
+ exports.commitReleaseCatalogMutations = commitReleaseCatalogMutations;
301
+ exports.preflightReleaseCatalogMutation = preflightReleaseCatalogMutation;
302
+ exports.preflightReleaseCatalogRebuild = preflightReleaseCatalogRebuild;
303
+ exports.rebuildReleaseCatalog = rebuildReleaseCatalog;