@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,231 @@
|
|
|
1
|
+
require("./_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_uuidv7 = require("./uuidv7.cjs");
|
|
3
|
+
const require_releaseCatalogMutation = require("./releaseCatalogMutation.cjs");
|
|
4
|
+
let _hot_updater_core = require("@hot-updater/core");
|
|
5
|
+
//#region src/releaseManagement.ts
|
|
6
|
+
const RELEASE_PAGE_SIZE = 1e3;
|
|
7
|
+
var ReleaseManagementError = class extends Error {
|
|
8
|
+
name = "ReleaseManagementError";
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const readReleaseScope = async (database, scopeKey) => {
|
|
15
|
+
const rows = [];
|
|
16
|
+
let afterReleaseId;
|
|
17
|
+
for (;;) {
|
|
18
|
+
const page = await database.models.releases.findManyByScope({
|
|
19
|
+
scopeKey,
|
|
20
|
+
...afterReleaseId === void 0 ? {} : { afterReleaseId },
|
|
21
|
+
consistency: "strong",
|
|
22
|
+
limit: RELEASE_PAGE_SIZE
|
|
23
|
+
});
|
|
24
|
+
rows.push(...page);
|
|
25
|
+
if (page.length < RELEASE_PAGE_SIZE) return rows;
|
|
26
|
+
const nextCursor = page.at(-1)?.id;
|
|
27
|
+
if (nextCursor === void 0 || nextCursor === afterReleaseId) throw new require_releaseCatalogMutation.ReleaseCatalogMutationError("INVALID_SCOPE", `Release paging did not advance for scope "${scopeKey}".`);
|
|
28
|
+
afterReleaseId = nextCursor;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const createForwardReleaseId = (rows, candidates, nowMs) => {
|
|
32
|
+
return require_uuidv7.createUUIDv7After([...candidates, rows.at(-1)?.id ?? null].filter((value) => value !== null && (0, _hot_updater_core.isUUIDv7)(value)).sort().at(-1) ?? null, nowMs);
|
|
33
|
+
};
|
|
34
|
+
const prepareTargetScope = async ({ channelName, database, fingerprintHash, platform, strategy }) => {
|
|
35
|
+
const channelKey = (0, _hot_updater_core.encodeChannelKey)(channelName);
|
|
36
|
+
const channel = (await database.models.channels.list({})).channels.find(({ name }) => name === channelName) ?? (await database.models.channels.insert({
|
|
37
|
+
row: {
|
|
38
|
+
id: `channel:${channelKey}`,
|
|
39
|
+
name: channelName
|
|
40
|
+
},
|
|
41
|
+
onConflict: "returnExisting"
|
|
42
|
+
})).row;
|
|
43
|
+
const scopeKey = strategy === "APP_VERSION" ? (0, _hot_updater_core.createReleaseCatalogScopeKey)({
|
|
44
|
+
channelKey,
|
|
45
|
+
platform,
|
|
46
|
+
strategy
|
|
47
|
+
}) : (0, _hot_updater_core.createReleaseCatalogScopeKey)({
|
|
48
|
+
channelKey,
|
|
49
|
+
fingerprintHash: fingerprintHash ?? "",
|
|
50
|
+
platform,
|
|
51
|
+
strategy
|
|
52
|
+
});
|
|
53
|
+
return { scope: {
|
|
54
|
+
channelId: channel.id,
|
|
55
|
+
channelName,
|
|
56
|
+
fingerprintHash,
|
|
57
|
+
platform,
|
|
58
|
+
scopeKey,
|
|
59
|
+
strategy
|
|
60
|
+
} };
|
|
61
|
+
};
|
|
62
|
+
const loadReleaseTarget = async (input) => {
|
|
63
|
+
const release = await input.database.models.releases.findById(input.releaseId);
|
|
64
|
+
if (release === null) throw new ReleaseManagementError("RELEASE_NOT_FOUND", `Release "${input.releaseId}" was not found.`);
|
|
65
|
+
if (input.expectedRevision !== void 0 && input.expectedRevision !== release.revision) throw new ReleaseManagementError("VERSION_CONFLICT", `Release "${release.id}" is revision ${release.revision}; expected ${input.expectedRevision}.`);
|
|
66
|
+
const catalog = await input.database.models.releaseCatalogs.findByScopeKey(release.scope_key);
|
|
67
|
+
if (catalog === null) throw new require_releaseCatalogMutation.ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${release.id}" has no catalog projection.`);
|
|
68
|
+
return {
|
|
69
|
+
release,
|
|
70
|
+
scope: {
|
|
71
|
+
channelId: release.channel_id,
|
|
72
|
+
channelName: (0, _hot_updater_core.decodeChannelKey)(catalog.channel_key),
|
|
73
|
+
fingerprintHash: release.fingerprint_hash,
|
|
74
|
+
platform: release.platform,
|
|
75
|
+
scopeKey: release.scope_key,
|
|
76
|
+
strategy: release.strategy
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const releasePolicyUpdate = (release, patch, updatedAtMs) => {
|
|
81
|
+
if (patch.fingerprintHash !== void 0 && patch.fingerprintHash !== release.fingerprint_hash) throw new ReleaseManagementError("SCOPE_MOVE_UNSUPPORTED", "Changing a fingerprint target requires an atomic Release scope move.");
|
|
82
|
+
return {
|
|
83
|
+
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
84
|
+
...patch.message === void 0 ? {} : { message: patch.message },
|
|
85
|
+
...patch.rolloutCohortCount === void 0 ? {} : { rollout_cohort_count: patch.rolloutCohortCount },
|
|
86
|
+
...patch.shouldForceUpdate === void 0 ? {} : { should_force_update: patch.shouldForceUpdate },
|
|
87
|
+
...patch.targetAppVersion === void 0 ? {} : { target_app_version: patch.targetAppVersion },
|
|
88
|
+
...patch.targetCohorts === void 0 ? {} : { target_cohorts: [...patch.targetCohorts] },
|
|
89
|
+
updated_at_ms: updatedAtMs
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
async function updateReleasePolicy(input) {
|
|
93
|
+
const { release, scope } = await loadReleaseTarget(input);
|
|
94
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
95
|
+
return require_releaseCatalogMutation.commitReleaseCatalogMutation({
|
|
96
|
+
database: input.database,
|
|
97
|
+
mutation: {
|
|
98
|
+
id: release.id,
|
|
99
|
+
operation: "update",
|
|
100
|
+
update: releasePolicyUpdate(release, input.patch, updatedAtMs)
|
|
101
|
+
},
|
|
102
|
+
scope,
|
|
103
|
+
updatedAtMs
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function preflightReleasePolicy(input) {
|
|
107
|
+
const { release, scope } = await loadReleaseTarget(input);
|
|
108
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
109
|
+
return require_releaseCatalogMutation.preflightReleaseCatalogMutation({
|
|
110
|
+
database: input.database,
|
|
111
|
+
mutation: {
|
|
112
|
+
id: release.id,
|
|
113
|
+
operation: "update",
|
|
114
|
+
update: releasePolicyUpdate(release, input.patch, updatedAtMs)
|
|
115
|
+
},
|
|
116
|
+
scope,
|
|
117
|
+
updatedAtMs
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function deleteRelease(input) {
|
|
121
|
+
const { release, scope } = await loadReleaseTarget(input);
|
|
122
|
+
if (release.enabled) throw new ReleaseManagementError("ENABLED_RELEASE", `Disable Release "${release.id}" before hard deletion.`);
|
|
123
|
+
return require_releaseCatalogMutation.commitReleaseCatalogMutation({
|
|
124
|
+
database: input.database,
|
|
125
|
+
mutation: {
|
|
126
|
+
id: release.id,
|
|
127
|
+
operation: "delete"
|
|
128
|
+
},
|
|
129
|
+
scope,
|
|
130
|
+
updatedAtMs: input.updatedAtMs
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function promoteRelease(input) {
|
|
134
|
+
const { release: source, scope: sourceScope } = await loadReleaseTarget(input);
|
|
135
|
+
if (source.kind !== "BUNDLE" || source.bundle_id === null) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Only a Bundle Release can be promoted.");
|
|
136
|
+
const targetChannel = input.targetChannel.trim();
|
|
137
|
+
if (targetChannel.length === 0) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Promotion requires a target channel.");
|
|
138
|
+
const { scope: targetScope } = await prepareTargetScope({
|
|
139
|
+
channelName: targetChannel,
|
|
140
|
+
database: input.database,
|
|
141
|
+
fingerprintHash: source.fingerprint_hash,
|
|
142
|
+
platform: source.platform,
|
|
143
|
+
strategy: source.strategy
|
|
144
|
+
});
|
|
145
|
+
if (targetScope.scopeKey === sourceScope.scopeKey) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Source and target Release scopes are the same.");
|
|
146
|
+
const targetRows = await readReleaseScope(input.database, targetScope.scopeKey);
|
|
147
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
148
|
+
const targetMutation = {
|
|
149
|
+
mutation: {
|
|
150
|
+
operation: "insert",
|
|
151
|
+
row: {
|
|
152
|
+
...source,
|
|
153
|
+
id: createForwardReleaseId(targetRows, [source.id, source.bundle_id], updatedAtMs),
|
|
154
|
+
revision: 1,
|
|
155
|
+
scope_key: targetScope.scopeKey,
|
|
156
|
+
channel_id: targetScope.channelId,
|
|
157
|
+
enabled: true,
|
|
158
|
+
rollout_cohort_count: 1e3,
|
|
159
|
+
target_cohorts: [],
|
|
160
|
+
operation: "PROMOTE",
|
|
161
|
+
source_release_id: source.id,
|
|
162
|
+
created_at_ms: updatedAtMs,
|
|
163
|
+
updated_at_ms: updatedAtMs
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
scope: targetScope,
|
|
167
|
+
updatedAtMs
|
|
168
|
+
};
|
|
169
|
+
const sourceMutation = input.action === "move" ? {
|
|
170
|
+
mutation: {
|
|
171
|
+
id: source.id,
|
|
172
|
+
operation: "update",
|
|
173
|
+
update: {
|
|
174
|
+
enabled: false,
|
|
175
|
+
updated_at_ms: updatedAtMs
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
scope: sourceScope,
|
|
179
|
+
updatedAtMs
|
|
180
|
+
} : null;
|
|
181
|
+
const results = await require_releaseCatalogMutation.commitReleaseCatalogMutations({
|
|
182
|
+
database: input.database,
|
|
183
|
+
mutations: sourceMutation === null ? [targetMutation] : [sourceMutation, targetMutation]
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
source: sourceMutation === null ? null : results[0],
|
|
187
|
+
target: results.at(-1)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const prepareRollbackRelease = async (input) => {
|
|
191
|
+
const { release: source, scope } = await loadReleaseTarget(input);
|
|
192
|
+
if (!source.enabled) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Rollback requires an enabled Release.");
|
|
193
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
194
|
+
return {
|
|
195
|
+
mutation: {
|
|
196
|
+
id: source.id,
|
|
197
|
+
operation: "update",
|
|
198
|
+
update: {
|
|
199
|
+
enabled: false,
|
|
200
|
+
updated_at_ms: updatedAtMs
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
scope,
|
|
204
|
+
updatedAtMs
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
async function rollbackReleases(input) {
|
|
208
|
+
const mutations = await Promise.all(input.rollbacks.map((rollback) => prepareRollbackRelease({
|
|
209
|
+
...rollback,
|
|
210
|
+
database: input.database
|
|
211
|
+
})));
|
|
212
|
+
return require_releaseCatalogMutation.commitReleaseCatalogMutations({
|
|
213
|
+
database: input.database,
|
|
214
|
+
mutations
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async function rollbackRelease(input) {
|
|
218
|
+
const [result] = await rollbackReleases({
|
|
219
|
+
database: input.database,
|
|
220
|
+
rollbacks: [input]
|
|
221
|
+
});
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
exports.ReleaseManagementError = ReleaseManagementError;
|
|
226
|
+
exports.deleteRelease = deleteRelease;
|
|
227
|
+
exports.preflightReleasePolicy = preflightReleasePolicy;
|
|
228
|
+
exports.promoteRelease = promoteRelease;
|
|
229
|
+
exports.rollbackRelease = rollbackRelease;
|
|
230
|
+
exports.rollbackReleases = rollbackReleases;
|
|
231
|
+
exports.updateReleasePolicy = updateReleasePolicy;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BundleRepository } from "./types/databasePlugin.cjs";
|
|
2
|
+
import { ReleaseCatalogMutationPreflight, ReleaseCatalogMutationResult } from "./releaseCatalogMutation.cjs";
|
|
3
|
+
|
|
4
|
+
//#region src/releaseManagement.d.ts
|
|
5
|
+
interface ReleasePolicyPatch {
|
|
6
|
+
readonly enabled?: boolean;
|
|
7
|
+
readonly fingerprintHash?: string;
|
|
8
|
+
readonly message?: string | null;
|
|
9
|
+
readonly rolloutCohortCount?: number;
|
|
10
|
+
readonly shouldForceUpdate?: boolean;
|
|
11
|
+
readonly targetAppVersion?: string;
|
|
12
|
+
readonly targetCohorts?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
interface ReleaseMutationTarget {
|
|
15
|
+
readonly database: BundleRepository;
|
|
16
|
+
readonly expectedRevision?: number;
|
|
17
|
+
readonly releaseId: string;
|
|
18
|
+
readonly updatedAtMs?: number;
|
|
19
|
+
}
|
|
20
|
+
declare class ReleaseManagementError extends Error {
|
|
21
|
+
readonly code: "ENABLED_RELEASE" | "RELEASE_NOT_FOUND" | "SCOPE_MOVE_UNSUPPORTED" | "TARGET_RELEASE_INVALID" | "VERSION_CONFLICT";
|
|
22
|
+
readonly name = "ReleaseManagementError";
|
|
23
|
+
constructor(code: "ENABLED_RELEASE" | "RELEASE_NOT_FOUND" | "SCOPE_MOVE_UNSUPPORTED" | "TARGET_RELEASE_INVALID" | "VERSION_CONFLICT", message: string);
|
|
24
|
+
}
|
|
25
|
+
declare function updateReleasePolicy(input: ReleaseMutationTarget & {
|
|
26
|
+
readonly patch: ReleasePolicyPatch;
|
|
27
|
+
}): Promise<ReleaseCatalogMutationResult>;
|
|
28
|
+
declare function preflightReleasePolicy(input: ReleaseMutationTarget & {
|
|
29
|
+
readonly patch: ReleasePolicyPatch;
|
|
30
|
+
}): Promise<ReleaseCatalogMutationPreflight>;
|
|
31
|
+
declare function deleteRelease(input: ReleaseMutationTarget): Promise<ReleaseCatalogMutationResult>;
|
|
32
|
+
interface PromoteReleaseInput extends ReleaseMutationTarget {
|
|
33
|
+
readonly action?: "copy" | "move";
|
|
34
|
+
readonly targetChannel: string;
|
|
35
|
+
}
|
|
36
|
+
interface PromoteReleaseResult {
|
|
37
|
+
readonly source: ReleaseCatalogMutationResult | null;
|
|
38
|
+
readonly target: ReleaseCatalogMutationResult;
|
|
39
|
+
}
|
|
40
|
+
declare function promoteRelease(input: PromoteReleaseInput): Promise<PromoteReleaseResult>;
|
|
41
|
+
type RollbackReleaseInput = ReleaseMutationTarget;
|
|
42
|
+
declare function rollbackReleases(input: {
|
|
43
|
+
readonly database: BundleRepository;
|
|
44
|
+
readonly rollbacks: readonly Omit<RollbackReleaseInput, "database">[];
|
|
45
|
+
}): Promise<readonly ReleaseCatalogMutationResult[]>;
|
|
46
|
+
declare function rollbackRelease(input: RollbackReleaseInput): Promise<ReleaseCatalogMutationResult>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { PromoteReleaseInput, PromoteReleaseResult, ReleaseManagementError, ReleaseMutationTarget, ReleasePolicyPatch, RollbackReleaseInput, deleteRelease, preflightReleasePolicy, promoteRelease, rollbackRelease, rollbackReleases, updateReleasePolicy };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BundleRepository } from "./types/databasePlugin.mjs";
|
|
2
|
+
import { ReleaseCatalogMutationPreflight, ReleaseCatalogMutationResult } from "./releaseCatalogMutation.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/releaseManagement.d.ts
|
|
5
|
+
interface ReleasePolicyPatch {
|
|
6
|
+
readonly enabled?: boolean;
|
|
7
|
+
readonly fingerprintHash?: string;
|
|
8
|
+
readonly message?: string | null;
|
|
9
|
+
readonly rolloutCohortCount?: number;
|
|
10
|
+
readonly shouldForceUpdate?: boolean;
|
|
11
|
+
readonly targetAppVersion?: string;
|
|
12
|
+
readonly targetCohorts?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
interface ReleaseMutationTarget {
|
|
15
|
+
readonly database: BundleRepository;
|
|
16
|
+
readonly expectedRevision?: number;
|
|
17
|
+
readonly releaseId: string;
|
|
18
|
+
readonly updatedAtMs?: number;
|
|
19
|
+
}
|
|
20
|
+
declare class ReleaseManagementError extends Error {
|
|
21
|
+
readonly code: "ENABLED_RELEASE" | "RELEASE_NOT_FOUND" | "SCOPE_MOVE_UNSUPPORTED" | "TARGET_RELEASE_INVALID" | "VERSION_CONFLICT";
|
|
22
|
+
readonly name = "ReleaseManagementError";
|
|
23
|
+
constructor(code: "ENABLED_RELEASE" | "RELEASE_NOT_FOUND" | "SCOPE_MOVE_UNSUPPORTED" | "TARGET_RELEASE_INVALID" | "VERSION_CONFLICT", message: string);
|
|
24
|
+
}
|
|
25
|
+
declare function updateReleasePolicy(input: ReleaseMutationTarget & {
|
|
26
|
+
readonly patch: ReleasePolicyPatch;
|
|
27
|
+
}): Promise<ReleaseCatalogMutationResult>;
|
|
28
|
+
declare function preflightReleasePolicy(input: ReleaseMutationTarget & {
|
|
29
|
+
readonly patch: ReleasePolicyPatch;
|
|
30
|
+
}): Promise<ReleaseCatalogMutationPreflight>;
|
|
31
|
+
declare function deleteRelease(input: ReleaseMutationTarget): Promise<ReleaseCatalogMutationResult>;
|
|
32
|
+
interface PromoteReleaseInput extends ReleaseMutationTarget {
|
|
33
|
+
readonly action?: "copy" | "move";
|
|
34
|
+
readonly targetChannel: string;
|
|
35
|
+
}
|
|
36
|
+
interface PromoteReleaseResult {
|
|
37
|
+
readonly source: ReleaseCatalogMutationResult | null;
|
|
38
|
+
readonly target: ReleaseCatalogMutationResult;
|
|
39
|
+
}
|
|
40
|
+
declare function promoteRelease(input: PromoteReleaseInput): Promise<PromoteReleaseResult>;
|
|
41
|
+
type RollbackReleaseInput = ReleaseMutationTarget;
|
|
42
|
+
declare function rollbackReleases(input: {
|
|
43
|
+
readonly database: BundleRepository;
|
|
44
|
+
readonly rollbacks: readonly Omit<RollbackReleaseInput, "database">[];
|
|
45
|
+
}): Promise<readonly ReleaseCatalogMutationResult[]>;
|
|
46
|
+
declare function rollbackRelease(input: RollbackReleaseInput): Promise<ReleaseCatalogMutationResult>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { PromoteReleaseInput, PromoteReleaseResult, ReleaseManagementError, ReleaseMutationTarget, ReleasePolicyPatch, RollbackReleaseInput, deleteRelease, preflightReleasePolicy, promoteRelease, rollbackRelease, rollbackReleases, updateReleasePolicy };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createUUIDv7After, isUUIDv7 as isUUIDv7$1 } from "./uuidv7.mjs";
|
|
2
|
+
import { ReleaseCatalogMutationError, commitReleaseCatalogMutation, commitReleaseCatalogMutations, preflightReleaseCatalogMutation } from "./releaseCatalogMutation.mjs";
|
|
3
|
+
import { createReleaseCatalogScopeKey, decodeChannelKey, encodeChannelKey } from "@hot-updater/core";
|
|
4
|
+
//#region src/releaseManagement.ts
|
|
5
|
+
const RELEASE_PAGE_SIZE = 1e3;
|
|
6
|
+
var ReleaseManagementError = class extends Error {
|
|
7
|
+
name = "ReleaseManagementError";
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const readReleaseScope = async (database, scopeKey) => {
|
|
14
|
+
const rows = [];
|
|
15
|
+
let afterReleaseId;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const page = await database.models.releases.findManyByScope({
|
|
18
|
+
scopeKey,
|
|
19
|
+
...afterReleaseId === void 0 ? {} : { afterReleaseId },
|
|
20
|
+
consistency: "strong",
|
|
21
|
+
limit: RELEASE_PAGE_SIZE
|
|
22
|
+
});
|
|
23
|
+
rows.push(...page);
|
|
24
|
+
if (page.length < RELEASE_PAGE_SIZE) return rows;
|
|
25
|
+
const nextCursor = page.at(-1)?.id;
|
|
26
|
+
if (nextCursor === void 0 || nextCursor === afterReleaseId) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release paging did not advance for scope "${scopeKey}".`);
|
|
27
|
+
afterReleaseId = nextCursor;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const createForwardReleaseId = (rows, candidates, nowMs) => {
|
|
31
|
+
return createUUIDv7After([...candidates, rows.at(-1)?.id ?? null].filter((value) => value !== null && isUUIDv7$1(value)).sort().at(-1) ?? null, nowMs);
|
|
32
|
+
};
|
|
33
|
+
const prepareTargetScope = async ({ channelName, database, fingerprintHash, platform, strategy }) => {
|
|
34
|
+
const channelKey = encodeChannelKey(channelName);
|
|
35
|
+
const channel = (await database.models.channels.list({})).channels.find(({ name }) => name === channelName) ?? (await database.models.channels.insert({
|
|
36
|
+
row: {
|
|
37
|
+
id: `channel:${channelKey}`,
|
|
38
|
+
name: channelName
|
|
39
|
+
},
|
|
40
|
+
onConflict: "returnExisting"
|
|
41
|
+
})).row;
|
|
42
|
+
const scopeKey = strategy === "APP_VERSION" ? createReleaseCatalogScopeKey({
|
|
43
|
+
channelKey,
|
|
44
|
+
platform,
|
|
45
|
+
strategy
|
|
46
|
+
}) : createReleaseCatalogScopeKey({
|
|
47
|
+
channelKey,
|
|
48
|
+
fingerprintHash: fingerprintHash ?? "",
|
|
49
|
+
platform,
|
|
50
|
+
strategy
|
|
51
|
+
});
|
|
52
|
+
return { scope: {
|
|
53
|
+
channelId: channel.id,
|
|
54
|
+
channelName,
|
|
55
|
+
fingerprintHash,
|
|
56
|
+
platform,
|
|
57
|
+
scopeKey,
|
|
58
|
+
strategy
|
|
59
|
+
} };
|
|
60
|
+
};
|
|
61
|
+
const loadReleaseTarget = async (input) => {
|
|
62
|
+
const release = await input.database.models.releases.findById(input.releaseId);
|
|
63
|
+
if (release === null) throw new ReleaseManagementError("RELEASE_NOT_FOUND", `Release "${input.releaseId}" was not found.`);
|
|
64
|
+
if (input.expectedRevision !== void 0 && input.expectedRevision !== release.revision) throw new ReleaseManagementError("VERSION_CONFLICT", `Release "${release.id}" is revision ${release.revision}; expected ${input.expectedRevision}.`);
|
|
65
|
+
const catalog = await input.database.models.releaseCatalogs.findByScopeKey(release.scope_key);
|
|
66
|
+
if (catalog === null) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${release.id}" has no catalog projection.`);
|
|
67
|
+
return {
|
|
68
|
+
release,
|
|
69
|
+
scope: {
|
|
70
|
+
channelId: release.channel_id,
|
|
71
|
+
channelName: decodeChannelKey(catalog.channel_key),
|
|
72
|
+
fingerprintHash: release.fingerprint_hash,
|
|
73
|
+
platform: release.platform,
|
|
74
|
+
scopeKey: release.scope_key,
|
|
75
|
+
strategy: release.strategy
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const releasePolicyUpdate = (release, patch, updatedAtMs) => {
|
|
80
|
+
if (patch.fingerprintHash !== void 0 && patch.fingerprintHash !== release.fingerprint_hash) throw new ReleaseManagementError("SCOPE_MOVE_UNSUPPORTED", "Changing a fingerprint target requires an atomic Release scope move.");
|
|
81
|
+
return {
|
|
82
|
+
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
83
|
+
...patch.message === void 0 ? {} : { message: patch.message },
|
|
84
|
+
...patch.rolloutCohortCount === void 0 ? {} : { rollout_cohort_count: patch.rolloutCohortCount },
|
|
85
|
+
...patch.shouldForceUpdate === void 0 ? {} : { should_force_update: patch.shouldForceUpdate },
|
|
86
|
+
...patch.targetAppVersion === void 0 ? {} : { target_app_version: patch.targetAppVersion },
|
|
87
|
+
...patch.targetCohorts === void 0 ? {} : { target_cohorts: [...patch.targetCohorts] },
|
|
88
|
+
updated_at_ms: updatedAtMs
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
async function updateReleasePolicy(input) {
|
|
92
|
+
const { release, scope } = await loadReleaseTarget(input);
|
|
93
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
94
|
+
return commitReleaseCatalogMutation({
|
|
95
|
+
database: input.database,
|
|
96
|
+
mutation: {
|
|
97
|
+
id: release.id,
|
|
98
|
+
operation: "update",
|
|
99
|
+
update: releasePolicyUpdate(release, input.patch, updatedAtMs)
|
|
100
|
+
},
|
|
101
|
+
scope,
|
|
102
|
+
updatedAtMs
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async function preflightReleasePolicy(input) {
|
|
106
|
+
const { release, scope } = await loadReleaseTarget(input);
|
|
107
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
108
|
+
return preflightReleaseCatalogMutation({
|
|
109
|
+
database: input.database,
|
|
110
|
+
mutation: {
|
|
111
|
+
id: release.id,
|
|
112
|
+
operation: "update",
|
|
113
|
+
update: releasePolicyUpdate(release, input.patch, updatedAtMs)
|
|
114
|
+
},
|
|
115
|
+
scope,
|
|
116
|
+
updatedAtMs
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function deleteRelease(input) {
|
|
120
|
+
const { release, scope } = await loadReleaseTarget(input);
|
|
121
|
+
if (release.enabled) throw new ReleaseManagementError("ENABLED_RELEASE", `Disable Release "${release.id}" before hard deletion.`);
|
|
122
|
+
return commitReleaseCatalogMutation({
|
|
123
|
+
database: input.database,
|
|
124
|
+
mutation: {
|
|
125
|
+
id: release.id,
|
|
126
|
+
operation: "delete"
|
|
127
|
+
},
|
|
128
|
+
scope,
|
|
129
|
+
updatedAtMs: input.updatedAtMs
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function promoteRelease(input) {
|
|
133
|
+
const { release: source, scope: sourceScope } = await loadReleaseTarget(input);
|
|
134
|
+
if (source.kind !== "BUNDLE" || source.bundle_id === null) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Only a Bundle Release can be promoted.");
|
|
135
|
+
const targetChannel = input.targetChannel.trim();
|
|
136
|
+
if (targetChannel.length === 0) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Promotion requires a target channel.");
|
|
137
|
+
const { scope: targetScope } = await prepareTargetScope({
|
|
138
|
+
channelName: targetChannel,
|
|
139
|
+
database: input.database,
|
|
140
|
+
fingerprintHash: source.fingerprint_hash,
|
|
141
|
+
platform: source.platform,
|
|
142
|
+
strategy: source.strategy
|
|
143
|
+
});
|
|
144
|
+
if (targetScope.scopeKey === sourceScope.scopeKey) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Source and target Release scopes are the same.");
|
|
145
|
+
const targetRows = await readReleaseScope(input.database, targetScope.scopeKey);
|
|
146
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
147
|
+
const targetMutation = {
|
|
148
|
+
mutation: {
|
|
149
|
+
operation: "insert",
|
|
150
|
+
row: {
|
|
151
|
+
...source,
|
|
152
|
+
id: createForwardReleaseId(targetRows, [source.id, source.bundle_id], updatedAtMs),
|
|
153
|
+
revision: 1,
|
|
154
|
+
scope_key: targetScope.scopeKey,
|
|
155
|
+
channel_id: targetScope.channelId,
|
|
156
|
+
enabled: true,
|
|
157
|
+
rollout_cohort_count: 1e3,
|
|
158
|
+
target_cohorts: [],
|
|
159
|
+
operation: "PROMOTE",
|
|
160
|
+
source_release_id: source.id,
|
|
161
|
+
created_at_ms: updatedAtMs,
|
|
162
|
+
updated_at_ms: updatedAtMs
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
scope: targetScope,
|
|
166
|
+
updatedAtMs
|
|
167
|
+
};
|
|
168
|
+
const sourceMutation = input.action === "move" ? {
|
|
169
|
+
mutation: {
|
|
170
|
+
id: source.id,
|
|
171
|
+
operation: "update",
|
|
172
|
+
update: {
|
|
173
|
+
enabled: false,
|
|
174
|
+
updated_at_ms: updatedAtMs
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
scope: sourceScope,
|
|
178
|
+
updatedAtMs
|
|
179
|
+
} : null;
|
|
180
|
+
const results = await commitReleaseCatalogMutations({
|
|
181
|
+
database: input.database,
|
|
182
|
+
mutations: sourceMutation === null ? [targetMutation] : [sourceMutation, targetMutation]
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
source: sourceMutation === null ? null : results[0],
|
|
186
|
+
target: results.at(-1)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const prepareRollbackRelease = async (input) => {
|
|
190
|
+
const { release: source, scope } = await loadReleaseTarget(input);
|
|
191
|
+
if (!source.enabled) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Rollback requires an enabled Release.");
|
|
192
|
+
const updatedAtMs = input.updatedAtMs ?? Date.now();
|
|
193
|
+
return {
|
|
194
|
+
mutation: {
|
|
195
|
+
id: source.id,
|
|
196
|
+
operation: "update",
|
|
197
|
+
update: {
|
|
198
|
+
enabled: false,
|
|
199
|
+
updated_at_ms: updatedAtMs
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
scope,
|
|
203
|
+
updatedAtMs
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
async function rollbackReleases(input) {
|
|
207
|
+
const mutations = await Promise.all(input.rollbacks.map((rollback) => prepareRollbackRelease({
|
|
208
|
+
...rollback,
|
|
209
|
+
database: input.database
|
|
210
|
+
})));
|
|
211
|
+
return commitReleaseCatalogMutations({
|
|
212
|
+
database: input.database,
|
|
213
|
+
mutations
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function rollbackRelease(input) {
|
|
217
|
+
const [result] = await rollbackReleases({
|
|
218
|
+
database: input.database,
|
|
219
|
+
rollbacks: [input]
|
|
220
|
+
});
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
export { ReleaseManagementError, deleteRelease, preflightReleasePolicy, promoteRelease, rollbackRelease, rollbackReleases, updateReleasePolicy };
|