@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.
- package/dist/assetStorageLayout.cjs +36 -18
- package/dist/assetStorageLayout.d.cts +21 -6
- package/dist/assetStorageLayout.d.mts +21 -6
- package/dist/assetStorageLayout.mjs +36 -18
- package/dist/bundleStorageLayout.cjs +2 -3
- package/dist/bundleStorageLayout.mjs +2 -3
- package/dist/contentAddressedAssets.cjs +5 -0
- package/dist/contentAddressedAssets.d.cts +2 -1
- package/dist/contentAddressedAssets.d.mts +2 -1
- package/dist/contentAddressedAssets.mjs +5 -1
- package/dist/createDatabasePlugin.cjs +775 -284
- package/dist/createDatabasePlugin.d.cts +20 -64
- package/dist/createDatabasePlugin.d.mts +20 -64
- package/dist/createDatabasePlugin.mjs +773 -284
- package/dist/createStorageKeyBuilder.cjs +9 -2
- package/dist/createStorageKeyBuilder.mjs +9 -2
- package/dist/createStoragePlugin.cjs +6 -145
- package/dist/createStoragePlugin.d.cts +7 -52
- package/dist/createStoragePlugin.d.mts +7 -52
- package/dist/createStoragePlugin.mjs +5 -143
- package/dist/databaseClient.cjs +112 -0
- package/dist/databaseClient.d.cts +33 -0
- package/dist/databaseClient.d.mts +33 -0
- package/dist/databaseClient.mjs +110 -0
- package/dist/databaseClientReads.cjs +113 -0
- package/dist/databaseClientReads.mjs +112 -0
- package/dist/databaseClientUpdates.cjs +39 -0
- package/dist/databaseClientUpdates.d.cts +11 -0
- package/dist/databaseClientUpdates.d.mts +11 -0
- package/dist/databaseClientUpdates.mjs +36 -0
- package/dist/databaseJsonValue.cjs +37 -0
- package/dist/databaseJsonValue.d.cts +5 -0
- package/dist/databaseJsonValue.d.mts +5 -0
- package/dist/databaseJsonValue.mjs +37 -0
- package/dist/databaseMetadata.cjs +13 -0
- package/dist/databaseMetadata.mjs +12 -0
- package/dist/databasePluginCrud.cjs +86 -0
- package/dist/databasePluginCrud.d.cts +1 -0
- package/dist/databasePluginCrud.d.mts +1 -0
- package/dist/databasePluginCrud.mjs +86 -0
- package/dist/databasePluginCrudValidation.d.cts +1 -0
- package/dist/databasePluginCrudValidation.d.mts +1 -0
- package/dist/databasePluginCrudValidationErrors.cjs +10 -0
- package/dist/databasePluginCrudValidationErrors.d.cts +9 -0
- package/dist/databasePluginCrudValidationErrors.d.mts +9 -0
- package/dist/databasePluginCrudValidationErrors.mjs +10 -0
- package/dist/databasePluginCrudValidationFields.cjs +291 -0
- package/dist/databasePluginCrudValidationFields.d.cts +1 -0
- package/dist/databasePluginCrudValidationFields.d.mts +1 -0
- package/dist/databasePluginCrudValidationFields.mjs +281 -0
- package/dist/databasePluginCrudValidationMutations.cjs +88 -0
- package/dist/databasePluginCrudValidationMutations.d.cts +1 -0
- package/dist/databasePluginCrudValidationMutations.d.mts +1 -0
- package/dist/databasePluginCrudValidationMutations.mjs +82 -0
- package/dist/databasePluginCrudValidationQueries.cjs +103 -0
- package/dist/databasePluginCrudValidationQueries.d.cts +1 -0
- package/dist/databasePluginCrudValidationQueries.d.mts +1 -0
- package/dist/databasePluginCrudValidationQueries.mjs +97 -0
- package/dist/databasePluginCrudValidationRows.cjs +55 -0
- package/dist/databasePluginCrudValidationRows.d.cts +1 -0
- package/dist/databasePluginCrudValidationRows.d.mts +1 -0
- package/dist/databasePluginCrudValidationRows.mjs +53 -0
- package/dist/databasePluginTransaction.cjs +7 -0
- package/dist/databasePluginTransaction.mjs +7 -0
- package/dist/databaseRows.cjs +149 -0
- package/dist/databaseRows.d.cts +34 -0
- package/dist/databaseRows.d.mts +34 -0
- package/dist/databaseRows.mjs +141 -0
- package/dist/index.cjs +76 -24
- package/dist/index.d.cts +22 -12
- package/dist/index.d.mts +22 -12
- package/dist/index.mjs +21 -14
- package/dist/internal.cjs +6 -0
- package/dist/internal.d.cts +9 -0
- package/dist/internal.d.mts +9 -0
- package/dist/internal.mjs +3 -0
- package/dist/node_modules/.pnpm/verkit@0.3.2/node_modules/verkit/dist/index.cjs +345 -0
- package/dist/node_modules/.pnpm/verkit@0.3.2/node_modules/verkit/dist/index.mjs +341 -0
- package/dist/paginateBundles.d.cts +6 -4
- package/dist/paginateBundles.d.mts +6 -4
- package/dist/parseStorageUri.cjs +46 -30
- package/dist/parseStorageUri.d.cts +20 -17
- package/dist/parseStorageUri.d.mts +20 -17
- package/dist/parseStorageUri.mjs +46 -30
- package/dist/queryBundles.cjs +1 -7
- package/dist/queryBundles.d.cts +3 -1
- package/dist/queryBundles.d.mts +3 -1
- package/dist/queryBundles.mjs +1 -7
- package/dist/releaseCatalogCompiler.cjs +391 -0
- package/dist/releaseCatalogCompiler.d.cts +55 -0
- package/dist/releaseCatalogCompiler.d.mts +55 -0
- package/dist/releaseCatalogCompiler.mjs +386 -0
- package/dist/releaseCatalogMutation.cjs +303 -0
- package/dist/releaseCatalogMutation.d.cts +86 -0
- package/dist/releaseCatalogMutation.d.mts +86 -0
- package/dist/releaseCatalogMutation.mjs +297 -0
- package/dist/releaseManagement.cjs +231 -0
- package/dist/releaseManagement.d.cts +48 -0
- package/dist/releaseManagement.d.mts +48 -0
- package/dist/releaseManagement.mjs +224 -0
- package/dist/remoteBundleSigning.cjs +296 -0
- package/dist/remoteBundleSigning.d.cts +43 -0
- package/dist/remoteBundleSigning.d.mts +43 -0
- package/dist/remoteBundleSigning.mjs +291 -0
- package/dist/requestBundleCache.cjs +22 -0
- package/dist/requestBundleCache.d.cts +9 -0
- package/dist/requestBundleCache.d.mts +9 -0
- package/dist/requestBundleCache.mjs +22 -0
- package/dist/semverSatisfies.cjs +3 -4
- package/dist/semverSatisfies.mjs +1 -1
- package/dist/storageDownloadPath.cjs +48 -0
- package/dist/storageDownloadPath.d.cts +15 -0
- package/dist/storageDownloadPath.d.mts +15 -0
- package/dist/storageDownloadPath.mjs +46 -0
- package/dist/types/database.d.cts +4 -0
- package/dist/types/database.d.mts +4 -0
- package/dist/types/databaseFields.cjs +92 -0
- package/dist/types/databaseFields.d.cts +12 -0
- package/dist/types/databaseFields.d.mts +12 -0
- package/dist/types/databaseFields.mjs +92 -0
- package/dist/types/databaseOperations.d.cts +173 -0
- package/dist/types/databaseOperations.d.mts +173 -0
- package/dist/types/databasePlugin.d.cts +240 -0
- package/dist/types/databasePlugin.d.mts +240 -0
- package/dist/types/databaseQuery.d.cts +47 -0
- package/dist/types/databaseQuery.d.mts +47 -0
- package/dist/types/databaseRows.d.cts +120 -0
- package/dist/types/databaseRows.d.mts +120 -0
- package/dist/types/index.d.cts +120 -162
- package/dist/types/index.d.mts +120 -162
- package/dist/types/internal.d.cts +7 -0
- package/dist/types/internal.d.mts +7 -0
- package/dist/types/public.d.cts +3 -0
- package/dist/types/public.d.mts +3 -0
- package/dist/uuidv7.cjs +9 -0
- package/dist/uuidv7.d.cts +4 -1
- package/dist/uuidv7.d.mts +4 -1
- package/dist/uuidv7.mjs +8 -1
- package/package.json +10 -5
- package/dist/bundleUnitOfWork.cjs +0 -158
- package/dist/bundleUnitOfWork.mjs +0 -158
- package/dist/bundleUnitOfWorkStore.cjs +0 -15
- package/dist/bundleUnitOfWorkStore.mjs +0 -15
- package/dist/createBlobDatabasePlugin.cjs +0 -382
- package/dist/createBlobDatabasePlugin.d.cts +0 -29
- package/dist/createBlobDatabasePlugin.d.mts +0 -29
- package/dist/createBlobDatabasePlugin.mjs +0 -381
- package/dist/legacyAssetStorageLayout.cjs +0 -12
- package/dist/legacyAssetStorageLayout.mjs +0 -12
- package/dist/requestUpdateBundleState.cjs +0 -26
- package/dist/requestUpdateBundleState.d.cts +0 -12
- package/dist/requestUpdateBundleState.d.mts +0 -12
- package/dist/requestUpdateBundleState.mjs +0 -24
- package/dist/resolveUpdateInfoFromBundles.cjs +0 -14
- package/dist/resolveUpdateInfoFromBundles.d.cts +0 -16
- package/dist/resolveUpdateInfoFromBundles.d.mts +0 -16
- package/dist/resolveUpdateInfoFromBundles.mjs +0 -13
- package/dist/storageProfile.cjs +0 -15
- package/dist/storageProfile.d.cts +0 -9
- package/dist/storageProfile.d.mts +0 -9
- 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;
|