@hot-updater/server 0.36.6 → 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 (291) hide show
  1. package/dist/adapters/databaseConstraintErrors.cjs +23 -0
  2. package/dist/adapters/databaseConstraintErrors.mjs +21 -0
  3. package/dist/adapters/databasePluginUtils.cjs +117 -0
  4. package/dist/adapters/databasePluginUtils.mjs +106 -0
  5. package/dist/adapters/drizzle.cjs +53 -176
  6. package/dist/adapters/drizzle.d.cts +6 -4
  7. package/dist/adapters/drizzle.d.mts +6 -4
  8. package/dist/adapters/drizzle.mjs +54 -177
  9. package/dist/adapters/drizzleCrud.cjs +250 -0
  10. package/dist/adapters/drizzleCrud.mjs +249 -0
  11. package/dist/adapters/drizzleLazyDB.cjs +84 -36
  12. package/dist/adapters/drizzleLazyDB.mjs +84 -36
  13. package/dist/adapters/drizzleQuery.cjs +68 -0
  14. package/dist/adapters/drizzleQuery.mjs +67 -0
  15. package/dist/adapters/kysely.cjs +18 -138
  16. package/dist/adapters/kysely.d.cts +3 -12
  17. package/dist/adapters/kysely.d.mts +3 -12
  18. package/dist/adapters/kysely.mjs +19 -139
  19. package/dist/adapters/kyselyCrud.cjs +254 -0
  20. package/dist/adapters/kyselyCrud.mjs +253 -0
  21. package/dist/adapters/kyselyQuery.cjs +56 -0
  22. package/dist/adapters/kyselyQuery.mjs +55 -0
  23. package/dist/adapters/mongodb.cjs +21 -174
  24. package/dist/adapters/mongodb.d.cts +3 -2
  25. package/dist/adapters/mongodb.d.mts +3 -2
  26. package/dist/adapters/mongodb.mjs +21 -174
  27. package/dist/adapters/mongodbCollections.cjs +36 -0
  28. package/dist/adapters/mongodbCollections.mjs +30 -0
  29. package/dist/adapters/mongodbQuery.cjs +77 -0
  30. package/dist/adapters/mongodbQuery.mjs +70 -0
  31. package/dist/adapters/mongodbReads.cjs +148 -0
  32. package/dist/adapters/mongodbReads.mjs +147 -0
  33. package/dist/adapters/mongodbWrites.cjs +134 -0
  34. package/dist/adapters/mongodbWrites.mjs +133 -0
  35. package/dist/adapters/prisma.cjs +205 -180
  36. package/dist/adapters/prisma.d.cts +3 -3
  37. package/dist/adapters/prisma.d.mts +3 -3
  38. package/dist/adapters/prisma.mjs +206 -181
  39. package/dist/adapters/prismaQuery.cjs +58 -0
  40. package/dist/adapters/prismaQuery.mjs +56 -0
  41. package/dist/adapters/prismaRows.cjs +198 -0
  42. package/dist/adapters/prismaRows.mjs +188 -0
  43. package/dist/analytics/bounded/activeOverview.cjs +87 -0
  44. package/dist/analytics/bounded/activeOverview.mjs +85 -0
  45. package/dist/analytics/bounded/installationSearch.cjs +42 -0
  46. package/dist/analytics/bounded/installationSearch.mjs +42 -0
  47. package/dist/analytics/bounded/persistence.cjs +39 -0
  48. package/dist/analytics/bounded/persistence.mjs +38 -0
  49. package/dist/analytics/bounded/provider.cjs +170 -0
  50. package/dist/analytics/bounded/provider.d.cts +7 -0
  51. package/dist/analytics/bounded/provider.d.mts +7 -0
  52. package/dist/analytics/bounded/provider.mjs +170 -0
  53. package/dist/analytics/bounded/scan.cjs +144 -0
  54. package/dist/analytics/bounded/scan.mjs +138 -0
  55. package/dist/analytics/domain.d.cts +121 -0
  56. package/dist/analytics/domain.d.mts +121 -0
  57. package/dist/analytics/errors.cjs +22 -0
  58. package/dist/analytics/errors.mjs +20 -0
  59. package/dist/analytics/eventInput.cjs +111 -0
  60. package/dist/analytics/eventInput.mjs +111 -0
  61. package/dist/analytics/persistence.d.cts +6 -0
  62. package/dist/analytics/persistence.d.mts +6 -0
  63. package/dist/analytics/queryInput.cjs +58 -0
  64. package/dist/analytics/queryInput.mjs +55 -0
  65. package/dist/analytics/routes.cjs +63 -0
  66. package/dist/analytics/routes.mjs +61 -0
  67. package/dist/analytics/types.d.cts +17 -0
  68. package/dist/analytics/types.d.mts +17 -0
  69. package/dist/apiKeys.cjs +139 -0
  70. package/dist/apiKeys.d.cts +50 -0
  71. package/dist/apiKeys.d.mts +50 -0
  72. package/dist/apiKeys.mjs +133 -0
  73. package/dist/createHotUpdaterCore.cjs +48 -26
  74. package/dist/createHotUpdaterCore.d.cts +49 -17
  75. package/dist/createHotUpdaterCore.d.mts +49 -17
  76. package/dist/createHotUpdaterCore.mjs +51 -29
  77. package/dist/db/createBundleDiff.cjs +45 -68
  78. package/dist/db/createBundleDiff.d.cts +3 -3
  79. package/dist/db/createBundleDiff.d.mts +3 -3
  80. package/dist/db/createBundleDiff.mjs +47 -68
  81. package/dist/db/databasePluginCore.cjs +138 -0
  82. package/dist/db/databasePluginCore.mjs +137 -0
  83. package/dist/db/fixedMigrator.cjs +36 -93
  84. package/dist/db/fixedMigrator.mjs +36 -93
  85. package/dist/db/fixedMigratorMongo.cjs +319 -0
  86. package/dist/db/fixedMigratorMongo.mjs +319 -0
  87. package/dist/db/fixedMigratorShared.cjs +23 -0
  88. package/dist/db/fixedMigratorShared.mjs +18 -0
  89. package/dist/db/mongoMigrationExecution.cjs +11 -0
  90. package/dist/db/mongoMigrationExecution.mjs +11 -0
  91. package/dist/db/releaseCatalog.cjs +66 -0
  92. package/dist/db/releaseCatalog.d.cts +17 -0
  93. package/dist/db/releaseCatalog.d.mts +17 -0
  94. package/dist/db/releaseCatalog.mjs +64 -0
  95. package/dist/db/schema/mongodb.cjs +14 -6
  96. package/dist/db/schema/mongodb.mjs +14 -6
  97. package/dist/db/schema/registry.cjs +0 -2
  98. package/dist/db/schema/registry.mjs +1 -2
  99. package/dist/db/schema/sql.cjs +31 -14
  100. package/dist/db/schema/sql.mjs +32 -13
  101. package/dist/db/schema/sqlOperations.cjs +4 -4
  102. package/dist/db/schema/sqlOperations.mjs +5 -5
  103. package/dist/db/schemaGenerators.cjs +52 -18
  104. package/dist/db/schemaGenerators.mjs +52 -18
  105. package/dist/db/schemaReadiness.cjs +4 -3
  106. package/dist/db/schemaReadiness.mjs +4 -3
  107. package/dist/db/sqlMigrationExecution.cjs +67 -0
  108. package/dist/db/sqlMigrationExecution.mjs +66 -0
  109. package/dist/db/types.cjs +2 -6
  110. package/dist/db/types.d.cts +60 -16
  111. package/dist/db/types.d.mts +60 -16
  112. package/dist/db/types.mjs +3 -6
  113. package/dist/db/updateArtifacts.cjs +114 -60
  114. package/dist/db/updateArtifacts.mjs +117 -62
  115. package/dist/handler.cjs +108 -282
  116. package/dist/handler.d.cts +3 -46
  117. package/dist/handler.d.mts +3 -46
  118. package/dist/handler.mjs +107 -281
  119. package/dist/handlerBundleRoutes.cjs +155 -0
  120. package/dist/handlerBundleRoutes.mjs +155 -0
  121. package/dist/handlerErrors.cjs +9 -0
  122. package/dist/handlerErrors.mjs +9 -0
  123. package/dist/handlerParameters.cjs +39 -0
  124. package/dist/handlerParameters.mjs +34 -0
  125. package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
  126. package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
  127. package/dist/handlerReleaseManagementRoutes.cjs +125 -0
  128. package/dist/handlerReleaseManagementRoutes.mjs +124 -0
  129. package/dist/handlerTypes.d.cts +63 -0
  130. package/dist/handlerTypes.d.mts +63 -0
  131. package/dist/handlerVersionRoutes.cjs +10 -0
  132. package/dist/handlerVersionRoutes.d.cts +4 -0
  133. package/dist/handlerVersionRoutes.d.mts +4 -0
  134. package/dist/handlerVersionRoutes.mjs +9 -0
  135. package/dist/index.cjs +10 -1
  136. package/dist/index.d.cts +9 -3
  137. package/dist/index.d.mts +9 -3
  138. package/dist/index.mjs +5 -2
  139. package/dist/node.cjs +19 -12
  140. package/dist/node.d.cts +15 -10
  141. package/dist/node.d.mts +15 -10
  142. package/dist/node.mjs +19 -13
  143. package/dist/package.cjs +1 -1
  144. package/dist/package.mjs +1 -1
  145. package/dist/schema/dsl.cjs +19 -2
  146. package/dist/schema/dsl.mjs +17 -3
  147. package/dist/schema/index.cjs +1 -8
  148. package/dist/schema/index.mjs +2 -8
  149. package/dist/schema/types.cjs +3 -1
  150. package/dist/schema/types.mjs +3 -2
  151. package/dist/schema/v1_0_0.cjs +364 -0
  152. package/dist/schema/v1_0_0.mjs +357 -0
  153. package/dist/storageAccess.cjs +69 -25
  154. package/dist/storageAccess.mjs +68 -25
  155. package/dist/types/index.d.cts +4 -3
  156. package/dist/types/index.d.mts +5 -4
  157. package/package.json +8 -7
  158. package/src/adapters/databaseConstraintErrors.ts +25 -0
  159. package/src/adapters/databasePluginTestDatabase.ts +106 -0
  160. package/src/adapters/databasePluginUtils.spec.ts +29 -0
  161. package/src/adapters/databasePluginUtils.ts +214 -0
  162. package/src/adapters/drizzle.spec.ts +208 -47
  163. package/src/adapters/drizzle.ts +106 -293
  164. package/src/adapters/drizzleCrud.ts +419 -0
  165. package/src/adapters/drizzleLazyDB.ts +206 -100
  166. package/src/adapters/drizzleQuery.ts +164 -0
  167. package/src/adapters/kysely.spec.ts +126 -88
  168. package/src/adapters/kysely.ts +76 -334
  169. package/src/adapters/kyselyCrud.ts +588 -0
  170. package/src/adapters/kyselyQuery.ts +128 -0
  171. package/src/adapters/mongodb.spec.ts +141 -0
  172. package/src/adapters/mongodb.ts +60 -288
  173. package/src/adapters/mongodbCollections.ts +62 -0
  174. package/src/adapters/mongodbQuery.ts +226 -0
  175. package/src/adapters/mongodbReads.ts +311 -0
  176. package/src/adapters/mongodbRows.spec.ts +64 -0
  177. package/src/adapters/mongodbRows.ts +118 -0
  178. package/src/adapters/mongodbTestClient.ts +342 -0
  179. package/src/adapters/mongodbTestFilter.ts +166 -0
  180. package/src/adapters/mongodbWrites.ts +260 -0
  181. package/src/adapters/prisma.spec.ts +196 -0
  182. package/src/adapters/prisma.ts +417 -292
  183. package/src/adapters/prismaQuery.ts +129 -0
  184. package/src/adapters/prismaRows.spec.ts +98 -0
  185. package/src/adapters/prismaRows.ts +301 -0
  186. package/src/adapters/prismaTestClient.ts +396 -0
  187. package/src/analytics/analytics.integration.spec.ts +144 -0
  188. package/src/analytics/bounded/activeOverview.spec.ts +189 -0
  189. package/src/analytics/bounded/activeOverview.ts +148 -0
  190. package/src/analytics/bounded/installationSearch.spec.ts +130 -0
  191. package/src/analytics/bounded/installationSearch.ts +69 -0
  192. package/src/analytics/bounded/persistence.ts +43 -0
  193. package/src/analytics/bounded/provider.spec.ts +219 -0
  194. package/src/analytics/bounded/provider.ts +229 -0
  195. package/src/analytics/bounded/scan.ts +235 -0
  196. package/src/analytics/domain.ts +139 -0
  197. package/src/analytics/errors.ts +18 -0
  198. package/src/analytics/eventInput.ts +168 -0
  199. package/src/analytics/persistence.ts +10 -0
  200. package/src/analytics/queryInput.ts +102 -0
  201. package/src/analytics/routes.ts +123 -0
  202. package/src/analytics/types.ts +44 -0
  203. package/src/apiKeys.integration.spec.ts +192 -0
  204. package/src/apiKeys.spec.ts +162 -0
  205. package/src/apiKeys.ts +220 -0
  206. package/src/createHotUpdaterCore.ts +164 -100
  207. package/src/db/bundleRows.spec.ts +164 -0
  208. package/src/db/bundleRows.ts +10 -140
  209. package/src/db/createBundleDiff.spec.ts +172 -127
  210. package/src/db/createBundleDiff.ts +91 -110
  211. package/src/db/databasePluginCore.spec.ts +62 -0
  212. package/src/db/databasePluginCore.testFixtures.ts +76 -0
  213. package/src/db/databasePluginCore.ts +185 -0
  214. package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
  215. package/src/db/fixedMigrator.spec.ts +184 -42
  216. package/src/db/fixedMigrator.ts +64 -205
  217. package/src/db/fixedMigratorMongo.spec.ts +192 -0
  218. package/src/db/fixedMigratorMongo.ts +425 -0
  219. package/src/db/fixedMigratorShared.ts +36 -0
  220. package/src/db/hotUpdaterSchema.ts +0 -1
  221. package/src/db/index.spec.ts +333 -1025
  222. package/src/db/mongoMigrationExecution.ts +23 -0
  223. package/src/db/node-sqlite.d.ts +25 -0
  224. package/src/db/releaseCatalog.ts +148 -0
  225. package/src/db/schema/mongodb.ts +20 -6
  226. package/src/db/schema/sql.ts +52 -11
  227. package/src/db/schema/sqlOperations.ts +5 -4
  228. package/src/db/schemaGenerators.ts +107 -23
  229. package/src/db/schemaReadiness.ts +2 -1
  230. package/src/db/sqlMigrationExecution.spec.ts +65 -0
  231. package/src/db/sqlMigrationExecution.ts +108 -0
  232. package/src/db/types.spec.ts +55 -0
  233. package/src/db/types.ts +129 -65
  234. package/src/db/updateArtifacts.spec.ts +359 -0
  235. package/src/db/updateArtifacts.ts +290 -126
  236. package/src/handler-standalone.integration.spec.ts +401 -364
  237. package/src/handler.spec.ts +137 -582
  238. package/src/handler.testFixtures.ts +30 -0
  239. package/src/handler.ts +218 -616
  240. package/src/handlerAuth.integration.spec.ts +33 -0
  241. package/src/handlerBundleRoutes.ts +259 -0
  242. package/src/handlerErrors.ts +7 -0
  243. package/src/handlerManagement.spec.ts +262 -0
  244. package/src/handlerParameters.ts +105 -0
  245. package/src/handlerReleaseCatalogRoutes.ts +154 -0
  246. package/src/handlerReleaseManagementRoutes.ts +213 -0
  247. package/src/handlerTypes.ts +96 -0
  248. package/src/handlerVersionRoutes.ts +12 -0
  249. package/src/index.ts +22 -2
  250. package/src/node.spec.ts +79 -10
  251. package/src/node.ts +20 -17
  252. package/src/releaseCatalogRoutes.integration.spec.ts +238 -0
  253. package/src/runtime.spec.ts +126 -959
  254. package/src/runtime.testFixtures.ts +71 -0
  255. package/src/schema/dsl-all-versions.spec.ts +26 -17
  256. package/src/schema/dsl.ts +41 -2
  257. package/src/schema/index.ts +3 -9
  258. package/src/schema/types.ts +9 -3
  259. package/src/schema/v1_0_0.spec.ts +146 -0
  260. package/src/schema/v1_0_0.ts +465 -0
  261. package/src/storageAccess.spec.ts +127 -32
  262. package/src/storageAccess.ts +106 -55
  263. package/src/types/index.ts +2 -1
  264. package/dist/db/bundleRows.cjs +0 -82
  265. package/dist/db/bundleRows.d.cts +0 -33
  266. package/dist/db/bundleRows.d.mts +0 -33
  267. package/dist/db/bundleRows.mjs +0 -79
  268. package/dist/db/pluginCore.cjs +0 -242
  269. package/dist/db/pluginCore.mjs +0 -241
  270. package/dist/db/schema/sqlMigrations.cjs +0 -74
  271. package/dist/db/schema/sqlMigrations.mjs +0 -73
  272. package/dist/db/schemaEnhancements.cjs +0 -20
  273. package/dist/db/schemaEnhancements.mjs +0 -19
  274. package/dist/route.cjs +0 -7
  275. package/dist/route.mjs +0 -7
  276. package/dist/schema/v0_21_0.cjs +0 -38
  277. package/dist/schema/v0_21_0.mjs +0 -37
  278. package/dist/schema/v0_29_0.cjs +0 -45
  279. package/dist/schema/v0_29_0.mjs +0 -44
  280. package/dist/schema/v0_31_0.cjs +0 -82
  281. package/dist/schema/v0_31_0.mjs +0 -80
  282. package/src/db/pluginCore.spec.ts +0 -1095
  283. package/src/db/pluginCore.ts +0 -475
  284. package/src/db/pluginUpdateCheck.bench.ts +0 -254
  285. package/src/db/schema/sqlMigrations.ts +0 -219
  286. package/src/db/schemaEnhancements.ts +0 -55
  287. package/src/route.ts +0 -7
  288. package/src/schema/v0_21_0.ts +0 -52
  289. package/src/schema/v0_29_0.ts +0 -62
  290. package/src/schema/v0_31_0.spec.ts +0 -73
  291. package/src/schema/v0_31_0.ts +0 -120
