@cosmicdrift/kumiko-framework 0.1.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 (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,584 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
3
+ import { createTestDb, type TestDb } from "../../stack";
4
+ import { generateId as uuid } from "../../utils";
5
+ import {
6
+ append,
7
+ createEventsTable,
8
+ loadAggregate,
9
+ loadAggregateAsOf,
10
+ loadAllEventsByType,
11
+ loadEventsAfterVersion,
12
+ type StoredEvent,
13
+ streamAllEventsByType,
14
+ VersionConflictError,
15
+ } from "../index";
16
+
17
+ let testDb: TestDb;
18
+
19
+ const tenantA = uuid();
20
+ const tenantB = uuid();
21
+ const userA = uuid();
22
+
23
+ beforeAll(async () => {
24
+ testDb = await createTestDb();
25
+ await createEventsTable(testDb.db);
26
+ });
27
+
28
+ afterAll(async () => {
29
+ await testDb.cleanup();
30
+ });
31
+
32
+ beforeEach(async () => {
33
+ await testDb.db.execute(sql`TRUNCATE kumiko_events RESTART IDENTITY`);
34
+ });
35
+
36
+ describe("event-store: append + load", () => {
37
+ test("append first event writes version=1 and round-trips via loadAggregate", async () => {
38
+ const aggregateId = uuid();
39
+
40
+ const stored = await append(testDb.db, {
41
+ aggregateId,
42
+ aggregateType: "task",
43
+ tenantId: tenantA,
44
+ expectedVersion: 0,
45
+ type: "task.created",
46
+ payload: { title: "Buy milk" },
47
+ metadata: { userId: userA },
48
+ });
49
+
50
+ expect(stored.version).toBe(1);
51
+ expect(stored.id).toBeDefined();
52
+
53
+ const events = await loadAggregate(testDb.db, aggregateId, tenantA);
54
+ expect(events).toHaveLength(1);
55
+ expect(events[0]?.type).toBe("task.created");
56
+ expect(events[0]?.payload).toEqual({ title: "Buy milk" });
57
+ expect(events[0]?.metadata.userId).toBe(userA);
58
+ });
59
+
60
+ test("subsequent appends increment version and are ordered", async () => {
61
+ const aggregateId = uuid();
62
+ const base = {
63
+ aggregateId,
64
+ aggregateType: "task",
65
+ tenantId: tenantA,
66
+ metadata: { userId: userA },
67
+ };
68
+
69
+ await append(testDb.db, {
70
+ ...base,
71
+ expectedVersion: 0,
72
+ type: "task.created",
73
+ payload: { title: "T" },
74
+ });
75
+ await append(testDb.db, {
76
+ ...base,
77
+ expectedVersion: 1,
78
+ type: "task.updated",
79
+ payload: { title: "T2" },
80
+ });
81
+ await append(testDb.db, {
82
+ ...base,
83
+ expectedVersion: 2,
84
+ type: "task.completed",
85
+ payload: {},
86
+ });
87
+
88
+ const events = await loadAggregate(testDb.db, aggregateId, tenantA);
89
+ expect(events.map((e) => e.version)).toEqual([1, 2, 3]);
90
+ expect(events.map((e) => e.type)).toEqual(["task.created", "task.updated", "task.completed"]);
91
+ });
92
+ });
93
+
94
+ describe("event-store: optimistic concurrency", () => {
95
+ test("wrong expectedVersion throws VersionConflictError (no write)", async () => {
96
+ const aggregateId = uuid();
97
+ await append(testDb.db, {
98
+ aggregateId,
99
+ aggregateType: "task",
100
+ tenantId: tenantA,
101
+ expectedVersion: 0,
102
+ type: "task.created",
103
+ payload: { title: "Orig" },
104
+ metadata: { userId: userA },
105
+ });
106
+
107
+ // Stale writer: thinks predecessor is at v0 — but v1 already exists.
108
+ await expect(
109
+ append(testDb.db, {
110
+ aggregateId,
111
+ aggregateType: "task",
112
+ tenantId: tenantA,
113
+ expectedVersion: 0,
114
+ type: "task.updated",
115
+ payload: { title: "Stale" },
116
+ metadata: { userId: userA },
117
+ }),
118
+ ).rejects.toThrow(VersionConflictError);
119
+
120
+ const events = await loadAggregate(testDb.db, aggregateId, tenantA);
121
+ expect(events).toHaveLength(1);
122
+ });
123
+
124
+ test("concurrent writers at same expectedVersion: exactly one wins", async () => {
125
+ const aggregateId = uuid();
126
+ await append(testDb.db, {
127
+ aggregateId,
128
+ aggregateType: "task",
129
+ tenantId: tenantA,
130
+ expectedVersion: 0,
131
+ type: "task.created",
132
+ payload: { title: "Orig" },
133
+ metadata: { userId: userA },
134
+ });
135
+
136
+ const update = (label: string) =>
137
+ append(testDb.db, {
138
+ aggregateId,
139
+ aggregateType: "task",
140
+ tenantId: tenantA,
141
+ expectedVersion: 1,
142
+ type: "task.updated",
143
+ payload: { title: label },
144
+ metadata: { userId: userA },
145
+ });
146
+
147
+ const results = await Promise.allSettled([update("A"), update("B")]);
148
+ const fulfilled = results.filter((r) => r.status === "fulfilled");
149
+ const rejected = results.filter((r) => r.status === "rejected");
150
+ expect(fulfilled).toHaveLength(1);
151
+ expect(rejected).toHaveLength(1);
152
+ expect((rejected[0] as PromiseRejectedResult).reason).toBeInstanceOf(VersionConflictError);
153
+
154
+ const events = await loadAggregate(testDb.db, aggregateId, tenantA);
155
+ expect(events).toHaveLength(2);
156
+ });
157
+ });
158
+
159
+ describe("event-store: tenant isolation", () => {
160
+ test("cross-tenant append at same aggregateId is rejected", async () => {
161
+ const aggregateId = uuid();
162
+ await append(testDb.db, {
163
+ aggregateId,
164
+ aggregateType: "task",
165
+ tenantId: tenantA,
166
+ expectedVersion: 0,
167
+ type: "task.created",
168
+ payload: {},
169
+ metadata: { userId: userA },
170
+ });
171
+
172
+ // Tenant B tries to write v2 against A's v1 — predecessor check must fail.
173
+ await expect(
174
+ append(testDb.db, {
175
+ aggregateId,
176
+ aggregateType: "task",
177
+ tenantId: tenantB,
178
+ expectedVersion: 1,
179
+ type: "task.updated",
180
+ payload: {},
181
+ metadata: { userId: userA },
182
+ }),
183
+ ).rejects.toThrow(VersionConflictError);
184
+
185
+ const eventsA = await loadAggregate(testDb.db, aggregateId, tenantA);
186
+ const eventsB = await loadAggregate(testDb.db, aggregateId, tenantB);
187
+ expect(eventsA).toHaveLength(1);
188
+ expect(eventsB).toHaveLength(0);
189
+ });
190
+ });
191
+
192
+ describe("event-store: requestId is a trace marker (no DB-level uniqueness)", () => {
193
+ test("same (tenant, requestId) twice → both events persist, no collision", async () => {
194
+ // Idempotency is an HTTP-level concern, handled via Redis in
195
+ // pipeline/idempotency.ts before the command executes. The events-table
196
+ // imposes no uniqueness on metadata.requestId — a single request may
197
+ // write N events (CRUD + ctx.appendEvent + saga follow-ups), all
198
+ // carrying the same requestId as a trace marker.
199
+ const aggregateId1 = uuid();
200
+ const aggregateId2 = uuid();
201
+ const requestId = uuid();
202
+
203
+ const first = await append(testDb.db, {
204
+ aggregateId: aggregateId1,
205
+ aggregateType: "task",
206
+ tenantId: tenantA,
207
+ expectedVersion: 0,
208
+ type: "task.created",
209
+ payload: { title: "First" },
210
+ metadata: { userId: userA, requestId },
211
+ });
212
+ const second = await append(testDb.db, {
213
+ aggregateId: aggregateId2,
214
+ aggregateType: "task",
215
+ tenantId: tenantA,
216
+ expectedVersion: 0,
217
+ type: "task.created",
218
+ payload: { title: "Second" },
219
+ metadata: { userId: userA, requestId },
220
+ });
221
+
222
+ expect(first.metadata.requestId).toBe(requestId);
223
+ expect(second.metadata.requestId).toBe(requestId);
224
+ expect(first.aggregateId).not.toBe(second.aggregateId);
225
+ });
226
+
227
+ test("metadata.headers (Marten free key/value) round-trips via append + load", async () => {
228
+ const aggregateId = uuid();
229
+ const headers = {
230
+ abTestBucket: "control",
231
+ sdkVersion: 42,
232
+ betaFeatures: true,
233
+ };
234
+
235
+ await append(testDb.db, {
236
+ aggregateId,
237
+ aggregateType: "task",
238
+ tenantId: tenantA,
239
+ expectedVersion: 0,
240
+ type: "task.created",
241
+ payload: { title: "with headers" },
242
+ metadata: { userId: userA, headers },
243
+ });
244
+
245
+ // Subsequent event uses the WHERE-EXISTS raw-SQL path — make sure
246
+ // headers survive that route too, not just the typed insertFirstEvent.
247
+ await append(testDb.db, {
248
+ aggregateId,
249
+ aggregateType: "task",
250
+ tenantId: tenantA,
251
+ expectedVersion: 1,
252
+ type: "task.updated",
253
+ payload: { title: "v2" },
254
+ metadata: { userId: userA, headers: { ...headers, sdkVersion: 43 } },
255
+ });
256
+
257
+ const events = await loadAggregate(testDb.db, aggregateId, tenantA);
258
+ expect(events).toHaveLength(2);
259
+ expect(events[0]?.metadata.headers).toEqual(headers);
260
+ expect(events[1]?.metadata.headers).toEqual({ ...headers, sdkVersion: 43 });
261
+ });
262
+ });
263
+
264
+ describe("event-store: asOf + after-version reads", () => {
265
+ test("loadAggregateAsOf excludes events after the timestamp", async () => {
266
+ const aggregateId = uuid();
267
+ const e1 = await append(testDb.db, {
268
+ aggregateId,
269
+ aggregateType: "task",
270
+ tenantId: tenantA,
271
+ expectedVersion: 0,
272
+ type: "task.created",
273
+ payload: {},
274
+ metadata: { userId: userA },
275
+ });
276
+ // Ensure the second event is strictly after.
277
+ await new Promise((r) => setTimeout(r, 5));
278
+ await append(testDb.db, {
279
+ aggregateId,
280
+ aggregateType: "task",
281
+ tenantId: tenantA,
282
+ expectedVersion: 1,
283
+ type: "task.updated",
284
+ payload: {},
285
+ metadata: { userId: userA },
286
+ });
287
+
288
+ const atT1 = await loadAggregateAsOf(testDb.db, aggregateId, tenantA, e1.createdAt);
289
+ expect(atT1).toHaveLength(1);
290
+ expect(atT1[0]?.version).toBe(1);
291
+ });
292
+
293
+ test("loadEventsAfterVersion returns only events strictly > given version", async () => {
294
+ const aggregateId = uuid();
295
+ for (let v = 0; v < 3; v++) {
296
+ await append(testDb.db, {
297
+ aggregateId,
298
+ aggregateType: "task",
299
+ tenantId: tenantA,
300
+ expectedVersion: v,
301
+ type: v === 0 ? "task.created" : "task.updated",
302
+ payload: { n: v },
303
+ metadata: { userId: userA },
304
+ });
305
+ }
306
+
307
+ const after1 = await loadEventsAfterVersion(testDb.db, aggregateId, tenantA, 1);
308
+ expect(after1.map((e) => e.version)).toEqual([2, 3]);
309
+ });
310
+ });
311
+
312
+ describe("event-store: loadAllEventsByType", () => {
313
+ // Backbone of projection-rebuild replay: all events of one aggregateType,
314
+ // cross-tenant, in chronological order. Flagged as untested in a prior
315
+ // audit — these tests close the gap.
316
+
317
+ test("returns only events of the requested aggregateType", async () => {
318
+ const taskId = uuid();
319
+ const invoiceId = uuid();
320
+
321
+ await append(testDb.db, {
322
+ aggregateId: taskId,
323
+ aggregateType: "task",
324
+ tenantId: tenantA,
325
+ expectedVersion: 0,
326
+ type: "task.created",
327
+ payload: { title: "T" },
328
+ metadata: { userId: userA },
329
+ });
330
+ await append(testDb.db, {
331
+ aggregateId: invoiceId,
332
+ aggregateType: "invoice",
333
+ tenantId: tenantA,
334
+ expectedVersion: 0,
335
+ type: "invoice.created",
336
+ payload: { amount: 42 },
337
+ metadata: { userId: userA },
338
+ });
339
+
340
+ const taskEvents = await loadAllEventsByType(testDb.db, "task");
341
+ const invoiceEvents = await loadAllEventsByType(testDb.db, "invoice");
342
+
343
+ expect(taskEvents).toHaveLength(1);
344
+ expect(taskEvents[0]?.aggregateId).toBe(taskId);
345
+ expect(taskEvents[0]?.type).toBe("task.created");
346
+ expect(invoiceEvents).toHaveLength(1);
347
+ expect(invoiceEvents[0]?.aggregateId).toBe(invoiceId);
348
+ });
349
+
350
+ test("spans all tenants — projection rebuild must see every row", async () => {
351
+ // Rebuilds run system-scoped (cross-tenant) because a projection table
352
+ // can hold data from many tenants. Missing this would leak tenant B's
353
+ // absence into tenant A's projection snapshot after a rebuild.
354
+ const aggA = uuid();
355
+ const aggB = uuid();
356
+
357
+ await append(testDb.db, {
358
+ aggregateId: aggA,
359
+ aggregateType: "task",
360
+ tenantId: tenantA,
361
+ expectedVersion: 0,
362
+ type: "task.created",
363
+ payload: { owner: "A" },
364
+ metadata: { userId: userA },
365
+ });
366
+ await append(testDb.db, {
367
+ aggregateId: aggB,
368
+ aggregateType: "task",
369
+ tenantId: tenantB,
370
+ expectedVersion: 0,
371
+ type: "task.created",
372
+ payload: { owner: "B" },
373
+ metadata: { userId: userA },
374
+ });
375
+
376
+ const all = await loadAllEventsByType(testDb.db, "task");
377
+ expect(all).toHaveLength(2);
378
+ const tenants = new Set(all.map((e) => e.tenantId));
379
+ expect(tenants).toEqual(new Set([tenantA, tenantB]));
380
+ });
381
+
382
+ test("ordered by (createdAt, id) for deterministic replay", async () => {
383
+ // Projection rebuild applies events in the order they were written.
384
+ // The ordering is part of the contract — without it, different
385
+ // replays would produce different projection states.
386
+ const a1 = uuid();
387
+ const a2 = uuid();
388
+ const a3 = uuid();
389
+
390
+ const e1 = await append(testDb.db, {
391
+ aggregateId: a1,
392
+ aggregateType: "task",
393
+ tenantId: tenantA,
394
+ expectedVersion: 0,
395
+ type: "task.created",
396
+ payload: { n: 1 },
397
+ metadata: { userId: userA },
398
+ });
399
+ await new Promise((r) => setTimeout(r, 5));
400
+ const e2 = await append(testDb.db, {
401
+ aggregateId: a2,
402
+ aggregateType: "task",
403
+ tenantId: tenantA,
404
+ expectedVersion: 0,
405
+ type: "task.created",
406
+ payload: { n: 2 },
407
+ metadata: { userId: userA },
408
+ });
409
+ await new Promise((r) => setTimeout(r, 5));
410
+ const e3 = await append(testDb.db, {
411
+ aggregateId: a3,
412
+ aggregateType: "task",
413
+ tenantId: tenantB,
414
+ expectedVersion: 0,
415
+ type: "task.created",
416
+ payload: { n: 3 },
417
+ metadata: { userId: userA },
418
+ });
419
+
420
+ const all = await loadAllEventsByType(testDb.db, "task");
421
+ expect(all.map((e) => e.id)).toEqual([e1.id, e2.id, e3.id]);
422
+ // createdAt strictly non-decreasing
423
+ for (let i = 1; i < all.length; i++) {
424
+ const prev = all[i - 1];
425
+ const cur = all[i];
426
+ if (!prev || !cur) throw new Error("unreachable");
427
+ expect(Temporal.Instant.compare(prev.createdAt, cur.createdAt)).toBeLessThanOrEqual(0);
428
+ }
429
+ });
430
+
431
+ test("returns empty array when no events of that type exist", async () => {
432
+ const events = await loadAllEventsByType(testDb.db, "nonexistent-type");
433
+ expect(events).toEqual([]);
434
+ });
435
+
436
+ test("includes every event of an aggregate — multiple versions in order", async () => {
437
+ // A single aggregate with multiple versions must appear in order in the
438
+ // replay stream, otherwise projection-apply sees events out-of-sequence.
439
+ const aggregateId = uuid();
440
+ for (let v = 0; v < 4; v++) {
441
+ await append(testDb.db, {
442
+ aggregateId,
443
+ aggregateType: "task",
444
+ tenantId: tenantA,
445
+ expectedVersion: v,
446
+ type: v === 0 ? "task.created" : "task.updated",
447
+ payload: { v },
448
+ metadata: { userId: userA },
449
+ });
450
+ }
451
+
452
+ const all = await loadAllEventsByType(testDb.db, "task");
453
+ expect(all).toHaveLength(4);
454
+ expect(all.map((e) => e.version)).toEqual([1, 2, 3, 4]);
455
+ expect(all.map((e) => (e.payload as { v: number }).v)).toEqual([0, 1, 2, 3]);
456
+ });
457
+ });
458
+
459
+ describe("event-store: streamAllEventsByType (memory-bounded iteration)", () => {
460
+ test("yields every event in id order across multiple batches", async () => {
461
+ // Seed 25 events; with batchSize=10 that's 3 batches (10+10+5).
462
+ // Verifies cursor advance (no skipping between batches) and final
463
+ // empty-batch termination.
464
+ for (let i = 0; i < 25; i++) {
465
+ await append(testDb.db, {
466
+ aggregateId: uuid(),
467
+ aggregateType: "stream-task",
468
+ tenantId: tenantA,
469
+ expectedVersion: 0,
470
+ type: "stream-task.created",
471
+ payload: { i },
472
+ metadata: { userId: userA },
473
+ });
474
+ }
475
+
476
+ const collected: Array<{ id: string; i: number }> = [];
477
+ for await (const event of streamAllEventsByType(testDb.db, "stream-task", 10)) {
478
+ collected.push({ id: event.id, i: (event.payload as { i: number }).i });
479
+ }
480
+
481
+ expect(collected).toHaveLength(25);
482
+ // Reihenfolge nach events.id (= chronological commit order).
483
+ expect(collected.map((c) => c.i)).toEqual(Array.from({ length: 25 }, (_, n) => n));
484
+ // ids strict aufsteigend (bigserial monotonic).
485
+ for (let i = 1; i < collected.length; i++) {
486
+ expect(BigInt(collected[i]!.id)).toBeGreaterThan(BigInt(collected[i - 1]!.id));
487
+ }
488
+ });
489
+
490
+ test("empty store yields nothing", async () => {
491
+ const collected: StoredEvent[] = [];
492
+ for await (const event of streamAllEventsByType(testDb.db, "nonexistent", 10)) {
493
+ collected.push(event);
494
+ }
495
+ expect(collected).toEqual([]);
496
+ });
497
+
498
+ test("filters by aggregateType — other types stay unstreamed", async () => {
499
+ await append(testDb.db, {
500
+ aggregateId: uuid(),
501
+ aggregateType: "stream-included",
502
+ tenantId: tenantA,
503
+ expectedVersion: 0,
504
+ type: "stream-included.x",
505
+ payload: {},
506
+ metadata: { userId: userA },
507
+ });
508
+ await append(testDb.db, {
509
+ aggregateId: uuid(),
510
+ aggregateType: "stream-excluded",
511
+ tenantId: tenantA,
512
+ expectedVersion: 0,
513
+ type: "stream-excluded.x",
514
+ payload: {},
515
+ metadata: { userId: userA },
516
+ });
517
+
518
+ const yielded: string[] = [];
519
+ for await (const event of streamAllEventsByType(testDb.db, "stream-included")) {
520
+ yielded.push(event.aggregateType);
521
+ }
522
+ expect(yielded).toEqual(["stream-included"]);
523
+ });
524
+
525
+ test("per-yield abort: stops at exactly the event after abort, regardless of batch size", async () => {
526
+ // Seed 25 events. Use batchSize=10 so an abort at length=5 lands
527
+ // mid-batch — verifies the per-yield check, not just batch-boundary
528
+ // semantics. The generator throws on its next yield after abort, so
529
+ // collected.length stays at 5.
530
+ for (let i = 0; i < 25; i++) {
531
+ await append(testDb.db, {
532
+ aggregateId: uuid(),
533
+ aggregateType: "stream-abort",
534
+ tenantId: tenantA,
535
+ expectedVersion: 0,
536
+ type: "stream-abort.x",
537
+ payload: { i },
538
+ metadata: { userId: userA },
539
+ });
540
+ }
541
+
542
+ const controller = new AbortController();
543
+ const collected: StoredEvent[] = [];
544
+
545
+ let thrown: unknown;
546
+ try {
547
+ for await (const event of streamAllEventsByType(
548
+ testDb.db,
549
+ "stream-abort",
550
+ 10,
551
+ controller.signal,
552
+ )) {
553
+ collected.push(event);
554
+ if (collected.length === 5) controller.abort();
555
+ }
556
+ } catch (e) {
557
+ thrown = e;
558
+ }
559
+
560
+ expect(collected.length).toBe(5);
561
+ expect(thrown).toBeInstanceOf(Error);
562
+ expect((thrown as Error).name).toBe("AbortError");
563
+ });
564
+
565
+ test("pre-aborted signal throws before any rows are fetched", async () => {
566
+ const controller = new AbortController();
567
+ controller.abort();
568
+
569
+ let thrown: unknown;
570
+ try {
571
+ for await (const _event of streamAllEventsByType(
572
+ testDb.db,
573
+ "stream-included",
574
+ 10,
575
+ controller.signal,
576
+ )) {
577
+ // unreachable
578
+ }
579
+ } catch (e) {
580
+ thrown = e;
581
+ }
582
+ expect((thrown as Error).name).toBe("AbortError");
583
+ });
584
+ });
@@ -0,0 +1,83 @@
1
+ // Block 0 perf probe — getStreamVersion is on the hot write-path: the CRUD
2
+ // executor calls it once per update/delete/restore to derive expectedVersion.
3
+ // A slow MAX(version) would regress every CRUD write on a hot aggregate.
4
+ //
5
+ // Claim: indexed lookup (events_aggregate_version_uq on (aggregate_id,
6
+ // version)) makes MAX(version) sub-ms even with thousands of events in the
7
+ // same stream.
8
+ //
9
+ // Not a strict SLA test — the threshold is generous enough to survive CI
10
+ // noise but tight enough that an index-miss regression would fail loudly.
11
+
12
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
13
+ import type { TenantId } from "../../engine/types";
14
+ import { createTestDb, type TestDb } from "../../stack";
15
+ import { generateId as uuid } from "../../utils";
16
+ import { append, createEventsTable, getStreamVersion } from "../index";
17
+
18
+ let testDb: TestDb;
19
+ const tenantId: TenantId = uuid();
20
+ const userId = uuid();
21
+
22
+ beforeAll(async () => {
23
+ testDb = await createTestDb();
24
+ await createEventsTable(testDb.db);
25
+ });
26
+
27
+ afterAll(async () => {
28
+ await testDb.cleanup();
29
+ });
30
+
31
+ async function seedStream(aggregateId: string, count: number): Promise<void> {
32
+ for (let v = 0; v < count; v++) {
33
+ await append(testDb.db, {
34
+ aggregateId,
35
+ aggregateType: "perfAgg",
36
+ tenantId,
37
+ expectedVersion: v,
38
+ type: "perfAgg.created",
39
+ payload: { seq: v },
40
+ metadata: { userId },
41
+ });
42
+ }
43
+ }
44
+
45
+ describe("event-store: getStreamVersion perf on hot streams", () => {
46
+ test("2000-event stream: MAX(version) stays under 30ms per call (indexed)", async () => {
47
+ const aggregateId = uuid();
48
+ await seedStream(aggregateId, 2000);
49
+
50
+ // Warm up — first query parses + plans.
51
+ await getStreamVersion(testDb.db, aggregateId, tenantId);
52
+
53
+ // Median of 50 calls to smooth CI noise. Each call is one indexed MAX.
54
+ const samples: number[] = [];
55
+ for (let i = 0; i < 50; i++) {
56
+ const start = performance.now();
57
+ const v = await getStreamVersion(testDb.db, aggregateId, tenantId);
58
+ samples.push(performance.now() - start);
59
+ expect(v).toBe(2000);
60
+ }
61
+
62
+ samples.sort((a, b) => a - b);
63
+ const p50 = samples[Math.floor(samples.length / 2)] ?? 0;
64
+ const p95 = samples[Math.floor(samples.length * 0.95)] ?? 0;
65
+
66
+ // Generous bound for CI — local runs typically see p50 < 1ms, p95 < 5ms.
67
+ // A regression to scan-instead-of-index would push p95 into tens of ms
68
+ // at 2000 rows and get worse linearly.
69
+ expect(p50).toBeLessThan(30);
70
+ expect(p95).toBeLessThan(50);
71
+ });
72
+
73
+ test("empty stream: returns 0 without full-table scan", async () => {
74
+ const aggregateId = uuid(); // never seeded
75
+ const start = performance.now();
76
+ const v = await getStreamVersion(testDb.db, aggregateId, tenantId);
77
+ const elapsed = performance.now() - start;
78
+
79
+ expect(v).toBe(0);
80
+ // Not a timing claim, just catch an accidental full-scan regression.
81
+ expect(elapsed).toBeLessThan(50);
82
+ });
83
+ });