@hot-updater/server 1.0.0-rc.1 → 1.0.0-rc.16

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 (542) hide show
  1. package/dist/_virtual/_rolldown/runtime.cjs +1 -1
  2. package/dist/adapters/drizzle.cjs +35 -69
  3. package/dist/adapters/drizzle.d.cts +13 -6
  4. package/dist/adapters/drizzle.d.mts +13 -6
  5. package/dist/adapters/drizzle.mjs +34 -69
  6. package/dist/adapters/drizzleExecutor.cjs +114 -0
  7. package/dist/adapters/drizzleExecutor.d.cts +9 -0
  8. package/dist/adapters/drizzleExecutor.d.mts +9 -0
  9. package/dist/adapters/drizzleExecutor.mjs +112 -0
  10. package/dist/adapters/kysely.cjs +25 -31
  11. package/dist/adapters/kysely.d.cts +10 -7
  12. package/dist/adapters/kysely.d.mts +10 -7
  13. package/dist/adapters/kysely.mjs +25 -31
  14. package/dist/adapters/kyselyExecutor.cjs +32 -0
  15. package/dist/adapters/kyselyExecutor.d.cts +12 -0
  16. package/dist/adapters/kyselyExecutor.d.mts +12 -0
  17. package/dist/adapters/kyselyExecutor.mjs +32 -0
  18. package/dist/adapters/mongodb.cjs +28 -27
  19. package/dist/adapters/mongodb.d.cts +10 -5
  20. package/dist/adapters/mongodb.d.mts +10 -5
  21. package/dist/adapters/mongodb.mjs +28 -27
  22. package/dist/adapters/mongodbAdapter.cjs +174 -0
  23. package/dist/adapters/mongodbAdapter.d.cts +9 -0
  24. package/dist/adapters/mongodbAdapter.d.mts +9 -0
  25. package/dist/adapters/mongodbAdapter.mjs +173 -0
  26. package/dist/adapters/prisma.cjs +38 -220
  27. package/dist/adapters/prisma.d.cts +12 -8
  28. package/dist/adapters/prisma.d.mts +12 -8
  29. package/dist/adapters/prisma.mjs +38 -219
  30. package/dist/adapters/prismaExecutor.cjs +64 -0
  31. package/dist/adapters/prismaExecutor.mjs +64 -0
  32. package/dist/adapters/sqlProviders.cjs +9 -0
  33. package/dist/adapters/sqlProviders.mjs +9 -0
  34. package/dist/apiKeys.cjs +8 -8
  35. package/dist/apiKeys.d.cts +1 -24
  36. package/dist/apiKeys.d.mts +1 -24
  37. package/dist/apiKeys.mjs +5 -9
  38. package/dist/assembly/assemblePlugins.cjs +147 -0
  39. package/dist/assembly/assemblePlugins.d.cts +12 -0
  40. package/dist/assembly/assemblePlugins.d.mts +12 -0
  41. package/dist/assembly/assemblePlugins.mjs +146 -0
  42. package/dist/assembly/databasePlugins.cjs +23 -0
  43. package/dist/assembly/databasePlugins.d.cts +43 -0
  44. package/dist/assembly/databasePlugins.d.mts +43 -0
  45. package/dist/assembly/databasePlugins.mjs +22 -0
  46. package/dist/core/api.cjs +91 -0
  47. package/dist/core/api.d.cts +65 -0
  48. package/dist/core/api.d.mts +65 -0
  49. package/dist/core/api.mjs +87 -0
  50. package/dist/core/baseCandidates.cjs +71 -0
  51. package/dist/core/baseCandidates.d.cts +17 -0
  52. package/dist/core/baseCandidates.d.mts +17 -0
  53. package/dist/core/baseCandidates.mjs +69 -0
  54. package/dist/core/operations.cjs +350 -0
  55. package/dist/core/operations.mjs +350 -0
  56. package/dist/core/reads.cjs +273 -0
  57. package/dist/core/reads.d.cts +48 -0
  58. package/dist/core/reads.d.mts +48 -0
  59. package/dist/core/reads.mjs +267 -0
  60. package/dist/core/releases.cjs +193 -0
  61. package/dist/core/releases.mjs +190 -0
  62. package/dist/core/schema.cjs +279 -0
  63. package/dist/core/schema.d.cts +307 -0
  64. package/dist/core/schema.d.mts +307 -0
  65. package/dist/core/schema.mjs +277 -0
  66. package/dist/core/writes.cjs +96 -0
  67. package/dist/core/writes.mjs +91 -0
  68. package/dist/createHotUpdaterCore.cjs +61 -63
  69. package/dist/createHotUpdaterCore.d.cts +53 -45
  70. package/dist/createHotUpdaterCore.d.mts +53 -45
  71. package/dist/createHotUpdaterCore.mjs +61 -63
  72. package/dist/database/cursor.cjs +44 -0
  73. package/dist/database/cursor.d.cts +7 -0
  74. package/dist/database/cursor.d.mts +7 -0
  75. package/dist/database/cursor.mjs +40 -0
  76. package/dist/database/database.cjs +44 -0
  77. package/dist/database/database.d.cts +123 -0
  78. package/dist/database/database.d.mts +123 -0
  79. package/dist/database/database.mjs +44 -0
  80. package/dist/database/definitions.d.cts +68 -0
  81. package/dist/database/definitions.d.mts +68 -0
  82. package/dist/database/engine.cjs +37 -0
  83. package/dist/database/engine.d.cts +40 -0
  84. package/dist/database/engine.d.mts +40 -0
  85. package/dist/database/engine.mjs +37 -0
  86. package/dist/database/engineAggregates.cjs +115 -0
  87. package/dist/database/engineAggregates.mjs +112 -0
  88. package/dist/database/engineReads.cjs +180 -0
  89. package/dist/database/engineReads.d.cts +35 -0
  90. package/dist/database/engineReads.d.mts +35 -0
  91. package/dist/database/engineReads.mjs +179 -0
  92. package/dist/database/engineTransaction.cjs +403 -0
  93. package/dist/database/engineTransaction.d.cts +26 -0
  94. package/dist/database/engineTransaction.d.mts +26 -0
  95. package/dist/database/engineTransaction.mjs +402 -0
  96. package/dist/database/errors.cjs +27 -0
  97. package/dist/database/errors.d.cts +21 -0
  98. package/dist/database/errors.d.mts +21 -0
  99. package/dist/database/errors.mjs +24 -0
  100. package/dist/database/fence.cjs +124 -0
  101. package/dist/database/fence.d.cts +42 -0
  102. package/dist/database/fence.d.mts +42 -0
  103. package/dist/database/fence.mjs +116 -0
  104. package/dist/database/index.cjs +127 -0
  105. package/dist/database/index.d.cts +17 -0
  106. package/dist/database/index.d.mts +17 -0
  107. package/dist/database/index.mjs +15 -0
  108. package/dist/database/kv/kvAdapter.cjs +273 -0
  109. package/dist/database/kv/kvAdapter.d.cts +101 -0
  110. package/dist/database/kv/kvAdapter.d.mts +101 -0
  111. package/dist/database/kv/kvAdapter.mjs +272 -0
  112. package/dist/database/resolveSchema.cjs +196 -0
  113. package/dist/database/resolveSchema.d.cts +42 -0
  114. package/dist/database/resolveSchema.d.mts +42 -0
  115. package/dist/database/resolveSchema.mjs +193 -0
  116. package/dist/database/schema.cjs +21 -0
  117. package/dist/database/schema.d.cts +61 -0
  118. package/dist/database/schema.d.mts +61 -0
  119. package/dist/database/schema.mjs +20 -0
  120. package/dist/database/sql/sqlAdapter.cjs +282 -0
  121. package/dist/database/sql/sqlAdapter.d.cts +46 -0
  122. package/dist/database/sql/sqlAdapter.d.mts +46 -0
  123. package/dist/database/sql/sqlAdapter.mjs +279 -0
  124. package/dist/database/sql/sqlSchema.cjs +115 -0
  125. package/dist/database/sql/sqlSchema.d.cts +8 -0
  126. package/dist/database/sql/sqlSchema.d.mts +8 -0
  127. package/dist/database/sql/sqlSchema.mjs +111 -0
  128. package/dist/db/builtInDatabase.cjs +104 -0
  129. package/dist/db/builtInDatabase.d.cts +38 -0
  130. package/dist/db/builtInDatabase.d.mts +38 -0
  131. package/dist/db/builtInDatabase.mjs +97 -0
  132. package/dist/db/createBundleDiff.cjs +6 -6
  133. package/dist/db/createBundleDiff.d.cts +4 -7
  134. package/dist/db/createBundleDiff.d.mts +4 -7
  135. package/dist/db/createBundleDiff.mjs +5 -5
  136. package/dist/db/engineDrizzleSchema.cjs +54 -0
  137. package/dist/db/engineDrizzleSchema.mjs +54 -0
  138. package/dist/db/engineMigrator.cjs +53 -0
  139. package/dist/db/engineMigrator.mjs +53 -0
  140. package/dist/db/enginePrismaSchema.cjs +85 -0
  141. package/dist/db/enginePrismaSchema.d.cts +5 -0
  142. package/dist/db/enginePrismaSchema.d.mts +5 -0
  143. package/dist/db/enginePrismaSchema.mjs +84 -0
  144. package/dist/db/engineSql.cjs +26 -0
  145. package/dist/db/engineSql.d.cts +17 -0
  146. package/dist/db/engineSql.d.mts +17 -0
  147. package/dist/db/engineSql.mjs +25 -0
  148. package/dist/db/engineSqlMigrator.cjs +66 -0
  149. package/dist/db/engineSqlMigrator.mjs +66 -0
  150. package/dist/db/fixedMigratorShared.cjs +0 -11
  151. package/dist/db/fixedMigratorShared.mjs +1 -8
  152. package/dist/db/index.cjs +23 -8
  153. package/dist/db/index.d.cts +10 -4
  154. package/dist/db/index.d.mts +10 -4
  155. package/dist/db/index.mjs +16 -8
  156. package/dist/db/releaseCatalog.cjs +20 -44
  157. package/dist/db/releaseCatalog.d.cts +2 -3
  158. package/dist/db/releaseCatalog.d.mts +2 -3
  159. package/dist/db/releaseCatalog.mjs +20 -44
  160. package/dist/db/schemaSettings.cjs +63 -0
  161. package/dist/db/schemaSettings.d.cts +12 -0
  162. package/dist/db/schemaSettings.d.mts +12 -0
  163. package/dist/db/schemaSettings.mjs +62 -0
  164. package/dist/db/settingsMigrator.cjs +92 -0
  165. package/dist/db/settingsMigrator.mjs +89 -0
  166. package/dist/db/types.cjs +3 -12
  167. package/dist/db/types.d.cts +26 -69
  168. package/dist/db/types.d.mts +26 -69
  169. package/dist/db/types.mjs +2 -11
  170. package/dist/db/updateArtifacts.cjs +38 -68
  171. package/dist/db/updateArtifacts.mjs +40 -69
  172. package/dist/handler.cjs +72 -43
  173. package/dist/handler.d.cts +3 -6
  174. package/dist/handler.d.mts +3 -6
  175. package/dist/handler.mjs +73 -43
  176. package/dist/handlerAdminRoutes.cjs +375 -0
  177. package/dist/handlerAdminRoutes.mjs +373 -0
  178. package/dist/handlerParameters.cjs +6 -11
  179. package/dist/handlerParameters.mjs +6 -10
  180. package/dist/handlerReleaseCatalogRoutes.cjs +11 -14
  181. package/dist/handlerReleaseCatalogRoutes.mjs +11 -13
  182. package/dist/handlerTypes.d.cts +2 -56
  183. package/dist/handlerTypes.d.mts +2 -56
  184. package/dist/handlerVersionRoutes.cjs +20 -4
  185. package/dist/handlerVersionRoutes.d.cts +1 -0
  186. package/dist/handlerVersionRoutes.d.mts +1 -0
  187. package/dist/handlerVersionRoutes.mjs +20 -4
  188. package/dist/index.cjs +4 -7
  189. package/dist/index.d.cts +10 -9
  190. package/dist/index.d.mts +10 -9
  191. package/dist/index.mjs +4 -4
  192. package/dist/insights/domain.d.cts +53 -70
  193. package/dist/insights/domain.d.mts +53 -70
  194. package/dist/insights/errors.cjs +35 -8
  195. package/dist/insights/errors.mjs +35 -8
  196. package/dist/insights/eventInput.cjs +64 -24
  197. package/dist/insights/eventInput.mjs +63 -25
  198. package/dist/insights/provider.cjs +356 -0
  199. package/dist/insights/provider.d.cts +6 -0
  200. package/dist/insights/provider.d.mts +6 -0
  201. package/dist/insights/provider.mjs +355 -0
  202. package/dist/insights/queryInput.cjs +78 -43
  203. package/dist/insights/queryInput.mjs +76 -40
  204. package/dist/insights/routes.cjs +94 -34
  205. package/dist/insights/routes.mjs +93 -35
  206. package/dist/insights/types.d.cts +27 -12
  207. package/dist/insights/types.d.mts +27 -12
  208. package/dist/node.d.cts +1 -2
  209. package/dist/node.d.mts +1 -2
  210. package/dist/package.cjs +1 -1
  211. package/dist/package.mjs +1 -1
  212. package/dist/plugins/api-keys/index.cjs +50 -0
  213. package/dist/plugins/api-keys/index.d.cts +85 -0
  214. package/dist/plugins/api-keys/index.d.mts +85 -0
  215. package/dist/plugins/api-keys/index.mjs +47 -0
  216. package/dist/plugins/api-keys/model.cjs +58 -0
  217. package/dist/plugins/api-keys/model.d.cts +8 -0
  218. package/dist/plugins/api-keys/model.d.mts +8 -0
  219. package/dist/plugins/api-keys/model.mjs +58 -0
  220. package/dist/plugins/api-keys/schema.cjs +38 -0
  221. package/dist/plugins/api-keys/schema.d.cts +44 -0
  222. package/dist/plugins/api-keys/schema.d.mts +44 -0
  223. package/dist/plugins/api-keys/schema.mjs +39 -0
  224. package/dist/plugins/builtIn.cjs +7 -0
  225. package/dist/plugins/builtIn.mjs +6 -0
  226. package/dist/plugins/definePlugin.cjs +5 -0
  227. package/dist/plugins/definePlugin.d.cts +73 -0
  228. package/dist/plugins/definePlugin.d.mts +73 -0
  229. package/dist/plugins/definePlugin.mjs +5 -0
  230. package/dist/plugins/index.cjs +17 -0
  231. package/dist/plugins/index.d.cts +9 -0
  232. package/dist/plugins/index.d.mts +9 -0
  233. package/dist/plugins/index.mjs +7 -0
  234. package/dist/plugins/insights/index.cjs +48 -0
  235. package/dist/plugins/insights/index.d.cts +295 -0
  236. package/dist/plugins/insights/index.d.mts +295 -0
  237. package/dist/plugins/insights/index.mjs +44 -0
  238. package/dist/plugins/insights/model.cjs +17 -0
  239. package/dist/plugins/insights/model.d.cts +10 -0
  240. package/dist/plugins/insights/model.d.mts +10 -0
  241. package/dist/plugins/insights/model.mjs +17 -0
  242. package/dist/plugins/insights/reads.cjs +471 -0
  243. package/dist/plugins/insights/reads.mjs +466 -0
  244. package/dist/plugins/insights/recordEvent.cjs +135 -0
  245. package/dist/plugins/insights/recordEvent.d.cts +13 -0
  246. package/dist/plugins/insights/recordEvent.d.mts +13 -0
  247. package/dist/plugins/insights/recordEvent.mjs +132 -0
  248. package/dist/plugins/insights/schema.cjs +347 -0
  249. package/dist/plugins/insights/schema.d.cts +258 -0
  250. package/dist/plugins/insights/schema.d.mts +258 -0
  251. package/dist/plugins/insights/schema.mjs +344 -0
  252. package/dist/storageAccess.cjs +0 -1
  253. package/dist/types/index.d.cts +2 -29
  254. package/dist/types/index.d.mts +2 -29
  255. package/package.json +30 -7
  256. package/src/adapters/drizzle.conformance.integration.spec.ts +34 -0
  257. package/src/adapters/drizzle.readBudgets.integration.spec.ts +41 -0
  258. package/src/adapters/drizzle.spec.ts +277 -208
  259. package/src/adapters/drizzle.ts +48 -128
  260. package/src/adapters/drizzleExecutor.ts +166 -0
  261. package/src/adapters/kysely.conformance.integration.spec.ts +36 -0
  262. package/src/adapters/kysely.readBudgets.integration.spec.ts +42 -0
  263. package/src/adapters/kysely.spec.ts +212 -117
  264. package/src/adapters/kysely.ts +32 -87
  265. package/src/adapters/kyselyExecutor.ts +53 -0
  266. package/src/adapters/mongodb.integration.spec.ts +180 -0
  267. package/src/adapters/mongodb.readBudgets.integration.spec.ts +61 -0
  268. package/src/adapters/mongodb.ts +36 -65
  269. package/src/adapters/mongodbAdapter.ts +293 -0
  270. package/src/adapters/prisma.conformance.integration.spec.ts +36 -0
  271. package/src/adapters/prisma.integration.spec.ts +138 -0
  272. package/src/adapters/prisma.readBudgets.integration.spec.ts +40 -0
  273. package/src/adapters/prisma.spec.ts +226 -173
  274. package/src/adapters/prisma.ts +60 -451
  275. package/src/adapters/prismaExecutor.ts +107 -0
  276. package/src/adapters/prismaTestClients.ts +175 -0
  277. package/src/adapters/sqlProviders.ts +14 -0
  278. package/src/apiKeys.integration.spec.ts +19 -119
  279. package/src/apiKeys.spec.ts +39 -35
  280. package/src/apiKeys.ts +2 -8
  281. package/src/assembly/assemblePlugins.ts +269 -0
  282. package/src/assembly/assembly.spec.ts +325 -0
  283. package/src/assembly/assembly.types.spec.ts +155 -0
  284. package/src/assembly/builtInRoutes.spec.ts +213 -0
  285. package/src/assembly/databasePlugins.spec.ts +71 -0
  286. package/src/assembly/databasePlugins.ts +96 -0
  287. package/src/core/api.ts +139 -0
  288. package/src/core/baseCandidates.spec.ts +378 -0
  289. package/src/core/baseCandidates.ts +136 -0
  290. package/src/core/index.ts +44 -0
  291. package/src/core/operations.spec.ts +415 -0
  292. package/src/core/operations.ts +551 -0
  293. package/src/core/reads.spec.ts +300 -0
  294. package/src/core/reads.ts +417 -0
  295. package/src/core/releases.spec.ts +308 -0
  296. package/src/core/releases.ts +345 -0
  297. package/src/core/schema.ts +173 -0
  298. package/src/core/writes.ts +160 -0
  299. package/src/createHotUpdaterCore.ts +191 -167
  300. package/src/database/cursor.ts +65 -0
  301. package/src/database/database.ts +335 -0
  302. package/src/database/database.types.spec.ts +135 -0
  303. package/src/database/definitions.ts +72 -0
  304. package/src/database/engine.ts +62 -0
  305. package/src/database/engineAggregates.spec.ts +539 -0
  306. package/src/database/engineAggregates.ts +193 -0
  307. package/src/database/engineReads.spec.ts +219 -0
  308. package/src/database/engineReads.ts +302 -0
  309. package/src/database/engineTransaction.spec.ts +710 -0
  310. package/src/database/engineTransaction.ts +588 -0
  311. package/src/database/errors.ts +30 -0
  312. package/src/database/fence.spec.ts +223 -0
  313. package/src/database/fence.ts +171 -0
  314. package/src/database/index.ts +154 -0
  315. package/src/database/kv/kvAdapter.conformance.integration.spec.ts +17 -0
  316. package/src/database/kv/kvAdapter.readBudgets.integration.spec.ts +17 -0
  317. package/src/database/kv/kvAdapter.spec.ts +468 -0
  318. package/src/database/kv/kvAdapter.ts +450 -0
  319. package/src/database/kv/kvTestStore.ts +91 -0
  320. package/src/database/memory.readBudgets.integration.spec.ts +13 -0
  321. package/src/database/resolveSchema.spec.ts +230 -0
  322. package/src/database/resolveSchema.ts +372 -0
  323. package/src/database/schema.ts +169 -0
  324. package/src/database/schema.types.spec.ts +94 -0
  325. package/src/database/sql/docker-compose.yml +36 -0
  326. package/src/database/sql/sqlAdapter.integration.spec.ts +148 -0
  327. package/src/database/sql/sqlAdapter.readBudgets.integration.spec.ts +109 -0
  328. package/src/database/sql/sqlAdapter.spec.ts +255 -0
  329. package/src/database/sql/sqlAdapter.ts +532 -0
  330. package/src/database/sql/sqlSchema.ts +205 -0
  331. package/src/database/sql/sqlTestExecutors.ts +183 -0
  332. package/src/databaseTestSuite.spec.ts +39 -0
  333. package/src/db/builtInDatabase.ts +149 -0
  334. package/src/db/bundleRows.spec.ts +3 -3
  335. package/src/db/createBundleDiff.spec.ts +43 -29
  336. package/src/db/createBundleDiff.ts +13 -7
  337. package/src/db/engineDrizzleSchema.ts +72 -0
  338. package/src/db/engineMigrator.ts +105 -0
  339. package/src/db/enginePrismaSchema.ts +147 -0
  340. package/src/db/engineSql.spec.ts +129 -0
  341. package/src/db/engineSql.ts +48 -0
  342. package/src/db/engineSqlMigrator.ts +116 -0
  343. package/src/db/fixedMigratorShared.ts +1 -20
  344. package/src/db/index.spec.ts +116 -824
  345. package/src/db/index.ts +43 -14
  346. package/src/db/pluginTables.spec.ts +218 -0
  347. package/src/db/releaseCatalog.ts +48 -90
  348. package/src/db/schemaSettings.ts +104 -0
  349. package/src/db/settingsMigrator.spec.ts +65 -0
  350. package/src/db/settingsMigrator.ts +152 -0
  351. package/src/db/types.ts +25 -165
  352. package/src/db/updateArtifacts.spec.ts +170 -294
  353. package/src/db/updateArtifacts.ts +74 -142
  354. package/src/db/updateCheck.bench.ts +47 -0
  355. package/src/handler-standalone.integration.spec.ts +204 -412
  356. package/src/handler.spec.ts +134 -51
  357. package/src/handler.testFixtures.ts +14 -19
  358. package/src/handler.ts +163 -75
  359. package/src/handlerAdminRoutes.spec.ts +183 -0
  360. package/src/handlerAdminRoutes.ts +536 -0
  361. package/src/handlerAuth.integration.spec.ts +3 -3
  362. package/src/handlerManagement.spec.ts +136 -196
  363. package/src/handlerReleaseCatalogRoutes.ts +15 -17
  364. package/src/handlerTypes.ts +3 -80
  365. package/src/handlerVersionRoutes.ts +20 -7
  366. package/src/index.ts +7 -14
  367. package/src/insights/domain.ts +56 -77
  368. package/src/insights/errors.spec.ts +48 -0
  369. package/src/insights/errors.ts +50 -6
  370. package/src/insights/eventInput.ts +80 -27
  371. package/src/insights/insights.integration.spec.ts +525 -44
  372. package/src/insights/provider.spec.ts +574 -0
  373. package/src/insights/provider.ts +667 -0
  374. package/src/insights/queryInput.ts +113 -73
  375. package/src/insights/routes.ts +125 -69
  376. package/src/insights/types.ts +47 -36
  377. package/src/lintRules.spec.ts +76 -0
  378. package/src/plugins/api-keys/apiKeysPlugin.spec.ts +172 -0
  379. package/src/plugins/api-keys/index.ts +66 -0
  380. package/src/plugins/api-keys/model.ts +69 -0
  381. package/src/plugins/api-keys/schema.ts +22 -0
  382. package/src/plugins/builtIn.ts +5 -0
  383. package/src/plugins/definePlugin.ts +100 -0
  384. package/src/plugins/index.ts +55 -0
  385. package/src/plugins/insights/index.ts +86 -0
  386. package/src/plugins/insights/insightsRollout.integration.spec.ts +187 -0
  387. package/src/plugins/insights/model.spec.ts +66 -0
  388. package/src/plugins/insights/model.ts +19 -0
  389. package/src/plugins/insights/reads.spec.ts +725 -0
  390. package/src/plugins/insights/reads.ts +707 -0
  391. package/src/plugins/insights/recordEvent.spec.ts +443 -0
  392. package/src/plugins/insights/recordEvent.ts +238 -0
  393. package/src/plugins/insights/schema.ts +229 -0
  394. package/src/plugins/pluginTestHarness.spec.ts +56 -0
  395. package/src/readBudgets.testFixtures.ts +15 -0
  396. package/src/releaseCatalogRoutes.integration.spec.ts +47 -92
  397. package/src/runtime.spec.ts +91 -77
  398. package/src/runtime.testFixtures.ts +28 -44
  399. package/src/runtimeBundles.integration.spec.ts +112 -0
  400. package/src/testUtilsMutation.integration.spec.ts +215 -0
  401. package/src/testUtilsPackage.integration.spec.ts +176 -0
  402. package/src/types/index.ts +0 -32
  403. package/dist/adapters/databaseConstraintErrors.cjs +0 -23
  404. package/dist/adapters/databaseConstraintErrors.mjs +0 -21
  405. package/dist/adapters/databasePluginUtils.cjs +0 -117
  406. package/dist/adapters/databasePluginUtils.mjs +0 -106
  407. package/dist/adapters/drizzleCrud.cjs +0 -250
  408. package/dist/adapters/drizzleCrud.mjs +0 -249
  409. package/dist/adapters/drizzleLazyDB.cjs +0 -101
  410. package/dist/adapters/drizzleLazyDB.mjs +0 -101
  411. package/dist/adapters/drizzleQuery.cjs +0 -68
  412. package/dist/adapters/drizzleQuery.mjs +0 -67
  413. package/dist/adapters/kyselyCrud.cjs +0 -254
  414. package/dist/adapters/kyselyCrud.mjs +0 -253
  415. package/dist/adapters/kyselyQuery.cjs +0 -56
  416. package/dist/adapters/kyselyQuery.mjs +0 -55
  417. package/dist/adapters/mongodbCollections.cjs +0 -36
  418. package/dist/adapters/mongodbCollections.mjs +0 -30
  419. package/dist/adapters/mongodbQuery.cjs +0 -77
  420. package/dist/adapters/mongodbQuery.mjs +0 -70
  421. package/dist/adapters/mongodbReads.cjs +0 -148
  422. package/dist/adapters/mongodbReads.mjs +0 -147
  423. package/dist/adapters/mongodbWrites.cjs +0 -134
  424. package/dist/adapters/mongodbWrites.mjs +0 -133
  425. package/dist/adapters/prismaQuery.cjs +0 -58
  426. package/dist/adapters/prismaQuery.mjs +0 -56
  427. package/dist/adapters/prismaRows.cjs +0 -198
  428. package/dist/adapters/prismaRows.mjs +0 -188
  429. package/dist/db/databasePluginCore.cjs +0 -138
  430. package/dist/db/databasePluginCore.mjs +0 -137
  431. package/dist/db/fixedMigrator.cjs +0 -79
  432. package/dist/db/fixedMigrator.mjs +0 -78
  433. package/dist/db/fixedMigratorMongo.cjs +0 -319
  434. package/dist/db/fixedMigratorMongo.mjs +0 -319
  435. package/dist/db/mongoMigrationExecution.cjs +0 -11
  436. package/dist/db/mongoMigrationExecution.mjs +0 -11
  437. package/dist/db/schema/mongodb.cjs +0 -25
  438. package/dist/db/schema/mongodb.mjs +0 -25
  439. package/dist/db/schema/registry.cjs +0 -21
  440. package/dist/db/schema/registry.mjs +0 -17
  441. package/dist/db/schema/sql.cjs +0 -101
  442. package/dist/db/schema/sql.mjs +0 -97
  443. package/dist/db/schema/sqlOperations.cjs +0 -27
  444. package/dist/db/schema/sqlOperations.mjs +0 -26
  445. package/dist/db/schemaGenerators.cjs +0 -256
  446. package/dist/db/schemaGenerators.mjs +0 -254
  447. package/dist/db/schemaReadiness.cjs +0 -24
  448. package/dist/db/schemaReadiness.d.cts +0 -8
  449. package/dist/db/schemaReadiness.d.mts +0 -8
  450. package/dist/db/schemaReadiness.mjs +0 -23
  451. package/dist/db/sqlMigrationExecution.cjs +0 -67
  452. package/dist/db/sqlMigrationExecution.mjs +0 -66
  453. package/dist/handlerBundleRoutes.cjs +0 -155
  454. package/dist/handlerBundleRoutes.mjs +0 -155
  455. package/dist/handlerReleaseManagementRoutes.cjs +0 -125
  456. package/dist/handlerReleaseManagementRoutes.mjs +0 -124
  457. package/dist/insights/bounded/activeOverview.cjs +0 -87
  458. package/dist/insights/bounded/activeOverview.mjs +0 -85
  459. package/dist/insights/bounded/installationSearch.cjs +0 -42
  460. package/dist/insights/bounded/installationSearch.mjs +0 -42
  461. package/dist/insights/bounded/persistence.cjs +0 -39
  462. package/dist/insights/bounded/persistence.mjs +0 -38
  463. package/dist/insights/bounded/provider.cjs +0 -170
  464. package/dist/insights/bounded/provider.d.cts +0 -7
  465. package/dist/insights/bounded/provider.d.mts +0 -7
  466. package/dist/insights/bounded/provider.mjs +0 -170
  467. package/dist/insights/bounded/scan.cjs +0 -144
  468. package/dist/insights/bounded/scan.mjs +0 -138
  469. package/dist/insights/persistence.d.cts +0 -6
  470. package/dist/insights/persistence.d.mts +0 -6
  471. package/dist/schema/dsl.cjs +0 -103
  472. package/dist/schema/dsl.mjs +0 -87
  473. package/dist/schema/index.cjs +0 -6
  474. package/dist/schema/index.mjs +0 -7
  475. package/dist/schema/settings.cjs +0 -9
  476. package/dist/schema/settings.mjs +0 -9
  477. package/dist/schema/types.cjs +0 -8
  478. package/dist/schema/types.mjs +0 -6
  479. package/dist/schema/v1_0_0.cjs +0 -364
  480. package/dist/schema/v1_0_0.mjs +0 -357
  481. package/src/adapters/databaseConstraintErrors.ts +0 -25
  482. package/src/adapters/databasePluginTestDatabase.ts +0 -106
  483. package/src/adapters/databasePluginUtils.spec.ts +0 -29
  484. package/src/adapters/databasePluginUtils.ts +0 -214
  485. package/src/adapters/drizzleCrud.ts +0 -419
  486. package/src/adapters/drizzleLazyDB.ts +0 -257
  487. package/src/adapters/drizzleQuery.ts +0 -164
  488. package/src/adapters/kyselyCrud.ts +0 -588
  489. package/src/adapters/kyselyQuery.ts +0 -128
  490. package/src/adapters/mongodb.spec.ts +0 -141
  491. package/src/adapters/mongodbCollections.ts +0 -62
  492. package/src/adapters/mongodbQuery.ts +0 -226
  493. package/src/adapters/mongodbReads.ts +0 -311
  494. package/src/adapters/mongodbRows.spec.ts +0 -64
  495. package/src/adapters/mongodbRows.ts +0 -118
  496. package/src/adapters/mongodbTestClient.ts +0 -342
  497. package/src/adapters/mongodbTestFilter.ts +0 -166
  498. package/src/adapters/mongodbWrites.ts +0 -260
  499. package/src/adapters/prismaQuery.ts +0 -129
  500. package/src/adapters/prismaRows.spec.ts +0 -98
  501. package/src/adapters/prismaRows.ts +0 -301
  502. package/src/adapters/prismaTestClient.ts +0 -396
  503. package/src/calculatePagination.ts +0 -34
  504. package/src/db/databasePluginCore.spec.ts +0 -62
  505. package/src/db/databasePluginCore.testFixtures.ts +0 -76
  506. package/src/db/databasePluginCore.ts +0 -185
  507. package/src/db/databasePluginUpdateCheck.bench.ts +0 -107
  508. package/src/db/fixedMigrator.spec.ts +0 -231
  509. package/src/db/fixedMigrator.ts +0 -147
  510. package/src/db/fixedMigratorMongo.spec.ts +0 -192
  511. package/src/db/fixedMigratorMongo.ts +0 -425
  512. package/src/db/hotUpdaterSchema.ts +0 -5
  513. package/src/db/mongoMigrationExecution.ts +0 -23
  514. package/src/db/schema/definitions.ts +0 -1
  515. package/src/db/schema/mongodb.ts +0 -40
  516. package/src/db/schema/registry.ts +0 -55
  517. package/src/db/schema/sql.ts +0 -241
  518. package/src/db/schema/sqlOperations.ts +0 -63
  519. package/src/db/schema/types.ts +0 -1
  520. package/src/db/schemaGenerators.ts +0 -466
  521. package/src/db/schemaReadiness.ts +0 -34
  522. package/src/db/sqlMigrationExecution.spec.ts +0 -65
  523. package/src/db/sqlMigrationExecution.ts +0 -108
  524. package/src/db/types.spec.ts +0 -55
  525. package/src/handlerBundleRoutes.ts +0 -259
  526. package/src/handlerReleaseManagementRoutes.ts +0 -213
  527. package/src/insights/bounded/activeOverview.spec.ts +0 -189
  528. package/src/insights/bounded/activeOverview.ts +0 -148
  529. package/src/insights/bounded/installationSearch.spec.ts +0 -130
  530. package/src/insights/bounded/installationSearch.ts +0 -69
  531. package/src/insights/bounded/persistence.ts +0 -43
  532. package/src/insights/bounded/provider.spec.ts +0 -219
  533. package/src/insights/bounded/provider.ts +0 -229
  534. package/src/insights/bounded/scan.ts +0 -235
  535. package/src/insights/persistence.ts +0 -10
  536. package/src/schema/dsl-all-versions.spec.ts +0 -35
  537. package/src/schema/dsl.ts +0 -187
  538. package/src/schema/index.ts +0 -10
  539. package/src/schema/settings.ts +0 -15
  540. package/src/schema/types.ts +0 -79
  541. package/src/schema/v1_0_0.spec.ts +0 -146
  542. package/src/schema/v1_0_0.ts +0 -465
