@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,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 };