@@ -1,6 +1,4 @@
1
- import crypto, { randomUUID } from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
1
+ import crypto from "node:crypto";
4
2
  import path from "node:path";
5
3
  import { promisify } from "node:util";
6
4
  import { brotliDecompress } from "node:zlib";
@@ -14,18 +12,27 @@ import {
14
12
  } from "@hot-updater/core";
15
13
  import type {
16
14
  Bundle,
17
- DatabasePlugin,
18
- NodeStoragePlugin,
15
+ BundleRepository,
16
+ StoragePluginWith,
19
17
  } from "@hot-updater/plugin-core";
20
18
  import {
21
19
  createBundleStorageKey,
20
+ createDatabaseClient,
22
21
  getManifestAssetDownloadPath,
23
22
  resolveManifestAssetStorageUri,
24
23
  } from "@hot-updater/plugin-core";
25
24
 
26
25
  type BundleManifest = {
27
26
  bundleId: string;
28
- assets: Record<string, { fileHash: string; signature?: string }>;
27
+ assets: Record<
28
+ string,
29
+ {
30
+ downloadByteSize?: unknown;
31
+ downloadFileHash?: unknown;
32
+ fileHash: string;
33
+ signature?: string;
34
+ }
35
+ >;
29
36
  };
30
37
 