@@ -0,0 +1,443 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+
3
+ import type { BundleEventRow } from "@hot-updater/plugin-core";
4
+ import {
5
+ countInsightsDistinct,
6
+ createMemoryAdapter,
7
+ type DatabaseAdapter,
8
+ type WriteOp,
9
+ } from "@hot-updater/plugin-core/internal";
10
+ import { createPluginTestHarness } from "@hot-updater/test-utils";
11
+ import { describe, expect, it } from "vitest";
12
+
13
+ import * as engine from "../../database";
14
+ import type { HotUpdaterDatabase } from "../../database/database";
15
+ import { createKvAdapter } from "../../database/kv/kvAdapter";
16
+ import { createMemoryKeyValueStore } from "../../database/kv/kvTestStore";
17
+ import { createSqlAdapter } from "../../database/sql/sqlAdapter";
18
+ import { sqliteExecutor } from "../../database/sql/sqlTestExecutors";
19
+ import {
20
+ insights,
21
+ insightsIdentity,
22
+ type InsightsIdentityParts,
23
+ type InsightsSchema,
24
+ } from "./index";
25
+ import { DAILY_EVENTS } from "./schema";
26
+
27
+ const HOUR = 3_600_000;
28
+ const DAY = 86_400_000;
29
+ const T = Date.UTC(2026, 8, 23, 10, 15);
30
+ const hour = (ms: number) => ms - (ms % HOUR);
31
+
32
+ const uuid = (n: number) =>
33
+ `00000000-0000-7000-8000-${String(n).padStart(12, "0")}`;
34
+
35
+ const event = (
36
+ n: number,
37
+ overrides: Partial<BundleEventRow> = {},
38
+ ): BundleEventRow =>
39
+ ({
40
+ id: uuid(n),
41
+ type: "UPDATE_APPLIED",
42
+ install_id: "install-1",
43
+ user_id: "user-1",
44
+ from_release_id: "release-1",
45
+ from_bundle_id: "bundle-1",
46
+ to_release_id: "release-2",
47
+ to_bundle_id: "bundle-2",
48
+ platform: "ios",
49
+ app_version: "1.0.0",
50
+ channel: "production",
51
+ metadata: {
52
+ username: null,
53
+ cohort: "1",
54
+ update_strategy: "appVersion",
55
+ fingerprint_hash: null,
56
+ sdk_version: null,
57
+ },
58
+ received_at_ms: T,
59
+ ...overrides,
60
+ }) as BundleEventRow;
61
+
62
+ const backends: [string, () => DatabaseAdapter][] = [
63
+ ["memory", () => createMemoryAdapter()],
64
+ [
65
+ "SQLite",
66
+ () =>
67
+ createSqlAdapter({
68
+ executor: sqliteExecutor(new DatabaseSync(":memory:")),
69
+ }),
70
+ ],
71
+ [
72
+ "the key-value helper",
73
+ () => createKvAdapter({ store: createMemoryKeyValueStore() }),
74
+ ],
75
+ ];
76
+
77
+ const identity = (
78
+ parts: Partial<InsightsIdentityParts> &
79
+ Pick<InsightsIdentityParts, "periodKind">,
80
+ ) =>
81
+ insightsIdentity({
82
+ scopeKind: "channel",
83
+ releaseKind: "all",
84
+ releaseId: "",
85
+ channel: "production",
86
+ platform: "ios",
87
+ appVersionKind: "all",
88
+ appVersion: "",
89
+ ...parts,
90
+ });
91
+
92
+ describe.each(backends)("insights recordEvent on %s", (_name, adapter) => {
93
+ const setup = async (wrap = (inner: DatabaseAdapter) => inner) => {
94
+ const harness = await createPluginTestHarness(insights(), {
95
+ engine,
96
+ adapter: wrap(adapter()),
97
+ });
98
+ const db = harness.db as HotUpdaterDatabase<InsightsSchema>;
99
+ const at = (key: string, bucket: number) => ({
100
+ index: "window" as const,
101
+ where: { identity: key },
102
+ range: { gte: bucket, lte: bucket },
103
+ limit: 100,
104
+ });
105
+ const overview = async (key: string, bucket: number) =>
106
+ (await db.findAggregates("insights_overview", at(key, bucket))).rows[0];
107
+ const sketch = async (key: string, bucket: number) =>
108
+ (await db.findAggregates("insights_sketches", at(key, bucket))).rows[0];
109
+ const distribution = async () =>
110
+ (
111
+ await db.findAggregates("insights_distribution", {
112
+ index: "byScope",
113
+ where: { channel: "production", platform: "ios" },
114
+ limit: 100,
115
+ })
116
+ ).rows;
117
+ const byBundle = async (
118
+ field: "from_bundle_id" | "to_bundle_id",
119
+ bundleId: string,
120
+ type: string,
121
+ ) =>
122
+ (
123
+ await db.findAggregates("insights_latest_by_bundle", {
124
+ index: "byBundle",
125
+ where: {
126
+ platform: "ios",
127
+ channel: "production",
128
+ bundle_field: field,
129
+ bundle_id: bundleId,
130
+ type,
131
+ },
132
+ limit: 100,
133
+ })
134
+ ).rows;
135
+ const outcomes = async (type: string, ref: string) =>
136
+ (
137
+ await db.findAggregates("insights_outcomes", {
138
+ index: "byRef",
139
+ where: {
140
+ platform: "ios",
141
+ channel: "production",
142
+ type,
143
+ bundle_ref: ref,
144
+ },
145
+ limit: 100,
146
+ })
147
+ ).rows;
148
+ /** The per-day count of every event, which the global event list reads. */
149
+ const everyEvent = async () =>
150
+ (
151
+ await db.findAggregates("insights_outcomes", {
152
+ index: "byRef",
153
+ where: DAILY_EVENTS,
154
+ limit: 100,
155
+ })
156
+ ).rows;
157
+ return {
158
+ ...harness,
159
+ db,
160
+ overview,
161
+ sketch,
162
+ distribution,
163
+ byBundle,
164
+ outcomes,
165
+ everyEvent,
166
+ };
167
+ };
168
+
169
+ it("records the event with derived keys, the head, gauges, counters, and sketches", async () => {
170
+ const {
171
+ api,
172
+ db,
173
+ overview,
174
+ sketch,
175
+ distribution,
176
+ byBundle,
177
+ outcomes,
178
+ everyEvent,
179
+ } = await setup();
180
+ await api.recordEvent(event(1));
181
+
182
+ await expect(
183
+ db.findOne("bundle_events", { id: uuid(1) }),
184
+ ).resolves.toMatchObject({
185
+ day: T - (T % DAY),
186
+ movement_install_id: "install-1",
187
+ bundle_ref: ["to:bundle-2"],
188
+ });
189
+ await expect(
190
+ db.findOne("bundle_event_heads", { install_id: "install-1" }),
191
+ ).resolves.toEqual({ ...event(1), current_release_id: "release-2" });
192
+ await expect(distribution()).resolves.toEqual([
193
+ {
194
+ channel: "production",
195
+ platform: "ios",
196
+ app_version: "1.0.0",
197
+ release_id: "release-2",
198
+ bucket_start_ms: hour(T),
199
+ latest_installations: 1,
200
+ },
201
+ ]);
202
+ for (const [field, bundle] of [
203
+ ["from_bundle_id", "bundle-1"],
204
+ ["to_bundle_id", "bundle-2"],
205
+ ] as const) {
206
+ await expect(
207
+ byBundle(field, bundle, "UPDATE_APPLIED"),
208
+ ).resolves.toMatchObject([{ installations: 1 }]);
209
+ }
210
+ await expect(
211
+ outcomes("UPDATE_APPLIED", "to:bundle-2"),
212
+ ).resolves.toMatchObject([{ bucket_start_ms: hour(T), events: 1 }]);
213
+ // Every event's row is a UTC day's, not an hour's.
214
+ await expect(everyEvent()).resolves.toEqual([
215
+ { ...DAILY_EVENTS, bucket_start_ms: T - (T % DAY), events: 1 },
216
+ ]);
217
+
218
+ const release = { scopeKind: "release", releaseKind: "specific" } as const;
219
+ const counters = { downloads: 0, launches: 1, failed_launches: 0 };
220
+ await expect(
221
+ overview(
222
+ identity({
223
+ ...release,
224
+ releaseId: "release-2",
225
+ periodKind: "lifetime",
226
+ }),
227
+ 0,
228
+ ),
229
+ ).resolves.toMatchObject(counters);
230
+ for (const [periodKind, bucket] of [
231
+ ["hour", hour(T)],
232
+ ["day", T - (T % DAY)],
233
+ ] as const) {
234
+ await expect(
235
+ overview(identity({ periodKind }), bucket),
236
+ ).resolves.toMatchObject(counters);
237
+ const launches = await sketch(identity({ periodKind }), bucket);
238
+ expect(countInsightsDistinct(launches!.launch_users)).toBe(1);
239
+ for (const platform of ["ios", "all"] as const) {
240
+ const usage = await sketch(
241
+ identity({
242
+ scopeKind: "usage",
243
+ platform,
244
+ appVersionKind: "specific",
245
+ appVersion: "1.0.0",
246
+ periodKind,
247
+ }),
248
+ bucket,
249
+ );
250
+ expect(countInsightsDistinct(usage!.activity_users)).toBe(1);
251
+ }
252
+ }
253
+ });
254
+
255
+ it("keys an event by the one bundle its bundle filter reads", async () => {
256
+ const { api, db } = await setup();
257
+ await api.recordEvent(event(1));
258
+ await api.recordEvent(
259
+ event(2, { type: "RECOVERED", install_id: "install-2" }),
260
+ );
261
+
262
+ await expect(
263
+ db.findOne("bundle_events", { id: uuid(2) }),
264
+ ).resolves.toMatchObject({ bundle_ref: ["from:bundle-1"] });
265
+ const scope = { platform: "ios", channel: "production" } as const;
266
+ for (const [n, filter] of [
267
+ [1, { ...scope, type: "UPDATE_APPLIED", toBundleId: "bundle-2" }],
268
+ [2, { ...scope, type: "RECOVERED", fromBundleId: "bundle-1" }],
269
+ ] as const) {
270
+ await expect(
271
+ api.listEvents({
272
+ filter: { kind: "bundle", ...filter },
273
+ sinceMs: T,
274
+ beforeReceivedAtMs: T + HOUR,
275
+ limit: 10,
276
+ }),
277
+ ).resolves.toMatchObject([{ id: uuid(n) }]);
278
+ }
279
+ });
280
+
281
+ it("changes nothing for a repeated id, even with another payload", async () => {
282
+ const { api, db, outcomes, everyEvent } = await setup();
283
+ await api.recordEvent(event(1));
284
+ await api.recordEvent(
285
+ event(1, { install_id: "install-2", received_at_ms: T + HOUR }),
286
+ );
287
+
288
+ await expect(
289
+ db.findOne("bundle_events", { id: uuid(1) }),
290
+ ).resolves.toMatchObject({ install_id: "install-1" });
291
+ await expect(
292
+ db.findOne("bundle_event_heads", { install_id: "install-2" }),
293
+ ).resolves.toBeNull();
294
+ await expect(
295
+ outcomes("UPDATE_APPLIED", "to:bundle-2"),
296
+ ).resolves.toMatchObject([{ events: 1 }]);
297
+ await expect(everyEvent()).resolves.toMatchObject([{ events: 1 }]);
298
+ });
299
+
300
+ it("moves the head only for a newer event and deletes gauge rows that reach zero", async () => {
301
+ const { api, db, distribution, byBundle, outcomes } = await setup();
302
+ await api.recordEvent(event(1));
303
+ await api.recordEvent(
304
+ event(2, {
305
+ type: "UPDATE_DOWNLOADED",
306
+ user_id: null,
307
+ from_release_id: "release-2",
308
+ from_bundle_id: "bundle-2",
309
+ to_release_id: "release-3",
310
+ to_bundle_id: "bundle-3",
311
+ received_at_ms: T + 2 * HOUR,
312
+ }),
313
+ );
314
+ await api.recordEvent(event(3, { received_at_ms: T + HOUR }));
315
+
316
+ await expect(
317
+ db.findOne("bundle_event_heads", { install_id: "install-1" }),
318
+ ).resolves.toMatchObject({
319
+ id: uuid(2),
320
+ user_id: null,
321
+ type: "UPDATE_DOWNLOADED",
322
+ current_release_id: "release-2",
323
+ });
324
+ await expect(distribution()).resolves.toMatchObject([
325
+ {
326
+ release_id: "release-2",
327
+ bucket_start_ms: hour(T + 2 * HOUR),
328
+ latest_installations: 1,
329
+ },
330
+ ]);
331
+ await expect(
332
+ byBundle("to_bundle_id", "bundle-2", "UPDATE_APPLIED"),
333
+ ).resolves.toEqual([]);
334
+ await expect(
335
+ byBundle("to_bundle_id", "bundle-3", "UPDATE_DOWNLOADED"),
336
+ ).resolves.toMatchObject([{ installations: 1 }]);
337
+ await expect(
338
+ outcomes("UPDATE_APPLIED", "to:bundle-2"),
339
+ ).resolves.toMatchObject([
340
+ { bucket_start_ms: hour(T), events: 1 },
341
+ { bucket_start_ms: hour(T + HOUR), events: 1 },
342
+ ]);
343
+
344
+ await api.recordEvent(event(4, { received_at_ms: T + 2 * HOUR }));
345
+ await expect(
346
+ db.findOne("bundle_event_heads", { install_id: "install-1" }),
347
+ ).resolves.toMatchObject({ id: uuid(4), type: "UPDATE_APPLIED" });
348
+ await expect(distribution()).resolves.toMatchObject([
349
+ {
350
+ release_id: "release-2",
351
+ bucket_start_ms: hour(T + 2 * HOUR),
352
+ latest_installations: 1,
353
+ },
354
+ ]);
355
+ });
356
+
357
+ it("reads the event and head in one round, gauge and sketch rows in a second, and writes once", async () => {
358
+ const calls: string[] = [];
359
+ const { api, measureReads } = await setup((inner) => ({
360
+ ...inner,
361
+ get: async (table, keys) => {
362
+ calls.push(`get ${table.name}`);
363
+ return inner.get(table, keys);
364
+ },
365
+ write: async (ops: readonly WriteOp[]) => {
366
+ calls.push("write");
367
+ return inner.write(ops);
368
+ },
369
+ }));
370
+ const measured = await measureReads(() => api.recordEvent(event(1)));
371
+
372
+ expect(measured.adapter.queries).toBe(0);
373
+ expect(calls).toEqual([
374
+ "get bundle_events",
375
+ "get bundle_event_heads",
376
+ "get insights_sketches",
377
+ "get insights_distribution",
378
+ "get insights_latest_by_bundle",
379
+ "write",
380
+ ]);
381
+ });
382
+
383
+ it("serializes events per installation and keeps gauges exact under concurrency", async () => {
384
+ const { api, db, distribution } = await setup();
385
+ const installs = ["a", "b", "c", "d"];
386
+ const events = installs.flatMap((install, position) =>
387
+ [0, 1, 2].map((step) =>
388
+ event(position * 10 + step + 1, {
389
+ install_id: `install-${install}`,
390
+ received_at_ms: T + ((step * 7 + position) % 3) * HOUR,
391
+ }),
392
+ ),
393
+ );
394
+ await Promise.all(events.map((row) => api.recordEvent(row)));
395
+
396
+ for (const install of installs) {
397
+ const latest = events
398
+ .filter((row) => row.install_id === `install-${install}`)
399
+ .toSorted(
400
+ (left, right) =>
401
+ left.received_at_ms - right.received_at_ms ||
402
+ (left.id < right.id ? -1 : 1),
403
+ )
404
+ .at(-1)!;
405
+ await expect(
406
+ db.findOne("bundle_event_heads", { install_id: `install-${install}` }),
407
+ ).resolves.toMatchObject({ id: latest.id });
408
+ }
409
+ const counted = (await distribution()).reduce(
410
+ (sum, row) => sum + row.latest_installations,
411
+ 0,
412
+ );
413
+ expect(counted).toBe(installs.length);
414
+ });
415
+
416
+ it("records a retried report once, and a racing installation's report under its id not at all", async () => {
417
+ const { api, db, outcomes } = await setup();
418
+ await api.recordEvent(event(1));
419
+ // A retry repeats the report's id, even when it arrives an hour later.
420
+ await api.recordEvent(event(1, { received_at_ms: T + HOUR }));
421
+ await expect(
422
+ db.findOne("bundle_event_heads", { install_id: "install-1" }),
423
+ ).resolves.toMatchObject({ id: uuid(1), received_at_ms: T });
424
+ await expect(
425
+ outcomes("UPDATE_APPLIED", "to:bundle-2"),
426
+ ).resolves.toMatchObject([{ events: 1 }]);
427
+
428
+ // The later writer reads the winner's row, at once or in the rerun after
429
+ // its own insert fails, and changes nothing.
430
+ const installs = ["install-2", "install-3"];
431
+ await Promise.all(
432
+ installs.map((install) =>
433
+ api.recordEvent(event(2, { install_id: install })),
434
+ ),
435
+ );
436
+ const stored = await db.findOne("bundle_events", { id: uuid(2) });
437
+ const loser = installs.find((install) => install !== stored?.install_id);
438
+ expect(installs).toContain(stored?.install_id);
439
+ await expect(
440
+ db.findOne("bundle_event_heads", { install_id: loser! }),
441
+ ).resolves.toBeNull();
442
+ });
443
+ });
@@ -0,0 +1,238 @@
1
+ import type { BundleEventRow } from "@hot-updater/plugin-core";
2
+ import {
3
+ addInsightsDistinct,
4
+ assertBundleEventRow,
5
+ compareUtf8,
6
+ currentInsightsReleaseId,
7
+ insightsKey,
8
+ insightsOverviewDeltas,
9
+ insightsOverviewId,
10
+ type InsightsOverviewIdentity,
11
+ } from "@hot-updater/plugin-core/internal";
12
+
13
+ import type {
14
+ HotUpdaterDatabase,
15
+ HotUpdaterTransaction,
16
+ } from "../../database/database";
17
+ import { DAILY_EVENTS, DAY_MS, HOUR_MS, type InsightsSchema } from "./schema";
18
+
19
+ /** The head columns `countHead` reads. */
20
+ interface Head {
21
+ readonly install_id: string;
22
+ readonly id: string;
23
+ readonly received_at_ms: number;
24
+ readonly platform: string;
25
+ readonly channel: string;
26
+ readonly type: string;
27
+ readonly from_bundle_id: string | null;
28
+ readonly to_bundle_id: string;
29
+ readonly current_release_id: string | null;
30
+ readonly app_version: string;
31
+ }
32
+
33
+ const hourOf = (ms: number) => ms - (ms % HOUR_MS);
34
+ const dayOf = (ms: number) => ms - (ms % DAY_MS);
35
+
36
+ /** An overview or sketch row's scope and period; day periods roll up channel and usage rows. */
37
+ export type InsightsIdentityParts = Omit<
38
+ InsightsOverviewIdentity,
39
+ "bucketStartMs" | "periodKind"
40
+ > & { readonly periodKind: InsightsOverviewIdentity["periodKind"] | "day" };
41
+
42
+ /** The `identity` of an overview or sketch row: its scope and period, hashed. */
43
+ export const insightsIdentity = (identity: InsightsIdentityParts): string =>
44
+ insightsOverviewId({
45
+ ...identity,
46
+ periodKind: identity.periodKind as InsightsOverviewIdentity["periodKind"],
47
+ bucketStartMs: 0,
48
+ });
49
+
50
+ /** The head columns besides its key: the whole event and its current release. */
51
+ const headFields = (event: BundleEventRow) => {
52
+ const { install_id: _, ...fields } = event;
53
+ return { ...fields, current_release_id: currentInsightsReleaseId(event) };
54
+ };
55
+
56
+ /**
57
+ * The `insights_latest_by_bundle` field of a head's (from, to) pair. A head
58
+ * matches a `from` and a `to` predicate of one type at once only through its
59
+ * pair, so this gauge is what a count subtracts to count it once.
60
+ */
61
+ export const PAIR_FIELD = "from_to";
62
+
63
+ /** The pair gauge's `bundle_id`: a hash of the two bundle ids, which fit no single 36-character column. */
64
+ export const bundlePairKey = (from: string, to: string): string =>
65
+ insightsKey(`${from.length}:${from}${to.length}:${to}`);
66
+
67
+ /** Moves a head's gauges: the distribution row, one row per bundle it references, and its pair. */
68
+ const countHead = (
69
+ tx: HotUpdaterTransaction<InsightsSchema>,
70
+ head: Head,
71
+ delta: 1 | -1,
72
+ ) => {
73
+ const bucket = hourOf(head.received_at_ms);
74
+ const shardBy = head.install_id;
75
+ tx.aggregate(
76
+ "insights_distribution",
77
+ {
78
+ channel: head.channel,
79
+ platform: head.platform,
80
+ app_version: head.app_version,
81
+ release_id: head.current_release_id ?? "",
82
+ bucket_start_ms: bucket,
83
+ },
84
+ { latest_installations: delta },
85
+ { shardBy },
86
+ );
87
+ for (const field of ["from_bundle_id", "to_bundle_id"] as const) {
88
+ const bundleId = head[field];
89
+ if (bundleId === null) continue;
90
+ tx.aggregate(
91
+ "insights_latest_by_bundle",
92
+ {
93
+ platform: head.platform,
94
+ channel: head.channel,
95
+ bundle_field: field,
96
+ bundle_id: bundleId,
97
+ type: head.type,
98
+ bucket_start_ms: bucket,
99
+ },
100
+ { installations: delta },
101
+ { shardBy },
102
+ );
103
+ }
104
+ if (head.from_bundle_id !== null) {
105
+ tx.aggregate(
106
+ "insights_latest_by_bundle",
107
+ {
108
+ platform: head.platform,
109
+ channel: head.channel,
110
+ bundle_field: PAIR_FIELD,
111
+ bundle_id: bundlePairKey(head.from_bundle_id, head.to_bundle_id),
112
+ type: head.type,
113
+ bucket_start_ms: bucket,
114
+ },
115
+ { installations: delta },
116
+ { shardBy },
117
+ );
118
+ }
119
+ };
120
+
121
+ /**
122
+ * Counters and sketches for one event: release, channel, and usage rows, with
123
+ * day rollups for channel and usage; and its outcome rows, its bundle
124
+ * filter's hour and every event's day.
125
+ */
126
+ const countEvent = (
127
+ tx: HotUpdaterTransaction<InsightsSchema>,
128
+ event: BundleEventRow,
129
+ ) => {
130
+ const shardBy = event.install_id;
131
+ for (const delta of insightsOverviewDeltas(event)) {
132
+ const { bucketStartMs, ...parts } = delta.identity;
133
+ const periods =
134
+ parts.scopeKind === "channel" || parts.scopeKind === "usage"
135
+ ? ([
136
+ ["hour", bucketStartMs],
137
+ ["day", dayOf(event.received_at_ms)],
138
+ ] as const)
139
+ : ([[parts.periodKind, bucketStartMs]] as const);
140
+ for (const [periodKind, bucket] of periods) {
141
+ const key = {
142
+ identity: insightsIdentity({ ...parts, periodKind }),
143
+ bucket_start_ms: bucket,
144
+ };
145
+ const counters: {
146
+ downloads?: number;
147
+ launches?: number;
148
+ failed_launches?: number;
149
+ } = Object.fromEntries(
150
+ [
151
+ ["downloads", delta.downloads],
152
+ ["launches", delta.launches],
153
+ ["failed_launches", delta.failedLaunches],
154
+ ].filter(([, value]) => value !== 0),
155
+ );
156
+ if (Object.keys(counters).length > 0) {
157
+ tx.aggregate("insights_overview", key, counters, { shardBy });
158
+ }
159
+ const sketches = {
160
+ ...(delta.launchIdentity === undefined
161
+ ? {}
162
+ : { launch_users: addInsightsDistinct(null, delta.launchIdentity) }),
163
+ ...(delta.activityIdentity === undefined
164
+ ? {}
165
+ : {
166
+ activity_users: addInsightsDistinct(null, delta.activityIdentity),
167
+ }),
168
+ };
169
+ if (Object.keys(sketches).length > 0) {
170
+ tx.aggregate("insights_sketches", key, sketches, { shardBy });
171
+ }
172
+ }
173
+ }
174
+ tx.aggregate(
175
+ "insights_outcomes",
176
+ {
177
+ platform: event.platform,
178
+ channel: event.channel,
179
+ type: event.type,
180
+ bundle_ref:
181
+ event.type === "RECOVERED"
182
+ ? `from:${event.from_bundle_id}`
183
+ : `to:${event.to_bundle_id}`,
184
+ bucket_start_ms: hourOf(event.received_at_ms),
185
+ },
186
+ { events: 1 },
187
+ { shardBy },
188
+ );
189
+ // The global event list reads only the days this row counts: a gap costs
190
+ // it one empty day and one read of this row, not a read a day. One more
191
+ // blind increment per event, on the event's shard like the others.
192
+ tx.aggregate(
193
+ "insights_outcomes",
194
+ { ...DAILY_EVENTS, bucket_start_ms: dayOf(event.received_at_ms) },
195
+ { events: 1 },
196
+ { shardBy },
197
+ );
198
+ };
199
+
200
+ const isNewer = (event: Head, head: Head) =>
201
+ event.received_at_ms !== head.received_at_ms
202
+ ? event.received_at_ms > head.received_at_ms
203
+ : compareUtf8(event.id, head.id) > 0;
204
+
205
+ /**
206
+ * Records one event in one transaction: one batch read of the event and its
207
+ * installation's head, one of the gauge and sketch rows it changes, then one
208
+ * write. A repeated id changes nothing, whichever installation sends it; an
209
+ * older event still counts in its own hour but never replaces the head.
210
+ */
211
+ export const recordEvent = (
212
+ db: HotUpdaterDatabase<InsightsSchema>,
213
+ event: BundleEventRow,
214
+ ): Promise<void> => {
215
+ assertBundleEventRow(event);
216
+ return db.transaction(async (tx) => {
217
+ const [existing, previous] = await Promise.all([
218
+ tx.findOne("bundle_events", { id: event.id }),
219
+ tx.findOne("bundle_event_heads", { install_id: event.install_id }),
220
+ ]);
221
+ // The id is the report's idempotency key: a retry, or any report under
222
+ // an id already stored, changes nothing, as analytics ingestion drops
223
+ // duplicates.
224
+ if (existing !== null) return;
225
+ tx.create("bundle_events", event);
226
+ countEvent(tx, event);
227
+ const fields = headFields(event);
228
+ const head = { ...fields, install_id: event.install_id };
229
+ if (previous !== null && !isNewer(head, previous)) return;
230
+ if (previous === null) {
231
+ tx.create("bundle_event_heads", head);
232
+ } else {
233
+ countHead(tx, previous, -1);
234
+ tx.update("bundle_event_heads", previous, fields);
235
+ }
236
+ countHead(tx, head, 1);
237
+ });
238
+ };