@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,86 @@
1
+ import { ReleaseCatalogRow, ReleaseRow } from "./types/databaseRows.cjs";
2
+ import { BundleRepository, DatabaseChange } from "./types/databasePlugin.cjs";
3
+ import { ReleaseRowUpdate } from "./types/databaseOperations.cjs";
4
+ import { ReleaseCatalogCompilation } from "./releaseCatalogCompiler.cjs";
5
+
6
+ //#region src/releaseCatalogMutation.d.ts
7
+ interface ReleaseCatalogScope {
8
+ readonly channelId: string;
9
+ readonly channelName: string;
10
+ readonly platform: "ios" | "android";
11
+ readonly scopeKey: string;
12
+ readonly strategy: "APP_VERSION" | "FINGERPRINT";
13
+ readonly fingerprintHash: string | null;
14
+ }
15
+ type ReleaseCatalogMutation = {
16
+ readonly operation: "insert";
17
+ readonly row: ReleaseRow;
18
+ } | {
19
+ readonly operation: "update";
20
+ readonly id: string;
21
+ readonly update: Omit<ReleaseRowUpdate, "revision" | "scope_key">;
22
+ } | {
23
+ readonly operation: "delete";
24
+ readonly id: string;
25
+ };
26
+ interface ReleaseCatalogMutationResult {
27
+ readonly attempts: number;
28
+ readonly catalog: ReleaseCatalogRow;
29
+ readonly release: ReleaseRow | null;
30
+ }
31
+ declare class ReleaseCatalogMutationError extends Error {
32
+ readonly code: "CATALOG_GENERATION_EXHAUSTED" | "CATALOG_IDENTITY_MISSING" | "INVALID_SCOPE" | "NON_MONOTONIC_RELEASE_ID" | "RELEASE_NOT_FOUND" | "VERSION_CONFLICT";
33
+ readonly name = "ReleaseCatalogMutationError";
34
+ constructor(code: "CATALOG_GENERATION_EXHAUSTED" | "CATALOG_IDENTITY_MISSING" | "INVALID_SCOPE" | "NON_MONOTONIC_RELEASE_ID" | "RELEASE_NOT_FOUND" | "VERSION_CONFLICT", message: string);
35
+ }
36
+ interface ReleaseCatalogMutationInput {
37
+ readonly scope: ReleaseCatalogScope;
38
+ readonly mutation: ReleaseCatalogMutation;
39
+ readonly companionChanges?: readonly DatabaseChange[];
40
+ readonly trailingChanges?: readonly DatabaseChange[];
41
+ readonly updatedAtMs?: number;
42
+ }
43
+ interface ReleaseCatalogMutationPreflight {
44
+ readonly catalog: ReleaseCatalogRow;
45
+ readonly currentCatalog: ReleaseCatalogRow | null;
46
+ readonly diagnostics: ReleaseCatalogCompilation["diagnostics"];
47
+ readonly expectedReleaseRevision: number | null;
48
+ readonly release: ReleaseRow | null;
49
+ }
50
+ declare function preflightReleaseCatalogMutation(input: ReleaseCatalogMutationInput & {
51
+ readonly database: BundleRepository;
52
+ }): Promise<ReleaseCatalogMutationPreflight>;
53
+ declare function commitReleaseCatalogMutations(input: {
54
+ readonly database: BundleRepository;
55
+ readonly mutations: readonly ReleaseCatalogMutationInput[];
56
+ readonly maxAttempts?: number;
57
+ }): Promise<readonly ReleaseCatalogMutationResult[]>;
58
+ declare function commitReleaseCatalogMutation(input: ReleaseCatalogMutationInput & {
59
+ readonly database: BundleRepository;
60
+ readonly maxAttempts?: number;
61
+ }): Promise<ReleaseCatalogMutationResult>;
62
+ interface ReleaseCatalogRebuildResult {
63
+ readonly attempts: number;
64
+ readonly catalog: ReleaseCatalogRow;
65
+ readonly changed: boolean;
66
+ readonly diagnostics: ReleaseCatalogCompilation["diagnostics"];
67
+ }
68
+ interface ReleaseCatalogRebuildPreflight {
69
+ readonly changed: boolean;
70
+ readonly currentCatalog: ReleaseCatalogRow | null;
71
+ readonly diagnostics: ReleaseCatalogCompilation["diagnostics"];
72
+ readonly projectedCatalog: ReleaseCatalogRow;
73
+ }
74
+ declare function preflightReleaseCatalogRebuild(input: {
75
+ readonly database: BundleRepository;
76
+ readonly scope: ReleaseCatalogScope;
77
+ readonly updatedAtMs?: number;
78
+ }): Promise<ReleaseCatalogRebuildPreflight>;
79
+ declare function rebuildReleaseCatalog(input: {
80
+ readonly database: BundleRepository;
81
+ readonly maxAttempts?: number;
82
+ readonly scope: ReleaseCatalogScope;
83
+ readonly updatedAtMs?: number;
84
+ }): Promise<ReleaseCatalogRebuildResult>;
85
+ //#endregion
86
+ export { ReleaseCatalogMutation, ReleaseCatalogMutationError, ReleaseCatalogMutationInput, ReleaseCatalogMutationPreflight, ReleaseCatalogMutationResult, ReleaseCatalogRebuildPreflight, ReleaseCatalogRebuildResult, ReleaseCatalogScope, commitReleaseCatalogMutation, commitReleaseCatalogMutations, preflightReleaseCatalogMutation, preflightReleaseCatalogRebuild, rebuildReleaseCatalog };
@@ -0,0 +1,86 @@
1
+ import { ReleaseCatalogRow, ReleaseRow } from "./types/databaseRows.mjs";
2
+ import { BundleRepository, DatabaseChange } from "./types/databasePlugin.mjs";
3
+ import { ReleaseRowUpdate } from "./types/databaseOperations.mjs";
4
+ import { ReleaseCatalogCompilation } from "./releaseCatalogCompiler.mjs";
5
+
6
+ //#region src/releaseCatalogMutation.d.ts
7
+ interface ReleaseCatalogScope {
8
+ readonly channelId: string;
9
+ readonly channelName: string;
10
+ readonly platform: "ios" | "android";
11
+ readonly scopeKey: string;
12
+ readonly strategy: "APP_VERSION" | "FINGERPRINT";
13
+ readonly fingerprintHash: string | null;
14
+ }
15
+ type ReleaseCatalogMutation = {
16
+ readonly operation: "insert";
17
+ readonly row: ReleaseRow;
18
+ } | {
19
+ readonly operation: "update";
20
+ readonly id: string;
21
+ readonly update: Omit<ReleaseRowUpdate, "revision" | "scope_key">;
22
+ } | {
23
+ readonly operation: "delete";
24
+ readonly id: string;
25
+ };
26
+ interface ReleaseCatalogMutationResult {
27
+ readonly attempts: number;
28
+ readonly catalog: ReleaseCatalogRow;
29
+ readonly release: ReleaseRow | null;
30
+ }
31
+ declare class ReleaseCatalogMutationError extends Error {
32
+ readonly code: "CATALOG_GENERATION_EXHAUSTED" | "CATALOG_IDENTITY_MISSING" | "INVALID_SCOPE" | "NON_MONOTONIC_RELEASE_ID" | "RELEASE_NOT_FOUND" | "VERSION_CONFLICT";
33
+ readonly name = "ReleaseCatalogMutationError";
34
+ constructor(code: "CATALOG_GENERATION_EXHAUSTED" | "CATALOG_IDENTITY_MISSING" | "INVALID_SCOPE" | "NON_MONOTONIC_RELEASE_ID" | "RELEASE_NOT_FOUND" | "VERSION_CONFLICT", message: string);
35
+ }
36
+ interface ReleaseCatalogMutationInput {
37
+ readonly scope: ReleaseCatalogScope;
38
+ readonly mutation: ReleaseCatalogMutation;
39
+ readonly companionChanges?: readonly DatabaseChange[];
40
+ readonly trailingChanges?: readonly DatabaseChange[];
41
+ readonly updatedAtMs?: number;
42
+ }
43
+ interface ReleaseCatalogMutationPreflight {
44
+ readonly catalog: ReleaseCatalogRow;
45
+ readonly currentCatalog: ReleaseCatalogRow | null;
46
+ readonly diagnostics: ReleaseCatalogCompilation["diagnostics"];
47
+ readonly expectedReleaseRevision: number | null;
48
+ readonly release: ReleaseRow | null;
49
+ }
50
+ declare function preflightReleaseCatalogMutation(input: ReleaseCatalogMutationInput & {
51
+ readonly database: BundleRepository;
52
+ }): Promise<ReleaseCatalogMutationPreflight>;
53
+ declare function commitReleaseCatalogMutations(input: {
54
+ readonly database: BundleRepository;
55
+ readonly mutations: readonly ReleaseCatalogMutationInput[];
56
+ readonly maxAttempts?: number;
57
+ }): Promise<readonly ReleaseCatalogMutationResult[]>;
58
+ declare function commitReleaseCatalogMutation(input: ReleaseCatalogMutationInput & {
59
+ readonly database: BundleRepository;
60
+ readonly maxAttempts?: number;
61
+ }): Promise<ReleaseCatalogMutationResult>;
62
+ interface ReleaseCatalogRebuildResult {
63
+ readonly attempts: number;
64
+ readonly catalog: ReleaseCatalogRow;
65
+ readonly changed: boolean;
66
+ readonly diagnostics: ReleaseCatalogCompilation["diagnostics"];
67
+ }
68
+ interface ReleaseCatalogRebuildPreflight {
69
+ readonly changed: boolean;
70
+ readonly currentCatalog: ReleaseCatalogRow | null;
71
+ readonly diagnostics: ReleaseCatalogCompilation["diagnostics"];
72
+ readonly projectedCatalog: ReleaseCatalogRow;
73
+ }
74
+ declare function preflightReleaseCatalogRebuild(input: {
75
+ readonly database: BundleRepository;
76
+ readonly scope: ReleaseCatalogScope;
77
+ readonly updatedAtMs?: number;
78
+ }): Promise<ReleaseCatalogRebuildPreflight>;
79
+ declare function rebuildReleaseCatalog(input: {
80
+ readonly database: BundleRepository;
81
+ readonly maxAttempts?: number;
82
+ readonly scope: ReleaseCatalogScope;
83
+ readonly updatedAtMs?: number;
84
+ }): Promise<ReleaseCatalogRebuildResult>;
85
+ //#endregion
86
+ export { ReleaseCatalogMutation, ReleaseCatalogMutationError, ReleaseCatalogMutationInput, ReleaseCatalogMutationPreflight, ReleaseCatalogMutationResult, ReleaseCatalogRebuildPreflight, ReleaseCatalogRebuildResult, ReleaseCatalogScope, commitReleaseCatalogMutation, commitReleaseCatalogMutations, preflightReleaseCatalogMutation, preflightReleaseCatalogRebuild, rebuildReleaseCatalog };
@@ -0,0 +1,297 @@
1
+ import { createUUIDv7 } from "./uuidv7.mjs";
2
+ import { releaseRowToRelease } from "./databaseRows.mjs";
3
+ import { compileReleaseCatalog } from "./releaseCatalogCompiler.mjs";
4
+ import { createReleaseCatalogScopeKey, encodeChannelKey } from "@hot-updater/core";
5
+ //#region src/releaseCatalogMutation.ts
6
+ const RELEASE_PAGE_SIZE = 1e3;
7
+ const DEFAULT_MAX_ATTEMPTS = 3;
8
+ var ReleaseCatalogMutationError = class extends Error {
9
+ name = "ReleaseCatalogMutationError";
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.code = code;
13
+ }
14
+ };
15
+ const readScopeReleases = async (database, scopeKey) => {
16
+ const releases = [];
17
+ let afterReleaseId;
18
+ for (;;) {
19
+ const page = await database.models.releases.findManyByScope({
20
+ scopeKey,
21
+ ...afterReleaseId === void 0 ? {} : { afterReleaseId },
22
+ limit: RELEASE_PAGE_SIZE,
23
+ consistency: "strong"
24
+ });
25
+ for (const row of page) {
26
+ if (row.scope_key !== scopeKey) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${row.id}" was returned for the wrong catalog scope`);
27
+ if (releases.at(-1)?.id === row.id) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${row.id}" was returned more than once`);
28
+ releases.push(row);
29
+ }
30
+ if (page.length < RELEASE_PAGE_SIZE) return releases;
31
+ const next = page.at(-1)?.id;
32
+ if (next === void 0 || next === afterReleaseId) throw new ReleaseCatalogMutationError("INVALID_SCOPE", "Release scope paging did not advance");
33
+ afterReleaseId = next;
34
+ }
35
+ };
36
+ const validateScope = (scope) => {
37
+ const channelKey = encodeChannelKey(scope.channelName);
38
+ if (scope.strategy === "FINGERPRINT" !== (scope.fingerprintHash !== null)) throw new ReleaseCatalogMutationError("INVALID_SCOPE", "Release catalog strategy and fingerprint are inconsistent");
39
+ const expectedScopeKey = scope.strategy === "APP_VERSION" ? createReleaseCatalogScopeKey({
40
+ channelKey,
41
+ platform: scope.platform,
42
+ strategy: "APP_VERSION"
43
+ }) : createReleaseCatalogScopeKey({
44
+ channelKey,
45
+ fingerprintHash: scope.fingerprintHash ?? "",
46
+ platform: scope.platform,
47
+ strategy: "FINGERPRINT"
48
+ });
49
+ if (scope.scopeKey !== expectedScopeKey) throw new ReleaseCatalogMutationError("INVALID_SCOPE", "Release catalog scope metadata is inconsistent");
50
+ };
51
+ const applyMutation = (rows, scope, mutation) => {
52
+ const currentIndex = mutation.operation === "insert" ? -1 : rows.findIndex(({ id }) => id === mutation.id);
53
+ const current = currentIndex < 0 ? void 0 : rows[currentIndex];
54
+ if (mutation.operation === "insert") {
55
+ const latestId = rows.at(-1)?.id;
56
+ 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");
57
+ if (latestId !== void 0 && mutation.row.id <= latestId) throw new ReleaseCatalogMutationError("NON_MONOTONIC_RELEASE_ID", `Release "${mutation.row.id}" must sort after "${latestId}"`);
58
+ return {
59
+ change: {
60
+ model: "releases",
61
+ operation: "insert",
62
+ row: mutation.row
63
+ },
64
+ expectedRevision: null,
65
+ nextRows: [...rows, mutation.row],
66
+ release: mutation.row
67
+ };
68
+ }
69
+ if (current === void 0) throw new ReleaseCatalogMutationError("RELEASE_NOT_FOUND", `Release "${mutation.id}" was not found in scope "${scope.scopeKey}"`);
70
+ if (mutation.operation === "delete") return {
71
+ change: {
72
+ model: "releases",
73
+ operation: "delete",
74
+ where: { id: mutation.id }
75
+ },
76
+ expectedRevision: current.revision,
77
+ nextRows: rows.filter(({ id }) => id !== mutation.id),
78
+ release: null
79
+ };
80
+ const release = {
81
+ ...current,
82
+ ...mutation.update,
83
+ revision: current.revision + 1
84
+ };
85
+ return {
86
+ change: {
87
+ model: "releases",
88
+ operation: "update",
89
+ where: { id: mutation.id },
90
+ update: {
91
+ ...mutation.update,
92
+ revision: release.revision
93
+ }
94
+ },
95
+ expectedRevision: current.revision,
96
+ nextRows: rows.map((row) => row.id === release.id ? release : row),
97
+ release
98
+ };
99
+ };
100
+ const catalogIdentity = (current, rows) => {
101
+ if (current !== null) return current.catalog_id;
102
+ if (rows.length > 0) throw new ReleaseCatalogMutationError("CATALOG_IDENTITY_MISSING", "Catalog history is missing. Restore its catalog row from backup before rebuilding or deploying.");
103
+ return createUUIDv7();
104
+ };
105
+ const readCatalogScope = async (database, scopeKey) => {
106
+ let currentCatalog = await database.models.releaseCatalogs.findByScopeKey(scopeKey);
107
+ let rows = await readScopeReleases(database, scopeKey);
108
+ if (currentCatalog === null && rows.length > 0) {
109
+ currentCatalog = await database.models.releaseCatalogs.findByScopeKey(scopeKey);
110
+ if (currentCatalog !== null) rows = await readScopeReleases(database, scopeKey);
111
+ }
112
+ return {
113
+ currentCatalog,
114
+ rows
115
+ };
116
+ };
117
+ const prepareReleaseCatalogMutation = async (database, mutationInput) => {
118
+ const { rows, currentCatalog } = await readCatalogScope(database, mutationInput.scope.scopeKey);
119
+ const applied = applyMutation(rows, mutationInput.scope, mutationInput.mutation);
120
+ const compilation = await compileReleaseCatalog({
121
+ strategy: mutationInput.scope.strategy,
122
+ releases: applied.nextRows.map(releaseRowToRelease)
123
+ });
124
+ const generation = (currentCatalog?.generation ?? 0) + 1;
125
+ if (!Number.isSafeInteger(generation)) throw new ReleaseCatalogMutationError("CATALOG_GENERATION_EXHAUSTED", `Catalog "${mutationInput.scope.scopeKey}" exhausted its generation counter`);
126
+ return {
127
+ applied,
128
+ catalog: {
129
+ scope_key: mutationInput.scope.scopeKey,
130
+ catalog_id: catalogIdentity(currentCatalog, rows),
131
+ strategy: mutationInput.scope.strategy,
132
+ channel_id: mutationInput.scope.channelId,
133
+ channel_key: encodeChannelKey(mutationInput.scope.channelName),
134
+ platform: mutationInput.scope.platform,
135
+ fingerprint_hash: mutationInput.scope.fingerprintHash,
136
+ generation,
137
+ payload: compilation.canonicalPayload,
138
+ catalog_hash: compilation.catalogHash,
139
+ byte_size: compilation.byteSize,
140
+ is_tombstone: compilation.payload.releaseDescriptors.length === 0,
141
+ updated_at_ms: mutationInput.updatedAtMs ?? Date.now()
142
+ },
143
+ compilation,
144
+ currentCatalog,
145
+ mutationInput
146
+ };
147
+ };
148
+ async function preflightReleaseCatalogMutation(input) {
149
+ validateScope(input.scope);
150
+ const prepared = await prepareReleaseCatalogMutation(input.database, input);
151
+ return {
152
+ catalog: prepared.catalog,
153
+ currentCatalog: prepared.currentCatalog,
154
+ diagnostics: prepared.compilation.diagnostics,
155
+ expectedReleaseRevision: prepared.applied.expectedRevision,
156
+ release: prepared.applied.release
157
+ };
158
+ }
159
+ async function commitReleaseCatalogMutations(input) {
160
+ if (input.mutations.length === 0) return [];
161
+ const scopeKeys = /* @__PURE__ */ new Set();
162
+ for (const mutation of input.mutations) {
163
+ validateScope(mutation.scope);
164
+ if (scopeKeys.has(mutation.scope.scopeKey)) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `A batch cannot mutate catalog scope "${mutation.scope.scopeKey}" more than once`);
165
+ scopeKeys.add(mutation.scope.scopeKey);
166
+ }
167
+ const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
168
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) throw new RangeError("maxAttempts must be a positive safe integer");
169
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
170
+ const prepared = await Promise.all(input.mutations.map((mutationInput) => prepareReleaseCatalogMutation(input.database, mutationInput)));
171
+ const result = await input.database.commit({
172
+ expectations: prepared.flatMap(({ applied, currentCatalog, mutationInput }) => [{
173
+ model: "releases",
174
+ id: mutationInput.mutation.operation === "insert" ? mutationInput.mutation.row.id : mutationInput.mutation.id,
175
+ revision: applied.expectedRevision
176
+ }, {
177
+ model: "releaseCatalogs",
178
+ scopeKey: mutationInput.scope.scopeKey,
179
+ generation: currentCatalog?.generation ?? null
180
+ }]),
181
+ changes: prepared.flatMap(({ applied, catalog, mutationInput }) => [
182
+ ...mutationInput.companionChanges ?? [],
183
+ applied.change,
184
+ {
185
+ model: "releaseCatalogs",
186
+ operation: "put",
187
+ row: catalog
188
+ },
189
+ ...mutationInput.trailingChanges ?? []
190
+ ])
191
+ });
192
+ if (result.committed) return prepared.map(({ applied, catalog }) => ({
193
+ attempts: attempt,
194
+ catalog,
195
+ release: applied.release
196
+ }));
197
+ if (result.conflict.reason !== "version_conflict") throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release mutation failed: ${result.conflict.reason}`);
198
+ }
199
+ throw new ReleaseCatalogMutationError("VERSION_CONFLICT", `Release catalog mutation batch conflicted ${maxAttempts} times`);
200
+ }
201
+ async function commitReleaseCatalogMutation(input) {
202
+ const [result] = await commitReleaseCatalogMutations({
203
+ database: input.database,
204
+ maxAttempts: input.maxAttempts,
205
+ mutations: [input]
206
+ });
207
+ return result;
208
+ }
209
+ const catalogProjectionMatches = (current, scope, compilation) => current.strategy === scope.strategy && current.channel_id === scope.channelId && current.channel_key === 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);
210
+ async function preflightReleaseCatalogRebuild(input) {
211
+ validateScope(input.scope);
212
+ const { rows, currentCatalog } = await readCatalogScope(input.database, input.scope.scopeKey);
213
+ const compilation = await compileReleaseCatalog({
214
+ releases: rows.map(releaseRowToRelease),
215
+ strategy: input.scope.strategy
216
+ });
217
+ const changed = currentCatalog === null || !catalogProjectionMatches(currentCatalog, input.scope, compilation);
218
+ const generation = changed ? (currentCatalog?.generation ?? 0) + 1 : currentCatalog.generation;
219
+ if (!Number.isSafeInteger(generation)) throw new ReleaseCatalogMutationError("CATALOG_GENERATION_EXHAUSTED", `Catalog "${input.scope.scopeKey}" exhausted its generation counter`);
220
+ return {
221
+ changed,
222
+ currentCatalog,
223
+ diagnostics: compilation.diagnostics,
224
+ projectedCatalog: {
225
+ catalog_id: catalogIdentity(currentCatalog, rows),
226
+ byte_size: compilation.byteSize,
227
+ catalog_hash: compilation.catalogHash,
228
+ channel_id: input.scope.channelId,
229
+ channel_key: encodeChannelKey(input.scope.channelName),
230
+ fingerprint_hash: input.scope.fingerprintHash,
231
+ generation,
232
+ is_tombstone: compilation.payload.releaseDescriptors.length === 0,
233
+ payload: compilation.canonicalPayload,
234
+ platform: input.scope.platform,
235
+ scope_key: input.scope.scopeKey,
236
+ strategy: input.scope.strategy,
237
+ updated_at_ms: changed || currentCatalog === null ? input.updatedAtMs ?? Date.now() : currentCatalog.updated_at_ms
238
+ }
239
+ };
240
+ }
241
+ async function rebuildReleaseCatalog(input) {
242
+ validateScope(input.scope);
243
+ const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
244
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts <= 0) throw new RangeError("maxAttempts must be a positive safe integer");
245
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
246
+ const { rows, currentCatalog } = await readCatalogScope(input.database, input.scope.scopeKey);
247
+ const compilation = await compileReleaseCatalog({
248
+ releases: rows.map(releaseRowToRelease),
249
+ strategy: input.scope.strategy
250
+ });
251
+ if (currentCatalog !== null && catalogProjectionMatches(currentCatalog, input.scope, compilation)) return {
252
+ attempts: attempt,
253
+ catalog: currentCatalog,
254
+ changed: false,
255
+ diagnostics: compilation.diagnostics
256
+ };
257
+ const generation = (currentCatalog?.generation ?? 0) + 1;
258
+ if (!Number.isSafeInteger(generation)) throw new ReleaseCatalogMutationError("CATALOG_GENERATION_EXHAUSTED", `Catalog "${input.scope.scopeKey}" exhausted its generation counter`);
259
+ const catalog = {
260
+ catalog_id: catalogIdentity(currentCatalog, rows),
261
+ byte_size: compilation.byteSize,
262
+ catalog_hash: compilation.catalogHash,
263
+ channel_id: input.scope.channelId,
264
+ channel_key: encodeChannelKey(input.scope.channelName),
265
+ fingerprint_hash: input.scope.fingerprintHash,
266
+ generation,
267
+ is_tombstone: compilation.payload.releaseDescriptors.length === 0,
268
+ payload: compilation.canonicalPayload,
269
+ platform: input.scope.platform,
270
+ scope_key: input.scope.scopeKey,
271
+ strategy: input.scope.strategy,
272
+ updated_at_ms: input.updatedAtMs ?? Date.now()
273
+ };
274
+ const result = await input.database.commit({
275
+ changes: [{
276
+ model: "releaseCatalogs",
277
+ operation: "put",
278
+ row: catalog
279
+ }],
280
+ expectations: [{
281
+ generation: currentCatalog?.generation ?? null,
282
+ model: "releaseCatalogs",
283
+ scopeKey: input.scope.scopeKey
284
+ }]
285
+ });
286
+ if (result.committed) return {
287
+ attempts: attempt,
288
+ catalog,
289
+ changed: true,
290
+ diagnostics: compilation.diagnostics
291
+ };
292
+ if (result.conflict.reason !== "version_conflict") throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release catalog rebuild failed: ${result.conflict.reason}`);
293
+ }
294
+ throw new ReleaseCatalogMutationError("VERSION_CONFLICT", `Release catalog rebuild conflicted ${maxAttempts} times`);
295
+ }
296
+ //#endregion
297
+ export { ReleaseCatalogMutationError, commitReleaseCatalogMutation, commitReleaseCatalogMutations, preflightReleaseCatalogMutation, preflightReleaseCatalogRebuild, rebuildReleaseCatalog };