31
38
  export interface CreateBundleDiffInput {
@@ -34,8 +41,8 @@ export interface CreateBundleDiffInput {
34
41
  }
35
42
 
36
43
  export interface CreateBundleDiffDependencies {
37
- databasePlugin: DatabasePlugin;
38
- storagePlugin: NodeStoragePlugin | null;
44
+ databasePlugin: BundleRepository;
45
+ storagePlugin: StoragePluginWith<"get" | "put" | "delete"> | null;
39
46
  }
40
47
 
41
48
  export interface CreateBundleDiffOptions {
@@ -43,7 +50,6 @@ export interface CreateBundleDiffOptions {
43
50
  }
44
51
 
45
52
  const HBC_ASSET_PATH_RE = /\.bundle$/;
46
- const HOT_UPDATER_DOWNLOAD_DIR_PREFIX = "downloads-";
47
53
  const decompressBrotli = promisify(brotliDecompress);
48
54
 
49
55
  const isBundleManifest = (value: unknown): value is BundleManifest => {
@@ -101,41 +107,30 @@ async function downloadFromUrl(url: string) {
101
107
 
102
108
  async function downloadStorageBytes(
103
109
  storageUri: string,
104
- storagePlugin: NodeStoragePlugin | null,
110
+ storagePlugin: StoragePluginWith<"get" | "put" | "delete"> | null,
105
111
  ) {
106
112
  const protocol = new URL(storageUri).protocol.replace(":", "");
107
113
 
108
- if (protocol === "http" || protocol === "https") {
109
- return downloadFromUrl(storageUri);
110
- }
111
-
112
- if (!storagePlugin) {
113
- throw new Error("Storage plugin is not configured");
114
- }
115
-
116
- if (storagePlugin.supportedProtocol !== protocol) {
114
+ if (!storagePlugin || storagePlugin.protocol !== protocol) {
115
+ if (protocol === "http" || protocol === "https") {
116
+ return downloadFromUrl(storageUri);
117
+ }
118
+ if (!storagePlugin) {
119
+ throw new Error("Storage plugin is not configured");
120
+ }
117
121
  throw new Error(`No storage plugin for protocol: ${protocol}`);
118
122
  }
119
123
 
120
- const downloadRoot = path.join(process.cwd(), ".hot-updater");
121
- await fs.mkdir(downloadRoot, { recursive: true });
122
- const workDir = await fs.mkdtemp(
123
- path.join(downloadRoot, HOT_UPDATER_DOWNLOAD_DIR_PREFIX),
124
- );
125
- const filename = path.basename(new URL(storageUri).pathname) || randomUUID();
126
- const filePath = path.join(workDir, filename);
127
-
128
- try {
129
- await storagePlugin.profiles.node.downloadFile(storageUri, filePath);
130
- return new Uint8Array(await fs.readFile(filePath));
131
- } finally {
132
- await fs.rm(workDir, { force: true, recursive: true });
124
+ const { response } = await storagePlugin.get({ storageUri });
125
+ if (response === null) {
126
+ throw new Error(`Storage object not found: ${storageUri}`);
133
127
  }
128
+ return new Uint8Array(await response.arrayBuffer());
134
129
  }
135
130
 
136
131
  async function fetchManifest(
137
132
  bundle: Bundle,
138
- storagePlugin: NodeStoragePlugin | null,
133
+ storagePlugin: StoragePluginWith<"get" | "put" | "delete"> | null,
139
134
  ): Promise<BundleManifest> {
140
135
  const manifestStorageUri = getManifestStorageUri(bundle);
141
136
  if (!manifestStorageUri) {
@@ -147,9 +142,7 @@ async function fetchManifest(
147
142
  storagePlugin,
148
143
  );
149
144
 
150
- const payload = JSON.parse(
151
- new TextDecoder().decode(manifestBytes),
152
- ) as unknown;
145
+ const payload: unknown = JSON.parse(new TextDecoder().decode(manifestBytes));
153
146
  if (!isBundleManifest(payload)) {
154
147
  throw new Error(`Invalid manifest payload for bundle ${bundle.id}`);
155
148
  }
@@ -178,7 +171,7 @@ async function fetchAssetBytes(
178
171
  bundle: Bundle,
179
172
  assetPath: string,
180
173
  manifest: BundleManifest,
181
- storagePlugin: NodeStoragePlugin | null,
174
+ storagePlugin: StoragePluginWith<"get" | "put" | "delete"> | null,
182
175
  ) {
183
176
  const assetBaseStorageUri = getAssetBaseStorageUri(bundle);
184
177
  if (!assetBaseStorageUri) {
@@ -194,6 +187,7 @@ async function fetchAssetBytes(
194
187
  const compressedAssetStorageUri = resolveManifestAssetStorageUri({
195
188
  assetBaseStorageUri,
196
189
  assetPath: downloadPath,
190
+ downloadFileHash: asset.downloadFileHash,
197
191
  fileHash: asset.fileHash,
198
192
  });
199
193
 
@@ -203,8 +197,9 @@ async function fetchAssetBytes(
203
197
  compressedAssetStorageUri,
204
198
  storagePlugin,
205
199
  );
206
- } catch {
207
- // Older deployments stored manifest assets uncompressed.
200
+ } catch (error) {
201
+ if (!(error instanceof Error)) throw error;
202
+ compressedBytes = null;
208
203
  }
209
204
 
210
205
  if (compressedBytes) {
@@ -220,11 +215,6 @@ async function fetchAssetBytes(
220
215
  return downloadStorageBytes(assetStorageUri, storagePlugin);
221
216
  }
222
217
 
223
- async function getFileHash(filePath: string) {
224
- const file = await fs.readFile(filePath);
225
- return crypto.createHash("sha256").update(file).digest("hex");
226
- }
227
-
228
218
  function buildNextPatchState({
229
219
  currentBundle,
230
220
  nextPatch,
@@ -240,12 +230,8 @@ function buildNextPatchState({
240
230
  const orderedPatches = makePrimary
241
231
  ? [nextPatch, ...existingPatches]
242
232
  : [...existingPatches, nextPatch];
243
- const primaryPatch = orderedPatches[0] ?? nextPatch;
244
233
 
245
- return {
246
- patches: orderedPatches,
247
- primaryPatch,
248
- };
234
+ return orderedPatches;
249
235
  }
250
236
 
251
237
  export async function createBundleDiff(
@@ -253,6 +239,8 @@ export async function createBundleDiff(
253
239
  deps: CreateBundleDiffDependencies,
254
240
  options: CreateBundleDiffOptions = {},
255
241
  ) {
242
+ const database = createDatabaseClient(deps.databasePlugin);
243
+
256
244
  if (!deps.storagePlugin) {
257
245
  throw new Error("Storage plugin is not configured");
258
246
  }
@@ -261,8 +249,8 @@ export async function createBundleDiff(
261
249
  throw new Error("Base bundle must be different from the target bundle");
262
250
  }
263
251
 
264
- const baseBundle = await deps.databasePlugin.getBundleById(baseBundleId);
265
- const targetBundle = await deps.databasePlugin.getBundleById(bundleId);
252
+ const baseBundle = await database.getBundleById(baseBundleId);
253
+ const targetBundle = await database.getBundleById(bundleId);
266
254
 
267
255
  if (!baseBundle || !targetBundle) {
268
256
  throw new Error("Bundle not found");
@@ -315,69 +303,62 @@ export async function createBundleDiff(
315
303
  ]);
316
304
 
317
305
  const patchBytes = await hdiff(baseBytes, targetBytes);
318
- const workDir = await fs.mkdtemp(
319
- path.join(os.tmpdir(), "hot-updater-console-bsdiff-"),
320
- );
306
+ const patchFileHash = crypto
307
+ .createHash("sha256")
308
+ .update(patchBytes)
309
+ .digest("hex");
321
310
  const patchFilename = `${path.posix.basename(targetAssetPath)}.bsdiff`;
322
- const patchPath = path.join(workDir, patchFilename);
323
311
  const previousPatch = getBundlePatch(targetBundle, baseBundle.id);
324
312
 
325
- try {
326
- await fs.writeFile(patchPath, patchBytes);
327
-
328
- const uploadKey = createBundleStorageKey(
329
- targetBundle.id,
330
- "patches",
331
- baseBundle.id,
332
- getRelativeStorageDir(targetAssetPath),
333
- );
334
- const patchUpload = await deps.storagePlugin.profiles.node.upload(
335
- uploadKey,
336
- patchPath,
337
- );
338
- const patchFileHash = await getFileHash(patchPath);
339
-
340
- const nextPatch = {
341
- baseBundleId: baseBundle.id,
342
- baseFileHash: baseAssetHash,
343
- patchFileHash,
344
- patchStorageUri: patchUpload.storageUri,
345
- };
346
- const nextState = buildNextPatchState({
347
- currentBundle: targetBundle,
348
- nextPatch,
349
- makePrimary: options.makePrimary ?? true,
350
- });
351
-
352
- await deps.databasePlugin.updateBundle(targetBundle.id, {
353
- patches: nextState.patches,
354
- patchBaseBundleId: nextState.primaryPatch.baseBundleId,
355
- patchBaseFileHash: nextState.primaryPatch.baseFileHash,
356
- patchFileHash: nextState.primaryPatch.patchFileHash,
357
- patchStorageUri: nextState.primaryPatch.patchStorageUri,
358
- });
359
- await deps.databasePlugin.commitBundle();
360
-
361
- if (
362
- previousPatch?.patchStorageUri &&
363
- previousPatch.patchStorageUri !== patchUpload.storageUri
364
- ) {
365
- await deps.storagePlugin.profiles.node
366
- .delete(previousPatch.patchStorageUri)
367
- .catch(() => {
368
- return;
369
- });
370
- }
313
+ const uploadKey = createBundleStorageKey(
314
+ targetBundle.id,
315
+ "patches",
316
+ baseBundle.id,
317
+ patchFileHash,
318
+ getRelativeStorageDir(targetAssetPath),
319
+ patchFilename,
320
+ );
321
+ const patchUpload = await deps.storagePlugin.put({
322
+ key: uploadKey,
323
+ body: new ReadableStream<Uint8Array>({
324
+ start(controller) {
325
+ controller.enqueue(patchBytes);
326
+ controller.close();
327
+ },
328
+ }),
329
+ contentLength: patchBytes.byteLength,
330
+ contentType: "application/octet-stream",
331
+ });
371
332
 
372
- const updatedBundle = await deps.databasePlugin.getBundleById(
373
- targetBundle.id,
374
- );
375
- if (!updatedBundle) {
376
- throw new Error("Updated bundle not found");
377
- }
333
+ const nextPatch = {
334
+ baseBundleId: baseBundle.id,
335
+ baseFileHash: baseAssetHash,
336
+ byteSize: patchBytes.byteLength,
337
+ patchFileHash,
338
+ patchStorageUri: patchUpload.storageUri,
339
+ };
340
+ const patches = buildNextPatchState({
341
+ currentBundle: targetBundle,
342
+ nextPatch,
343
+ makePrimary: options.makePrimary ?? true,
344
+ });
378
345
 
379
- return updatedBundle;
380
- } finally {
381
- await fs.rm(workDir, { force: true, recursive: true });
346
+ const updatedBundle: Bundle = {
347
+ ...targetBundle,
348
+ patches,
349
+ };
350
+ await database.updateBundleById(targetBundle.id, updatedBundle);
351
+
352
+ if (
353
+ previousPatch?.patchStorageUri &&
354
+ previousPatch.patchStorageUri !== patchUpload.storageUri
355
+ ) {
356
+ await deps.storagePlugin
357
+ .delete({ storageUri: previousPatch.patchStorageUri })
358
+ .catch(() => {
359
+ return;
360
+ });
382
361
  }
362
+
363
+ return updatedBundle;
383
364
  }
@@ -0,0 +1,62 @@
1
+ import { type DatabasePlugin } from "@hot-updater/plugin-core";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { createInMemoryDatabasePlugin } from "../../../test-utils/test/inMemoryDatabasePlugin";
5
+ import { createDatabasePluginCore } from "./databasePluginCore";
6
+ import {
7
+ currentBundle,
8
+ resolveFileUrl,
9
+ } from "./databasePluginCore.testFixtures";
10
+
11
+ describe("createDatabasePluginCore", () => {
12
+ it("runs the schema readiness guard before a low plugin operation", async () => {
13
+ const plugin: DatabasePlugin = createInMemoryDatabasePlugin();
14
+ const beforeOperation = vi.fn(async () => {});
15
+ const core = createDatabasePluginCore(plugin, resolveFileUrl, {
16
+ beforeOperation,
17
+ });
18
+
19
+ await core.api.getChannels();
20
+
21
+ expect(beforeOperation).toHaveBeenCalledOnce();
22
+ });
23
+
24
+ it("inserts an artifact without synthesizing a Release", async () => {
25
+ const plugin: DatabasePlugin = createInMemoryDatabasePlugin();
26
+ const core = createDatabasePluginCore(plugin, resolveFileUrl);
27
+
28
+ await core.api.insertBundle(currentBundle);
29
+
30
+ await expect(
31
+ core.api.getBundleById(currentBundle.id),
32
+ ).resolves.toMatchObject(currentBundle);
33
+ await expect(
34
+ plugin.models.releases.findMany({
35
+ bundleId: currentBundle.id,
36
+ limit: 10,
37
+ }),
38
+ ).resolves.toEqual([]);
39
+ });
40
+
41
+ it("updates an artifact without requiring or mutating a Release", async () => {
42
+ const plugin: DatabasePlugin = createInMemoryDatabasePlugin();
43
+ const core = createDatabasePluginCore(plugin, resolveFileUrl);
44
+ await core.api.insertBundle(currentBundle);
45
+ await core.api.updateBundleById(currentBundle.id, {
46
+ storageUri: `r2://bucket/bundles/${currentBundle.id}/updated.zip`,
47
+ });
48
+
49
+ await expect(
50
+ core.api.getBundleById(currentBundle.id),
51
+ ).resolves.toMatchObject({
52
+ id: currentBundle.id,
53
+ storageUri: `r2://bucket/bundles/${currentBundle.id}/updated.zip`,
54
+ });
55
+ await expect(
56
+ plugin.models.releases.findMany({
57
+ bundleId: currentBundle.id,
58
+ limit: 10,
59
+ }),
60
+ ).resolves.toEqual([]);
61
+ });
62
+ });
@@ -0,0 +1,76 @@
1
+ import type { Bundle } from "@hot-updater/core";
2
+ import {
3
+ createDatabaseClient,
4
+ type DatabasePlugin,
5
+ } from "@hot-updater/plugin-core";
6
+
7
+ export const currentBundle: Bundle = {
8
+ id: "00000000-0000-0000-0000-000000000001",
9
+ fileHash: "current-archive-hash",
10
+ gitCommitHash: null,
11
+ platform: "ios",
12
+ storageUri:
13
+ "r2://bucket/bundles/00000000-0000-0000-0000-000000000001/archive.zip",
14
+ archiveByteSize: 3_000_000_001,
15
+ manifestStorageUri:
16
+ "r2://bucket/bundles/00000000-0000-0000-0000-000000000001/manifest.json",
17
+ manifestFileHash: "sig:current-manifest",
18
+ assetBaseStorageUri: "r2://bucket/assets",
19
+ };
20
+
21
+ export const targetBundle: Bundle = {
22
+ ...currentBundle,
23
+ id: "00000000-0000-0000-0000-000000000002",
24
+ fileHash: "target-archive-hash",
25
+ storageUri:
26
+ "r2://bucket/bundles/00000000-0000-0000-0000-000000000002/archive.zip",
27
+ manifestStorageUri:
28
+ "r2://bucket/bundles/00000000-0000-0000-0000-000000000002/manifest.json",
29
+ manifestFileHash: "sig:target-manifest",
30
+ assetBaseStorageUri: "r2://bucket/assets",
31
+ patches: [
32
+ {
33
+ baseBundleId: currentBundle.id,
34
+ baseFileHash: "current-bundle-hash",
35
+ patchFileHash: "patch-hash",
36
+ patchStorageUri:
37
+ "r2://bucket/bundles/00000000-0000-0000-0000-000000000002/patches/00000000-0000-0000-0000-000000000001/index.ios.bundle.bsdiff",
38
+ byteSize: 3_000_000_002,
39
+ },
40
+ ],
41
+ };
42
+
43
+ export const resolveFileUrl = async (storageUri: string | null) => {
44
+ if (!storageUri) return null;
45
+ const url = new URL(storageUri);
46
+ return `https://assets.example.com/${url.host}${url.pathname}`;
47
+ };
48
+
49
+ export const seedBundles = async (plugin: DatabasePlugin): Promise<void> => {
50
+ const client = createDatabaseClient(plugin);
51
+ await client.insertBundle(currentBundle);
52
+ await client.insertBundle(targetBundle);
53
+ };
54
+
55
+ export const manifests = new Map<string, string>([
56
+ [
57
+ currentBundle.manifestStorageUri ?? "",
58
+ JSON.stringify({
59
+ bundleId: currentBundle.id,
60
+ assets: {
61
+ "index.ios.bundle": { fileHash: "current-bundle-hash" },
62
+ "shared.png": { fileHash: "shared-hash" },
63
+ },
64
+ }),
65
+ ],
66
+ [
67
+ targetBundle.manifestStorageUri ?? "",
68
+ JSON.stringify({
69
+ bundleId: targetBundle.id,
70
+ assets: {
71
+ "index.ios.bundle": { fileHash: "target-bundle-hash" },
72
+ "shared.png": { fileHash: "shared-hash" },
73
+ },
74
+ }),
75
+ ],
76
+ ]);
@@ -0,0 +1,185 @@
1
+ import type { Bundle } from "@hot-updater/core";
2
+ import { decodeChannelKey } from "@hot-updater/core";
3
+ import {
4
+ type ChannelRow,
5
+ createDatabaseClient,
6
+ deleteRelease as deleteReleasePolicy,
7
+ preflightReleasePolicy as preflightReleasePolicyMutation,
8
+ rebuildReleaseCatalog as rebuildReleaseCatalogProjection,
9
+ updateReleasePolicy as updateReleasePolicyMutation,
10
+ } from "@hot-updater/plugin-core";
11
+
12
+ import {
13
+ createArtifactResolver,
14
+ createReleaseCatalogReader,
15
+ } from "./releaseCatalog";
16
+ import type { DatabaseAPI, DatabasePlugin } from "./types";
17
+
18
+ export function createDatabasePluginCore(
19
+ database: DatabasePlugin,
20
+ resolveFileUrl: (storageUri: string | null) => Promise<string | null>,
21
+ options?: {
22
+ beforeOperation?: () => Promise<void>;
23
+ readStorageText?: (storageUri: string) => Promise<string | null>;
24
+ },
25
+ ): {
26
+ api: DatabaseAPI;
27
+ adapterName: string;
28
+ createMigrator: () => never;
29
+ generateSchema: () => never;
30
+ } {
31
+ const client = createDatabaseClient(database);
32
+ const beforeOperation = options?.beforeOperation;
33
+ const getReleaseCatalog = createReleaseCatalogReader(database);
34
+ const getArtifact = createArtifactResolver({
35
+ database,
36
+ readStorageText: options?.readStorageText,
37
+ resolveFileUrl,
38
+ });
39
+ const api: DatabaseAPI = {
40
+ async getBundleById(id: string): Promise<Bundle | null> {
41
+ await beforeOperation?.();
42
+ return client.getBundleById(id);
43
+ },
44
+
45
+ async getReleaseCatalog(input) {
46
+ await beforeOperation?.();
47
+ return getReleaseCatalog(input);
48
+ },
49
+
50
+ async getArtifactInfo(targetBundleId, currentBundleId) {
51
+ await beforeOperation?.();
52
+ return getArtifact(targetBundleId, currentBundleId);
53
+ },
54
+
55
+ async getReleaseById(id) {
56
+ await beforeOperation?.();
57
+ return database.models.releases.findById(id);
58
+ },
59
+
60
+ async getReleasesByScope(input) {
61
+ await beforeOperation?.();
62
+ return database.models.releases.findManyByScope({
63
+ ...input,
64
+ consistency: "strong",
65
+ });
66
+ },
67
+
68
+ async getReleases(input) {
69
+ await beforeOperation?.();
70
+ return database.models.releases.findMany(input);
71
+ },
72
+
73
+ async getReleaseCatalogByScopeKey(scopeKey) {
74
+ await beforeOperation?.();
75
+ return database.models.releaseCatalogs.findByScopeKey(scopeKey);
76
+ },
77
+
78
+ async getReleaseCatalogs(input) {
79
+ await beforeOperation?.();
80
+ return database.models.releaseCatalogs.findMany(input);
81
+ },
82
+
83
+ async updateReleasePolicy(input) {
84
+ await beforeOperation?.();
85
+ return updateReleasePolicyMutation({ database, ...input });
86
+ },
87
+
88
+ async preflightReleasePolicy(input) {
89
+ await beforeOperation?.();
90
+ return preflightReleasePolicyMutation({ database, ...input });
91
+ },
92
+
93
+ async deleteRelease(input) {
94
+ await beforeOperation?.();
95
+ return deleteReleasePolicy({ database, ...input });
96
+ },
97
+
98
+ async rebuildReleaseCatalog(scopeKey) {
99
+ await beforeOperation?.();
100
+ const catalog =
101
+ await database.models.releaseCatalogs.findByScopeKey(scopeKey);
102
+ if (catalog === null) {
103
+ throw new Error(`Release catalog "${scopeKey}" was not found.`);
104
+ }
105
+ return rebuildReleaseCatalogProjection({
106
+ database,
107
+ scope: {
108
+ channelId: catalog.channel_id,
109
+ channelName: decodeChannelKey(catalog.channel_key),
110
+ fingerprintHash: catalog.fingerprint_hash,
111
+ platform: catalog.platform,
112
+ scopeKey: catalog.scope_key,
113
+ strategy: catalog.strategy,
114
+ },
115
+ });
116
+ },
117
+
118
+ async commitDatabase(input) {
119
+ await beforeOperation?.();
120
+ return database.commit(input);
121
+ },
122
+
123
+ async getChannels(): Promise<readonly ChannelRow[]> {
124
+ await beforeOperation?.();
125
+ return client.getChannels();
126
+ },
127
+
128
+ async insertChannel(input) {
129
+ await beforeOperation?.();
130
+ return database.models.channels.insert(input);
131
+ },
132
+
133
+ async deleteChannel(input) {
134
+ await beforeOperation?.();
135
+ return database.models.channels.delete(input);
136
+ },
137
+
138
+ async getBundles(options) {
139
+ await beforeOperation?.();
140
+ return client.getBundles(options);
141
+ },
142
+
143
+ async insertBundle(bundle: Bundle): Promise<void> {
144
+ await beforeOperation?.();
145
+ await client.insertBundle(bundle);
146
+ },
147
+
148
+ async insertBundles(bundles: readonly Bundle[]): Promise<void> {
149
+ await beforeOperation?.();
150
+ await client.mutate(async (transaction) => {
151
+ for (const bundle of bundles) {
152
+ await transaction.insertBundle(bundle);
153
+ }
154
+ });
155
+ },
156
+
157
+ async updateBundleById(
158
+ bundleId: string,
159
+ update: Partial<Bundle>,
160
+ ): Promise<void> {
161
+ await beforeOperation?.();
162
+ await client.updateBundleById(bundleId, update);
163
+ },
164
+
165
+ async deleteBundleById(bundleId: string): Promise<void> {
166
+ await beforeOperation?.();
167
+ await client.deleteBundleById(bundleId);
168
+ },
169
+ };
170
+
171
+ return {
172
+ api,
173
+ adapterName: database.name,
174
+ createMigrator: () => {
175
+ throw new Error(
176
+ "createMigrator is only available for Kysely/MongoDB database plugins.",
177
+ );
178
+ },
179
+ generateSchema: () => {
180
+ throw new Error(
181
+ "generateSchema is only available for Drizzle/Prisma database plugins.",
182
+ );
183
+ },
184
+ };
185
+ }