@fragno-dev/db 0.3.0 → 0.4.1

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 (516) hide show
  1. package/.turbo/turbo-build.log +327 -160
  2. package/CHANGELOG.md +74 -0
  3. package/README.md +24 -0
  4. package/dist/adapters/adapters.d.ts +1 -1
  5. package/dist/adapters/adapters.d.ts.map +1 -1
  6. package/dist/adapters/adapters.js.map +1 -1
  7. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts +0 -3
  8. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts.map +1 -1
  9. package/dist/adapters/generic-sql/generic-sql-adapter.js +11 -12
  10. package/dist/adapters/generic-sql/generic-sql-adapter.js.map +1 -1
  11. package/dist/adapters/generic-sql/generic-sql-uow-executor.js +46 -6
  12. package/dist/adapters/generic-sql/generic-sql-uow-executor.js.map +1 -1
  13. package/dist/adapters/generic-sql/migration/cold-kysely.js.map +1 -1
  14. package/dist/adapters/generic-sql/migration/dialect/mysql.js +1 -1
  15. package/dist/adapters/generic-sql/migration/dialect/mysql.js.map +1 -1
  16. package/dist/adapters/generic-sql/migration/dialect/postgres.js +1 -1
  17. package/dist/adapters/generic-sql/migration/dialect/postgres.js.map +1 -1
  18. package/dist/adapters/generic-sql/migration/dialect/sqlite.js +185 -19
  19. package/dist/adapters/generic-sql/migration/dialect/sqlite.js.map +1 -1
  20. package/dist/adapters/generic-sql/migration/executor.d.ts.map +1 -1
  21. package/dist/adapters/generic-sql/migration/executor.js +30 -3
  22. package/dist/adapters/generic-sql/migration/executor.js.map +1 -1
  23. package/dist/adapters/generic-sql/migration/prepared-migrations.d.ts.map +1 -1
  24. package/dist/adapters/generic-sql/migration/prepared-migrations.js +3 -3
  25. package/dist/adapters/generic-sql/migration/prepared-migrations.js.map +1 -1
  26. package/dist/adapters/generic-sql/migration/sql-generator.js +1 -1
  27. package/dist/adapters/generic-sql/migration/sql-generator.js.map +1 -1
  28. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js +1 -1
  29. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js.map +1 -1
  30. package/dist/adapters/generic-sql/query/cursor-utils.js.map +1 -1
  31. package/dist/adapters/generic-sql/query/db-now-sql.js +27 -0
  32. package/dist/adapters/generic-sql/query/db-now-sql.js.map +1 -0
  33. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js +9 -6
  34. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js.map +1 -1
  35. package/dist/adapters/generic-sql/query/select-builder.js.map +1 -1
  36. package/dist/adapters/generic-sql/query/sql-query-compiler.js +37 -9
  37. package/dist/adapters/generic-sql/query/sql-query-compiler.js.map +1 -1
  38. package/dist/adapters/generic-sql/query/where-builder.js +24 -20
  39. package/dist/adapters/generic-sql/query/where-builder.js.map +1 -1
  40. package/dist/adapters/generic-sql/uow-decoder.js +1 -1
  41. package/dist/adapters/generic-sql/uow-decoder.js.map +1 -1
  42. package/dist/adapters/generic-sql/uow-encoder.js +8 -9
  43. package/dist/adapters/generic-sql/uow-encoder.js.map +1 -1
  44. package/dist/adapters/in-memory/condition-evaluator.js +10 -6
  45. package/dist/adapters/in-memory/condition-evaluator.js.map +1 -1
  46. package/dist/adapters/in-memory/in-memory-adapter.d.ts.map +1 -1
  47. package/dist/adapters/in-memory/in-memory-adapter.js +45 -25
  48. package/dist/adapters/in-memory/in-memory-adapter.js.map +1 -1
  49. package/dist/adapters/in-memory/in-memory-uow.js +236 -13
  50. package/dist/adapters/in-memory/in-memory-uow.js.map +1 -1
  51. package/dist/adapters/in-memory/options.d.ts +2 -0
  52. package/dist/adapters/in-memory/options.d.ts.map +1 -1
  53. package/dist/adapters/in-memory/options.js +3 -2
  54. package/dist/adapters/in-memory/options.js.map +1 -1
  55. package/dist/adapters/in-memory/reference-resolution.js.map +1 -1
  56. package/dist/adapters/in-memory/store.js +1 -1
  57. package/dist/adapters/in-memory/store.js.map +1 -1
  58. package/dist/adapters/shared/from-unit-of-work-compiler.js +51 -24
  59. package/dist/adapters/shared/from-unit-of-work-compiler.js.map +1 -1
  60. package/dist/adapters/shared/uow-operation-compiler.js.map +1 -1
  61. package/dist/browser/adapters/adapters.d.ts +61 -0
  62. package/dist/browser/adapters/adapters.d.ts.map +1 -0
  63. package/dist/browser/adapters/generic-sql/migration/executor.d.ts +15 -0
  64. package/dist/browser/adapters/generic-sql/migration/executor.d.ts.map +1 -0
  65. package/dist/browser/adapters/generic-sql/migration/prepared-migrations.d.ts +66 -0
  66. package/dist/browser/adapters/generic-sql/migration/prepared-migrations.d.ts.map +1 -0
  67. package/dist/browser/adapters/generic-sql/sqlite-storage.d.ts +11 -0
  68. package/dist/browser/adapters/generic-sql/sqlite-storage.d.ts.map +1 -0
  69. package/dist/browser/adapters/in-memory/in-memory-adapter.d.ts +5 -0
  70. package/dist/browser/adapters/in-memory/index.d.ts +2 -0
  71. package/dist/browser/adapters/in-memory/options.d.ts +1 -0
  72. package/dist/browser/db-fragment-definition-builder.d.ts +237 -0
  73. package/dist/browser/db-fragment-definition-builder.d.ts.map +1 -0
  74. package/dist/browser/durable-hooks.d.ts +3 -0
  75. package/dist/browser/fragments/internal-fragment.d.ts +317 -0
  76. package/dist/browser/fragments/internal-fragment.d.ts.map +1 -0
  77. package/dist/browser/fragments/internal-fragment.schema.d.ts +1 -0
  78. package/dist/browser/hooks/durable-hooks-logger.d.ts +10 -0
  79. package/dist/browser/hooks/durable-hooks-logger.d.ts.map +1 -0
  80. package/dist/browser/hooks/hooks.d.ts +146 -0
  81. package/dist/browser/hooks/hooks.d.ts.map +1 -0
  82. package/dist/browser/id.js +1 -0
  83. package/dist/browser/internal/adapter-registry.d.ts +4 -0
  84. package/dist/browser/internal/outbox-state.d.ts +2 -0
  85. package/dist/browser/mod.d.ts +15 -0
  86. package/dist/browser/mod.d.ts.map +1 -0
  87. package/dist/browser/mod.js +17 -0
  88. package/dist/browser/mod.js.map +1 -0
  89. package/dist/browser/mod2.d.ts +48 -0
  90. package/dist/browser/mod2.d.ts.map +1 -0
  91. package/dist/browser/naming/sql-naming.d.ts +19 -0
  92. package/dist/browser/naming/sql-naming.d.ts.map +1 -0
  93. package/dist/browser/outbox/outbox.d.ts +21 -0
  94. package/dist/browser/outbox/outbox.d.ts.map +1 -0
  95. package/dist/browser/query/column-defaults.js +1 -0
  96. package/dist/browser/query/condition-builder.d.ts +44 -0
  97. package/dist/browser/query/condition-builder.d.ts.map +1 -0
  98. package/dist/browser/query/condition-builder.js +97 -0
  99. package/dist/browser/query/condition-builder.js.map +1 -0
  100. package/dist/browser/query/cursor.d.ts +105 -0
  101. package/dist/browser/query/cursor.d.ts.map +1 -0
  102. package/dist/browser/query/cursor.js +150 -0
  103. package/dist/browser/query/cursor.js.map +1 -0
  104. package/dist/browser/query/db-now.d.ts +22 -0
  105. package/dist/browser/query/db-now.d.ts.map +1 -0
  106. package/dist/browser/query/db-now.js +33 -0
  107. package/dist/browser/query/db-now.js.map +1 -0
  108. package/dist/browser/query/orm/orm.d.ts +18 -0
  109. package/dist/browser/query/orm/orm.d.ts.map +1 -0
  110. package/dist/browser/query/simple-query-interface.d.ts +108 -0
  111. package/dist/browser/query/simple-query-interface.d.ts.map +1 -0
  112. package/dist/browser/query/unit-of-work/execute-unit-of-work.d.ts +423 -0
  113. package/dist/browser/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -0
  114. package/dist/browser/query/unit-of-work/execute-unit-of-work.js +507 -0
  115. package/dist/browser/query/unit-of-work/execute-unit-of-work.js.map +1 -0
  116. package/dist/browser/query/unit-of-work/retry-policy.d.ts +23 -0
  117. package/dist/browser/query/unit-of-work/retry-policy.d.ts.map +1 -0
  118. package/dist/browser/query/unit-of-work/retry-policy.js +40 -0
  119. package/dist/browser/query/unit-of-work/retry-policy.js.map +1 -0
  120. package/dist/browser/query/unit-of-work/unit-of-work.d.ts +703 -0
  121. package/dist/browser/query/unit-of-work/unit-of-work.d.ts.map +1 -0
  122. package/dist/browser/query/unit-of-work/unit-of-work.js +1206 -0
  123. package/dist/browser/query/unit-of-work/unit-of-work.js.map +1 -0
  124. package/dist/browser/query/value-encoding.js +38 -0
  125. package/dist/browser/query/value-encoding.js.map +1 -0
  126. package/dist/browser/schema/create.d.ts +326 -0
  127. package/dist/browser/schema/create.d.ts.map +1 -0
  128. package/dist/browser/schema/create.js +89 -0
  129. package/dist/browser/schema/create.js.map +1 -0
  130. package/dist/browser/schema/generate-id.js +28 -0
  131. package/dist/browser/schema/generate-id.js.map +1 -0
  132. package/dist/browser/shared/providers.d.ts +6 -0
  133. package/dist/browser/shared/providers.d.ts.map +1 -0
  134. package/dist/browser/sql-driver/connection/connection-provider.d.ts +13 -0
  135. package/dist/browser/sql-driver/connection/connection-provider.d.ts.map +1 -0
  136. package/dist/browser/sql-driver/dialect-adapter/dialect-adapter.d.ts +7 -0
  137. package/dist/browser/sql-driver/dialect-adapter/dialect-adapter.d.ts.map +1 -0
  138. package/dist/browser/sql-driver/driver/runtime-driver.d.ts +23 -0
  139. package/dist/browser/sql-driver/driver/runtime-driver.d.ts.map +1 -0
  140. package/dist/browser/sql-driver/query-executor/plugin.d.ts +17 -0
  141. package/dist/browser/sql-driver/query-executor/plugin.d.ts.map +1 -0
  142. package/dist/browser/sql-driver/query-executor/query-executor.d.ts +36 -0
  143. package/dist/browser/sql-driver/query-executor/query-executor.d.ts.map +1 -0
  144. package/dist/browser/sql-driver/sql-driver-adapter.d.ts +29 -0
  145. package/dist/browser/sql-driver/sql-driver-adapter.d.ts.map +1 -0
  146. package/dist/browser/sql-driver/sql-driver.d.ts +38 -0
  147. package/dist/browser/sql-driver/sql-driver.d.ts.map +1 -0
  148. package/dist/browser/sync/commands.d.ts +15 -0
  149. package/dist/browser/sync/commands.d.ts.map +1 -0
  150. package/dist/browser/sync/commands.js +27 -0
  151. package/dist/browser/sync/commands.js.map +1 -0
  152. package/dist/browser/sync/types.d.ts +63 -0
  153. package/dist/browser/sync/types.d.ts.map +1 -0
  154. package/dist/browser/util/types.d.ts +8 -0
  155. package/dist/browser/util/types.d.ts.map +1 -0
  156. package/dist/browser/with-database.d.ts +29 -0
  157. package/dist/browser/with-database.d.ts.map +1 -0
  158. package/dist/client.d.ts +4 -0
  159. package/dist/client.js +5 -0
  160. package/dist/db-fragment-definition-builder.d.ts +85 -28
  161. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  162. package/dist/db-fragment-definition-builder.js +374 -46
  163. package/dist/db-fragment-definition-builder.js.map +1 -1
  164. package/dist/dispatchers/cloudflare-do/dispatcher.d.ts +20 -0
  165. package/dist/dispatchers/cloudflare-do/dispatcher.d.ts.map +1 -0
  166. package/dist/dispatchers/cloudflare-do/dispatcher.js +147 -0
  167. package/dist/dispatchers/cloudflare-do/dispatcher.js.map +1 -0
  168. package/dist/dispatchers/cloudflare-do/index.d.ts +5 -20
  169. package/dist/dispatchers/cloudflare-do/index.d.ts.map +1 -1
  170. package/dist/dispatchers/cloudflare-do/index.js +23 -55
  171. package/dist/dispatchers/cloudflare-do/index.js.map +1 -1
  172. package/dist/dispatchers/node/dispatcher.d.ts +14 -0
  173. package/dist/dispatchers/node/dispatcher.d.ts.map +1 -0
  174. package/dist/dispatchers/node/dispatcher.js +80 -0
  175. package/dist/dispatchers/node/dispatcher.js.map +1 -0
  176. package/dist/dispatchers/node/index.d.ts +5 -10
  177. package/dist/dispatchers/node/index.d.ts.map +1 -1
  178. package/dist/dispatchers/node/index.js +21 -53
  179. package/dist/dispatchers/node/index.js.map +1 -1
  180. package/dist/durable-hooks.d.ts +31 -0
  181. package/dist/durable-hooks.d.ts.map +1 -0
  182. package/dist/durable-hooks.js +23 -0
  183. package/dist/durable-hooks.js.map +1 -0
  184. package/dist/fragments/internal-fragment.d.ts +128 -27
  185. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  186. package/dist/fragments/internal-fragment.js +125 -78
  187. package/dist/fragments/internal-fragment.js.map +1 -1
  188. package/dist/fragments/internal-fragment.routes.js +138 -3
  189. package/dist/fragments/internal-fragment.routes.js.map +1 -1
  190. package/dist/fragments/internal-fragment.schema.d.ts +7 -1
  191. package/dist/fragments/internal-fragment.schema.d.ts.map +1 -1
  192. package/dist/fragments/internal-fragment.schema.js +18 -1
  193. package/dist/fragments/internal-fragment.schema.js.map +1 -1
  194. package/dist/hooks/durable-hooks-logger.d.ts +10 -0
  195. package/dist/hooks/durable-hooks-logger.d.ts.map +1 -0
  196. package/dist/hooks/durable-hooks-logger.js +75 -0
  197. package/dist/hooks/durable-hooks-logger.js.map +1 -0
  198. package/dist/hooks/durable-hooks-processor.d.ts +1 -14
  199. package/dist/hooks/durable-hooks-processor.js +58 -10
  200. package/dist/hooks/durable-hooks-processor.js.map +1 -1
  201. package/dist/hooks/durable-hooks-runtime.js +44 -0
  202. package/dist/hooks/durable-hooks-runtime.js.map +1 -0
  203. package/dist/hooks/hooks.d.ts +60 -2
  204. package/dist/hooks/hooks.d.ts.map +1 -1
  205. package/dist/hooks/hooks.js +214 -53
  206. package/dist/hooks/hooks.js.map +1 -1
  207. package/dist/id.d.ts +2 -2
  208. package/dist/id.js +2 -2
  209. package/dist/internal/adapter-registry.d.ts +11 -0
  210. package/dist/internal/adapter-registry.d.ts.map +1 -0
  211. package/dist/internal/adapter-registry.js +135 -0
  212. package/dist/internal/adapter-registry.js.map +1 -0
  213. package/dist/internal/outbox-state.d.ts +2 -0
  214. package/dist/internal/outbox-state.js +26 -0
  215. package/dist/internal/outbox-state.js.map +1 -0
  216. package/dist/migration-engine/auto-from-schema.d.ts +33 -0
  217. package/dist/migration-engine/auto-from-schema.d.ts.map +1 -0
  218. package/dist/migration-engine/auto-from-schema.js +210 -27
  219. package/dist/migration-engine/auto-from-schema.js.map +1 -1
  220. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  221. package/dist/migration-engine/generation-engine.js +17 -5
  222. package/dist/migration-engine/generation-engine.js.map +1 -1
  223. package/dist/migration-engine/shared.d.ts +113 -0
  224. package/dist/migration-engine/shared.d.ts.map +1 -0
  225. package/dist/migration-engine/shared.js.map +1 -1
  226. package/dist/mod.d.ts +12 -11
  227. package/dist/mod.d.ts.map +1 -1
  228. package/dist/mod.js +10 -10
  229. package/dist/mod.js.map +1 -1
  230. package/dist/naming/sql-naming.d.ts.map +1 -1
  231. package/dist/naming/sql-naming.js.map +1 -1
  232. package/dist/outbox/outbox-builder.js.map +1 -1
  233. package/dist/outbox/outbox.d.ts +3 -1
  234. package/dist/outbox/outbox.d.ts.map +1 -1
  235. package/dist/outbox/outbox.js.map +1 -1
  236. package/dist/query/column-defaults.js.map +1 -1
  237. package/dist/query/condition-builder.d.ts +7 -1
  238. package/dist/query/condition-builder.d.ts.map +1 -1
  239. package/dist/query/condition-builder.js +5 -1
  240. package/dist/query/condition-builder.js.map +1 -1
  241. package/dist/query/cursor-client.d.ts +105 -0
  242. package/dist/query/cursor-client.d.ts.map +1 -0
  243. package/dist/query/cursor-client.js +165 -0
  244. package/dist/query/cursor-client.js.map +1 -0
  245. package/dist/query/cursor.d.ts.map +1 -1
  246. package/dist/query/cursor.js +7 -1
  247. package/dist/query/cursor.js.map +1 -1
  248. package/dist/query/db-now.d.ts +15 -1
  249. package/dist/query/db-now.d.ts.map +1 -1
  250. package/dist/query/db-now.js +30 -2
  251. package/dist/query/db-now.js.map +1 -1
  252. package/dist/query/orm/orm.js.map +1 -1
  253. package/dist/query/serialize/create-sql-serializer.js +2 -2
  254. package/dist/query/serialize/create-sql-serializer.js.map +1 -1
  255. package/dist/query/serialize/dialect/mysql-serializer.js.map +1 -1
  256. package/dist/query/serialize/dialect/postgres-serializer.js.map +1 -1
  257. package/dist/query/serialize/dialect/sqlite-serializer.js +6 -2
  258. package/dist/query/serialize/dialect/sqlite-serializer.js.map +1 -1
  259. package/dist/query/simple-query-interface.d.ts +7 -3
  260. package/dist/query/simple-query-interface.d.ts.map +1 -1
  261. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +37 -2
  262. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
  263. package/dist/query/unit-of-work/execute-unit-of-work.js +39 -18
  264. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  265. package/dist/query/unit-of-work/unit-of-work.d.ts +42 -16
  266. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  267. package/dist/query/unit-of-work/unit-of-work.js +50 -6
  268. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  269. package/dist/query/value-decoding.js +8 -1
  270. package/dist/query/value-decoding.js.map +1 -1
  271. package/dist/query/value-encoding.js.map +1 -1
  272. package/dist/schema/create.d.ts +69 -25
  273. package/dist/schema/create.d.ts.map +1 -1
  274. package/dist/schema/create.js +91 -16
  275. package/dist/schema/create.js.map +1 -1
  276. package/dist/schema/type-conversion/create-sql-type-mapper.js +1 -1
  277. package/dist/schema/type-conversion/create-sql-type-mapper.js.map +1 -1
  278. package/dist/schema/type-conversion/dialect/sqlite.js.map +1 -1
  279. package/dist/schema/validator.d.ts.map +1 -1
  280. package/dist/schema/validator.js.map +1 -1
  281. package/dist/schema-output/drizzle.d.ts.map +1 -1
  282. package/dist/schema-output/drizzle.js +8 -6
  283. package/dist/schema-output/drizzle.js.map +1 -1
  284. package/dist/schema-output/prisma.d.ts.map +1 -1
  285. package/dist/schema-output/prisma.js +21 -10
  286. package/dist/schema-output/prisma.js.map +1 -1
  287. package/dist/sql-driver/dialects/durable-object-dialect.js +3 -9
  288. package/dist/sql-driver/dialects/durable-object-dialect.js.map +1 -1
  289. package/dist/sql-driver/query-executor/default-query-executor.js.map +1 -1
  290. package/dist/sql-driver/query-executor/query-executor-base.js.map +1 -1
  291. package/dist/sql-driver/sql-driver-adapter.js.map +1 -1
  292. package/dist/sql-driver/sql.js.map +1 -1
  293. package/dist/sync/commands.d.ts +15 -0
  294. package/dist/sync/commands.d.ts.map +1 -0
  295. package/dist/sync/commands.js +27 -0
  296. package/dist/sync/commands.js.map +1 -0
  297. package/dist/sync/index.d.ts +4 -0
  298. package/dist/sync/index.js +4 -0
  299. package/dist/sync/read-tracking.d.ts +25 -0
  300. package/dist/sync/read-tracking.d.ts.map +1 -0
  301. package/dist/sync/read-tracking.js +148 -0
  302. package/dist/sync/read-tracking.js.map +1 -0
  303. package/dist/sync/submit.js +213 -0
  304. package/dist/sync/submit.js.map +1 -0
  305. package/dist/sync/types.d.ts +63 -0
  306. package/dist/sync/types.d.ts.map +1 -0
  307. package/dist/util/default-database-adapter.js +6 -1
  308. package/dist/util/default-database-adapter.js.map +1 -1
  309. package/dist/with-database.d.ts +3 -6
  310. package/dist/with-database.d.ts.map +1 -1
  311. package/dist/with-database.js +7 -15
  312. package/dist/with-database.js.map +1 -1
  313. package/package.json +33 -41
  314. package/src/adapters/adapters.ts +5 -4
  315. package/src/adapters/drizzle/migrate-drizzle.test.ts +46 -9
  316. package/src/adapters/drizzle/migration-parity-drizzle-kit.test.ts +5 -3
  317. package/src/adapters/drizzle/test-utils.ts +2 -1
  318. package/src/adapters/generic-sql/generic-sql-adapter.test.ts +5 -3
  319. package/src/adapters/generic-sql/generic-sql-adapter.ts +21 -24
  320. package/src/adapters/generic-sql/generic-sql-uow-executor.test.ts +1 -0
  321. package/src/adapters/generic-sql/generic-sql-uow-executor.ts +81 -15
  322. package/src/adapters/generic-sql/migration/adapter-migration-parity.test.ts +4 -2
  323. package/src/adapters/generic-sql/migration/cold-kysely.ts +1 -0
  324. package/src/adapters/generic-sql/migration/dialect/mysql.test.ts +3 -2
  325. package/src/adapters/generic-sql/migration/dialect/mysql.ts +1 -0
  326. package/src/adapters/generic-sql/migration/dialect/postgres.test.ts +5 -4
  327. package/src/adapters/generic-sql/migration/dialect/postgres.ts +2 -1
  328. package/src/adapters/generic-sql/migration/dialect/sqlite.test.ts +795 -3
  329. package/src/adapters/generic-sql/migration/dialect/sqlite.ts +385 -57
  330. package/src/adapters/generic-sql/migration/executor.test.ts +52 -0
  331. package/src/adapters/generic-sql/migration/executor.ts +47 -4
  332. package/src/adapters/generic-sql/migration/prepared-migrations.test.ts +117 -14
  333. package/src/adapters/generic-sql/migration/prepared-migrations.ts +9 -8
  334. package/src/adapters/generic-sql/migration/sql-generator.ts +5 -3
  335. package/src/adapters/generic-sql/query/create-sql-query-compiler.ts +3 -3
  336. package/src/adapters/generic-sql/query/cursor-utils.test.ts +3 -2
  337. package/src/adapters/generic-sql/query/cursor-utils.ts +1 -1
  338. package/src/adapters/generic-sql/query/db-now-sql.ts +49 -0
  339. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.test.ts +144 -8
  340. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.ts +16 -17
  341. package/src/adapters/generic-sql/query/select-builder.test.ts +1 -0
  342. package/src/adapters/generic-sql/query/select-builder.ts +2 -2
  343. package/src/adapters/generic-sql/query/sql-query-compiler.test.ts +24 -5
  344. package/src/adapters/generic-sql/query/sql-query-compiler.ts +83 -13
  345. package/src/adapters/generic-sql/query/where-builder.test.ts +7 -5
  346. package/src/adapters/generic-sql/query/where-builder.ts +48 -29
  347. package/src/adapters/generic-sql/sql-adapter-pglite-migrations.test.ts +6 -15
  348. package/src/adapters/generic-sql/sql-adapter-pglite-pagination.test.ts +52 -7
  349. package/src/adapters/generic-sql/sql-adapter-pglite-queries.test.ts +9 -6
  350. package/src/adapters/generic-sql/sql-adapter-sqlite3-driver.test.ts +273 -5
  351. package/src/adapters/generic-sql/sql-adapter-sqlite3-uow.test.ts +123 -6
  352. package/src/adapters/generic-sql/sql-adapter-sqlocal.test.ts +4 -2
  353. package/src/adapters/generic-sql/uow-decoder.test.ts +4 -3
  354. package/src/adapters/generic-sql/uow-decoder.ts +3 -3
  355. package/src/adapters/generic-sql/uow-encoder.test.ts +4 -2
  356. package/src/adapters/generic-sql/uow-encoder.ts +14 -18
  357. package/src/adapters/in-memory/condition-evaluator.test.ts +2 -1
  358. package/src/adapters/in-memory/condition-evaluator.ts +9 -4
  359. package/src/adapters/in-memory/in-memory-adapter.ts +155 -44
  360. package/src/adapters/in-memory/in-memory-uow.mutations.test.ts +50 -2
  361. package/src/adapters/in-memory/in-memory-uow.retrieval.test.ts +158 -3
  362. package/src/adapters/in-memory/in-memory-uow.ts +402 -26
  363. package/src/adapters/in-memory/options.test.ts +1 -0
  364. package/src/adapters/in-memory/options.ts +5 -1
  365. package/src/adapters/in-memory/outbox.test.ts +361 -0
  366. package/src/adapters/in-memory/reference-resolution.test.ts +3 -2
  367. package/src/adapters/in-memory/reference-resolution.ts +2 -2
  368. package/src/adapters/in-memory/sorted-array-index.test.ts +1 -0
  369. package/src/adapters/in-memory/store.test.ts +1 -0
  370. package/src/adapters/in-memory/store.ts +3 -3
  371. package/src/adapters/in-memory/value-normalization.test.ts +1 -0
  372. package/src/adapters/prisma/prisma-adapter-sqlite3.test.ts +51 -7
  373. package/src/adapters/shared/from-unit-of-work-compiler.ts +156 -46
  374. package/src/adapters/shared/uow-operation-compiler.ts +3 -3
  375. package/src/browser/mod.ts +64 -0
  376. package/src/client.ts +19 -0
  377. package/src/db-fragment-definition-builder.test.ts +821 -47
  378. package/src/db-fragment-definition-builder.ts +857 -110
  379. package/src/db-fragment-instantiator.test.ts +114 -90
  380. package/src/db-fragment-integration.test.ts +9 -6
  381. package/src/dispatchers/cloudflare-do/dispatcher.ts +204 -0
  382. package/src/dispatchers/cloudflare-do/index.test.ts +145 -12
  383. package/src/dispatchers/cloudflare-do/index.ts +49 -90
  384. package/src/dispatchers/node/dispatcher.ts +112 -0
  385. package/src/dispatchers/node/index.test.ts +43 -14
  386. package/src/dispatchers/node/index.ts +38 -75
  387. package/src/durable-hooks.test.ts +80 -0
  388. package/src/durable-hooks.ts +67 -0
  389. package/src/fragments/internal-fragment.routes.test.ts +570 -0
  390. package/src/fragments/internal-fragment.routes.ts +297 -5
  391. package/src/fragments/internal-fragment.schema.ts +45 -1
  392. package/src/fragments/internal-fragment.test.ts +223 -251
  393. package/src/fragments/internal-fragment.ts +278 -154
  394. package/src/hooks/durable-hooks-logger.ts +126 -0
  395. package/src/hooks/durable-hooks-processor.pglite.test.ts +87 -0
  396. package/src/hooks/durable-hooks-processor.test.ts +179 -14
  397. package/src/hooks/durable-hooks-processor.ts +120 -14
  398. package/src/hooks/durable-hooks-runtime.test.ts +65 -0
  399. package/src/hooks/durable-hooks-runtime.ts +81 -0
  400. package/src/hooks/hooks.test.ts +314 -53
  401. package/src/hooks/hooks.ts +360 -81
  402. package/src/id.test.ts +34 -0
  403. package/src/id.ts +1 -3
  404. package/src/internal/adapter-registry.test.ts +93 -0
  405. package/src/internal/adapter-registry.ts +239 -0
  406. package/src/internal/outbox-state.ts +43 -0
  407. package/src/migration-engine/auto-from-schema.test.ts +93 -0
  408. package/src/migration-engine/auto-from-schema.ts +360 -42
  409. package/src/migration-engine/create.test.ts +2 -1
  410. package/src/migration-engine/create.ts +1 -1
  411. package/src/migration-engine/generation-engine.test.ts +66 -9
  412. package/src/migration-engine/generation-engine.ts +31 -10
  413. package/src/migration-engine/shared.ts +13 -0
  414. package/src/mod.ts +45 -27
  415. package/src/naming/sql-naming.ts +1 -0
  416. package/src/outbox/outbox-builder.ts +2 -2
  417. package/src/outbox/outbox.test.ts +216 -45
  418. package/src/outbox/outbox.ts +3 -1
  419. package/src/query/column-defaults.ts +1 -1
  420. package/src/query/condition-builder.test.ts +15 -0
  421. package/src/query/condition-builder.ts +7 -0
  422. package/src/query/cursor-client.test.ts +70 -0
  423. package/src/query/cursor-client.ts +263 -0
  424. package/src/query/cursor.test.ts +3 -2
  425. package/src/query/cursor.ts +15 -3
  426. package/src/query/db-now.ts +69 -2
  427. package/src/query/orm/orm.ts +2 -2
  428. package/src/query/query-type.test.ts +2 -1
  429. package/src/query/serialize/create-sql-serializer.ts +3 -3
  430. package/src/query/serialize/dialect/mysql-serializer.ts +1 -1
  431. package/src/query/serialize/dialect/postgres-serializer.ts +1 -1
  432. package/src/query/serialize/dialect/sqlite-serializer.test.ts +39 -2
  433. package/src/query/serialize/dialect/sqlite-serializer.ts +18 -5
  434. package/src/query/simple-query-interface.ts +10 -4
  435. package/src/query/unit-of-work/execute-unit-of-work.test.ts +347 -9
  436. package/src/query/unit-of-work/execute-unit-of-work.ts +63 -20
  437. package/src/query/unit-of-work/retry-policy.test.ts +1 -0
  438. package/src/query/unit-of-work/tx-builder.test.ts +73 -1
  439. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +5 -4
  440. package/src/query/unit-of-work/unit-of-work-types.test.ts +41 -11
  441. package/src/query/unit-of-work/unit-of-work.test.ts +28 -2
  442. package/src/query/unit-of-work/unit-of-work.ts +105 -19
  443. package/src/query/value-decoding.test.ts +50 -2
  444. package/src/query/value-decoding.ts +17 -4
  445. package/src/query/value-encoding.test.ts +1 -0
  446. package/src/query/value-encoding.ts +1 -1
  447. package/src/schema/create.test.ts +164 -5
  448. package/src/schema/create.ts +222 -24
  449. package/src/schema/generate-id.test.ts +1 -0
  450. package/src/schema/serialize.test.ts +4 -3
  451. package/src/schema/type-conversion/create-sql-type-mapper.ts +1 -1
  452. package/src/schema/type-conversion/dialect/sqlite.ts +2 -2
  453. package/src/schema/type-conversion/type-mapping.test.ts +2 -1
  454. package/src/schema/validator.test.ts +4 -2
  455. package/src/schema/validator.ts +1 -0
  456. package/src/schema-output/drizzle.test.ts +72 -19
  457. package/src/schema-output/drizzle.ts +24 -18
  458. package/src/schema-output/prisma.test.ts +172 -14
  459. package/src/schema-output/prisma.ts +34 -14
  460. package/src/sql-driver/better-sqlite3.test.ts +5 -3
  461. package/src/sql-driver/dialects/durable-object-dialect.ts +3 -8
  462. package/src/sql-driver/query-executor/default-query-executor.ts +1 -1
  463. package/src/sql-driver/query-executor/query-executor-base.ts +1 -1
  464. package/src/sql-driver/query-executor/query-executor.ts +1 -1
  465. package/src/sql-driver/sql-driver-adapter.ts +2 -2
  466. package/src/sql-driver/sql.ts +2 -1
  467. package/src/sql-driver/sqlocal.test.ts +4 -2
  468. package/src/sync/commands.test.ts +39 -0
  469. package/src/sync/commands.ts +51 -0
  470. package/src/sync/conflict-checker.test.ts +450 -0
  471. package/src/sync/conflict-checker.ts +248 -0
  472. package/src/sync/index.ts +14 -0
  473. package/src/sync/plan.ts +9 -0
  474. package/src/sync/read-tracking.test.ts +177 -0
  475. package/src/sync/read-tracking.ts +287 -0
  476. package/src/sync/submit.test.ts +205 -0
  477. package/src/sync/submit.ts +328 -0
  478. package/src/sync/types.ts +80 -0
  479. package/src/util/default-database-adapter.ts +15 -2
  480. package/src/with-database.ts +20 -50
  481. package/tsconfig.json +1 -1
  482. package/tsdown.config.ts +38 -26
  483. package/vitest.config.ts +1 -0
  484. package/dist/hooks/durable-hooks-processor.d.ts.map +0 -1
  485. package/dist/node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/index.js +0 -168
  486. package/dist/node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/index.js.map +0 -1
  487. package/dist/packages/fragno/dist/api/bind-services.js +0 -20
  488. package/dist/packages/fragno/dist/api/bind-services.js.map +0 -1
  489. package/dist/packages/fragno/dist/api/error.js +0 -48
  490. package/dist/packages/fragno/dist/api/error.js.map +0 -1
  491. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +0 -321
  492. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +0 -1
  493. package/dist/packages/fragno/dist/api/fragment-instantiator.js +0 -669
  494. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +0 -1
  495. package/dist/packages/fragno/dist/api/fragno-response.js +0 -73
  496. package/dist/packages/fragno/dist/api/fragno-response.js.map +0 -1
  497. package/dist/packages/fragno/dist/api/internal/response-stream.js +0 -81
  498. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +0 -1
  499. package/dist/packages/fragno/dist/api/internal/route.js +0 -10
  500. package/dist/packages/fragno/dist/api/internal/route.js.map +0 -1
  501. package/dist/packages/fragno/dist/api/mutable-request-state.js +0 -97
  502. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +0 -1
  503. package/dist/packages/fragno/dist/api/request-context-storage.js +0 -43
  504. package/dist/packages/fragno/dist/api/request-context-storage.js.map +0 -1
  505. package/dist/packages/fragno/dist/api/request-input-context.js +0 -185
  506. package/dist/packages/fragno/dist/api/request-input-context.js.map +0 -1
  507. package/dist/packages/fragno/dist/api/request-middleware.js +0 -83
  508. package/dist/packages/fragno/dist/api/request-middleware.js.map +0 -1
  509. package/dist/packages/fragno/dist/api/request-output-context.js +0 -119
  510. package/dist/packages/fragno/dist/api/request-output-context.js.map +0 -1
  511. package/dist/packages/fragno/dist/api/route.js +0 -30
  512. package/dist/packages/fragno/dist/api/route.js.map +0 -1
  513. package/dist/packages/fragno/dist/internal/symbols.js +0 -10
  514. package/dist/packages/fragno/dist/internal/symbols.js.map +0 -1
  515. package/dist/packages/fragno/dist/internal/trace-context.js +0 -12
  516. package/dist/packages/fragno/dist/internal/trace-context.js.map +0 -1
@@ -1,36 +1,107 @@
1
- import type { AnySchema } from "./schema/create";
2
- import type { SimpleQueryInterface } from "./query/simple-query-interface";
3
- import type { DatabaseAdapter } from "./adapters/adapters";
4
- import type { IUnitOfWork } from "./query/unit-of-work/unit-of-work";
1
+ import { FragnoApiError } from "@fragno-dev/core/api";
2
+
5
3
  import type {
6
4
  RequestThisContext,
7
5
  FragnoPublicConfig,
8
- AnyFragnoInstantiatedFragment,
6
+ AnyRouteOrFactory,
7
+ FragnoRouteConfig,
8
+ BoundServices,
9
9
  } from "@fragno-dev/core";
10
10
  import {
11
11
  FragmentDefinitionBuilder,
12
12
  type FragmentDefinition,
13
13
  type ServiceConstructorFn,
14
14
  } from "@fragno-dev/core";
15
+
16
+ import type { DatabaseAdapter, DatabaseContextStorage } from "./adapters/adapters";
17
+ import type { InternalFragmentInstance } from "./fragments/internal-fragment";
18
+ import { DurableHooksLogger } from "./hooks/durable-hooks-logger";
15
19
  import {
16
- createServiceTxBuilder,
17
- createHandlerTxBuilder,
18
- ServiceTxBuilder,
19
- HandlerTxBuilder,
20
- type ExecuteTxOptions,
21
- } from "./query/unit-of-work/execute-unit-of-work";
20
+ getDurableHooksRuntimeByConfig,
21
+ getDurableHooksRuntimeByNamespace,
22
+ registerDurableHooksRuntime,
23
+ } from "./hooks/durable-hooks-runtime";
22
24
  import {
23
25
  prepareHookMutations,
24
26
  type HooksMap,
25
27
  type HookFn,
26
28
  type HookContext,
27
29
  type HookProcessorConfig,
30
+ type HookNotifier,
31
+ type HookNotifyContext,
28
32
  type DurableHooksProcessingOptions,
29
- createHookScheduler,
33
+ createDurableHooksRunner,
30
34
  } from "./hooks/hooks";
31
- import type { InternalFragmentInstance } from "./fragments/internal-fragment";
32
- import { resolveDatabaseAdapter } from "./util/default-database-adapter";
33
35
  import { sanitizeNamespace } from "./naming/sql-naming";
36
+ import type { SimpleQueryInterface } from "./query/simple-query-interface";
37
+ import {
38
+ createServiceTxBuilder,
39
+ createHandlerTxBuilder,
40
+ ServiceTxBuilder,
41
+ HandlerTxBuilder,
42
+ type AwaitedPromisesInObject,
43
+ type ExtractServiceFinalResults,
44
+ type ExecuteTxOptions,
45
+ type TxResult,
46
+ } from "./query/unit-of-work/execute-unit-of-work";
47
+ import type { IUnitOfWork } from "./query/unit-of-work/unit-of-work";
48
+ import type { AnySchema } from "./schema/create";
49
+ import type { SyncCommandRegistry, SyncCommandTargetRegistration } from "./sync/types";
50
+ import { resolveDatabaseAdapter } from "./util/default-database-adapter";
51
+ type RegistrySchemaInfo = {
52
+ name: string;
53
+ namespace: string | null;
54
+ version: number;
55
+ tables: string[];
56
+ };
57
+
58
+ type RegistryFragmentMeta = {
59
+ name: string;
60
+ mountRoute: string;
61
+ };
62
+
63
+ type ExtractServiceFinalResultOrSingle<T> = T extends readonly (
64
+ | TxResult<unknown, unknown>
65
+ | undefined
66
+ )[]
67
+ ? AwaitedPromisesInObject<ExtractServiceFinalResults<T>>
68
+ : T extends undefined
69
+ ? undefined
70
+ : AwaitedPromisesInObject<ExtractServiceFinalResults<readonly [T]>>[0];
71
+
72
+ type RegistryResolver = {
73
+ getRegistryForAdapterSync: <TUOWConfig>(adapter: DatabaseAdapter<TUOWConfig>) => {
74
+ registerSchema: (
75
+ schema: RegistrySchemaInfo,
76
+ fragment: RegistryFragmentMeta,
77
+ options?: { outboxEnabled?: boolean },
78
+ ) => void;
79
+ registerSyncCommands: (registration: SyncCommandTargetRegistration) => void;
80
+ };
81
+ getInternalFragment: <TUOWConfig>(
82
+ adapter: DatabaseAdapter<TUOWConfig>,
83
+ ) => InternalFragmentInstance;
84
+ };
85
+
86
+ type HooksFactoryContext<TConfig> = {
87
+ config: TConfig;
88
+ options: FragnoPublicConfigWithDatabase;
89
+ deps: unknown;
90
+ services: unknown;
91
+ serviceDeps: unknown;
92
+ };
93
+
94
+ type AnyHttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
95
+
96
+ type AnyFragnoRouteConfig = FragnoRouteConfig<
97
+ AnyHttpMethod,
98
+ string,
99
+ undefined,
100
+ undefined,
101
+ string,
102
+ string,
103
+ RequestThisContext
104
+ >;
34
105
 
35
106
  /**
36
107
  * Extended FragnoPublicConfig for database fragments.
@@ -39,6 +110,19 @@ import { sanitizeNamespace } from "./naming/sql-naming";
39
110
  export type FragnoPublicConfigWithDatabase = FragnoPublicConfig & {
40
111
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
112
  databaseAdapter?: DatabaseAdapter<any>;
113
+ /**
114
+ * Optional guard to limit database roundtrips per request (primarily for tests).
115
+ * When enabled, retrieve-only and mutate-only handlerTx().execute() calls are
116
+ * counted separately (one of each by default).
117
+ * Applied only for route handlers (not inContext).
118
+ */
119
+ dbRoundtripGuard?: boolean | DbRoundtripGuardConfig;
120
+ /**
121
+ * Optional outbox configuration for this fragment.
122
+ */
123
+ outbox?: {
124
+ enabled?: boolean;
125
+ };
42
126
  /**
43
127
  * Optional durable hooks processing configuration.
44
128
  */
@@ -51,16 +135,27 @@ export type FragnoPublicConfigWithDatabase = FragnoPublicConfig & {
51
135
  databaseNamespace?: string | null;
52
136
  };
53
137
 
138
+ /**
139
+ * Configuration for limiting database roundtrips per request.
140
+ */
141
+ export type DbRoundtripGuardConfig = {
142
+ /**
143
+ * Maximum allowed retrieve-only and mutate-only handlerTx().execute() calls per request.
144
+ * Each type is tracked separately.
145
+ * Defaults to 1 when the guard is enabled.
146
+ */
147
+ maxRoundtrips?: number;
148
+ };
149
+
54
150
  /**
55
151
  * Implicit dependencies that database fragments get automatically.
56
152
  * These are injected without requiring explicit configuration.
57
153
  */
58
154
  export type ImplicitDatabaseDependencies<TSchema extends AnySchema> = {
59
155
  /**
60
- * Database query engine for the fragment's schema.
61
- * @deprecated Prefer handlerTx/serviceTx instead of direct db usage.
156
+ * Database adapter instance.
62
157
  */
63
- db: SimpleQueryInterface<TSchema>;
158
+ databaseAdapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
64
159
  /**
65
160
  * The schema definition for this fragment.
66
161
  */
@@ -137,19 +232,34 @@ export type DatabaseHandlerContext<THooks extends HooksMap = {}> = RequestThisCo
137
232
  handlerTx(
138
233
  options?: Omit<ExecuteTxOptions, "createUnitOfWork">,
139
234
  ): HandlerTxBuilder<readonly [], [], [], unknown, unknown, false, false, false, false, THooks>;
235
+
236
+ /**
237
+ * Execute multiple service calls in a handler context and return their final results.
238
+ */
239
+ callServices<
240
+ TServiceCalls extends
241
+ | TxResult<unknown, unknown>
242
+ | undefined
243
+ | readonly (TxResult<unknown, unknown> | undefined)[],
244
+ >(
245
+ /**
246
+ * Factory to create service calls inside the active context.
247
+ */
248
+ serviceCalls: () => TServiceCalls,
249
+ ): Promise<ExtractServiceFinalResultOrSingle<TServiceCalls>>;
140
250
  };
141
251
 
142
252
  /**
143
253
  * Database fragment context provided to user callbacks.
144
254
  */
145
- export type DatabaseFragmentContext<TSchema extends AnySchema> = {
255
+ export type DatabaseFragmentContext = {
146
256
  /**
147
257
  * Database adapter instance.
148
258
  */
149
259
  databaseAdapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
150
- /**
151
- * ORM query engine for the fragment's schema.
152
- */
260
+ };
261
+
262
+ type DatabaseFragmentContextInternal<TSchema extends AnySchema> = DatabaseFragmentContext & {
153
263
  db: SimpleQueryInterface<TSchema>;
154
264
  };
155
265
 
@@ -160,7 +270,7 @@ export type DatabaseFragmentContext<TSchema extends AnySchema> = {
160
270
  function createDatabaseContext<TSchema extends AnySchema>(
161
271
  options: FragnoPublicConfigWithDatabase,
162
272
  schema: TSchema,
163
- ): DatabaseFragmentContext<TSchema> {
273
+ ): DatabaseFragmentContextInternal<TSchema> {
164
274
  const databaseAdapter = resolveDatabaseAdapter(options, schema);
165
275
 
166
276
  const namespace = resolveDatabaseNamespace(options, schema);
@@ -177,6 +287,295 @@ function resolveDatabaseNamespace<TSchema extends AnySchema>(
177
287
  return hasOverride ? (options.databaseNamespace ?? null) : sanitizeNamespace(schema.name);
178
288
  }
179
289
 
290
+ function resolveMountRoute(name: string, mountRoute?: string): string {
291
+ const resolved = mountRoute ?? `/api/${name}`;
292
+ return resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
293
+ }
294
+
295
+ const dbRoundtripGuardStateSymbol = Symbol("fragno-db-roundtrip-guard");
296
+ const requestSourceSymbol = Symbol.for("fragno-request-source");
297
+ const requestRouteSymbol = Symbol.for("fragno-request-route");
298
+ const requestWaitUntilSymbol = Symbol.for("fragno-request-wait-until");
299
+ const roundtripGuardDocsUrl = "https://fragno.dev/docs/fragno/for-library-authors/rules-of-fragno";
300
+
301
+ type DbRoundtripGuardState = {
302
+ retrieveCount: number;
303
+ mutateCount: number;
304
+ maxRoundtrips: number;
305
+ };
306
+
307
+ type DurableHooksLogContext = HookNotifyContext;
308
+
309
+ type AnyHandlerTxBuilder = HandlerTxBuilder<
310
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
311
+ any,
312
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
313
+ any,
314
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
315
+ any,
316
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
+ any,
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ any,
320
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
321
+ any,
322
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
323
+ any,
324
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
325
+ any,
326
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
327
+ any,
328
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
329
+ any
330
+ >;
331
+
332
+ function wrapHandlerTxBuilderWithRoundtripGuard<
333
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
334
+ TBuilder extends HandlerTxBuilder<any, any, any, any, any, any, any, any, any, any>,
335
+ >(builder: TBuilder, onExecute: () => void): TBuilder {
336
+ const wrappedBuilders = new WeakSet<AnyHandlerTxBuilder>();
337
+
338
+ const applyExecuteGuard = (target: TBuilder): TBuilder => {
339
+ if (wrappedBuilders.has(target)) {
340
+ return target;
341
+ }
342
+ wrappedBuilders.add(target);
343
+ const execute = target.execute.bind(target);
344
+ target.execute = () => {
345
+ onExecute();
346
+ return execute();
347
+ };
348
+ return target;
349
+ };
350
+
351
+ const wrap = (target: TBuilder): TBuilder => {
352
+ const guarded = applyExecuteGuard(target);
353
+ return new Proxy(guarded, {
354
+ get(obj, prop, receiver) {
355
+ const value = Reflect.get(obj, prop, receiver);
356
+ if (typeof value !== "function") {
357
+ return value;
358
+ }
359
+ if (prop === "execute") {
360
+ return value;
361
+ }
362
+ return (...args: unknown[]) => {
363
+ const result = value.apply(obj, args);
364
+ if (result instanceof HandlerTxBuilder) {
365
+ return wrap(result as TBuilder);
366
+ }
367
+ return result;
368
+ };
369
+ },
370
+ }) as TBuilder;
371
+ };
372
+
373
+ return wrap(builder);
374
+ }
375
+
376
+ function buildDurableHooksLogContext(context?: DurableHooksLogContext) {
377
+ const logContext: Record<string, unknown> = {};
378
+ if (context?.route !== undefined) {
379
+ logContext["route"] = context.route;
380
+ }
381
+ if (context?.source !== undefined) {
382
+ logContext["source"] = context.source;
383
+ }
384
+ return logContext;
385
+ }
386
+
387
+ function notifyDurableHooks(
388
+ notifier: HookNotifier,
389
+ namespace: string,
390
+ notifyContext: HookNotifyContext,
391
+ logContextFields: Record<string, unknown>,
392
+ options?: { crossNamespace?: boolean },
393
+ ) {
394
+ const crossNamespace = options?.crossNamespace ?? false;
395
+ const suffix = crossNamespace ? " (cross-namespace)" : "";
396
+ const notifyStart = Date.now();
397
+ DurableHooksLogger.debug(`Durable hooks notify requested${suffix}`, {
398
+ namespace,
399
+ fields: logContextFields,
400
+ });
401
+ const notifyPromise = Promise.resolve()
402
+ .then(() => notifier.notify(notifyContext))
403
+ .then((processed) => {
404
+ DurableHooksLogger.debug(`Durable hooks notify completed${suffix}`, {
405
+ namespace,
406
+ fields: {
407
+ result: processed === undefined ? null : processed,
408
+ ms: Date.now() - notifyStart,
409
+ ...logContextFields,
410
+ },
411
+ });
412
+ })
413
+ .catch((error) => {
414
+ DurableHooksLogger.error(`Durable hooks notify failed${suffix}`, {
415
+ namespace,
416
+ fields: {
417
+ error: DurableHooksLogger.toErrorMessage(error),
418
+ ...logContextFields,
419
+ },
420
+ });
421
+ });
422
+
423
+ if (notifyContext.waitUntil) {
424
+ notifyContext.waitUntil(notifyPromise);
425
+ return;
426
+ }
427
+ void notifyPromise;
428
+ }
429
+
430
+ function notifyDurableHooksAfterMutate<THooks extends HooksMap>({
431
+ uow,
432
+ hooksConfig,
433
+ internalFragment,
434
+ autoSchedule,
435
+ planMode,
436
+ logContext,
437
+ }: {
438
+ uow: IUnitOfWork;
439
+ hooksConfig?: HookProcessorConfig<THooks>;
440
+ internalFragment?: InternalFragmentInstance;
441
+ autoSchedule: boolean;
442
+ planMode: boolean;
443
+ logContext?: DurableHooksLogContext;
444
+ }) {
445
+ if (planMode) {
446
+ return;
447
+ }
448
+
449
+ const notifyContext: HookNotifyContext = {
450
+ source: logContext?.source ?? "request",
451
+ route: logContext?.route,
452
+ waitUntil: logContext?.waitUntil,
453
+ };
454
+
455
+ const logContextFields = buildDurableHooksLogContext(logContext);
456
+ const triggeredHooks = uow.getTriggeredHooks();
457
+ const ownNamespaceTriggeredCount = hooksConfig
458
+ ? triggeredHooks.filter((hook) => hook.namespace === hooksConfig.namespace).length
459
+ : 0;
460
+
461
+ if (hooksConfig?.notifier && autoSchedule && ownNamespaceTriggeredCount > 0) {
462
+ notifyDurableHooks(
463
+ hooksConfig.notifier,
464
+ hooksConfig.namespace,
465
+ notifyContext,
466
+ logContextFields,
467
+ );
468
+ } else if (hooksConfig && !autoSchedule && ownNamespaceTriggeredCount > 0) {
469
+ DurableHooksLogger.debug("Durable hooks notify skipped (autoSchedule=false)", {
470
+ namespace: hooksConfig.namespace,
471
+ fields: {
472
+ queued: ownNamespaceTriggeredCount,
473
+ ...logContextFields,
474
+ },
475
+ });
476
+ } else if (hooksConfig && !hooksConfig.notifier && ownNamespaceTriggeredCount > 0) {
477
+ DurableHooksLogger.debug("Durable hooks notify skipped (notifier missing)", {
478
+ namespace: hooksConfig.namespace,
479
+ fields: {
480
+ queued: ownNamespaceTriggeredCount,
481
+ ...logContextFields,
482
+ },
483
+ });
484
+ }
485
+
486
+ if (triggeredHooks.length === 0) {
487
+ return;
488
+ }
489
+
490
+ const namespaces = new Set<string>();
491
+ const triggeredCountByNamespace = new Map<string, number>();
492
+ for (const hook of triggeredHooks) {
493
+ namespaces.add(hook.namespace);
494
+ triggeredCountByNamespace.set(
495
+ hook.namespace,
496
+ (triggeredCountByNamespace.get(hook.namespace) ?? 0) + 1,
497
+ );
498
+ }
499
+ if (hooksConfig) {
500
+ namespaces.delete(hooksConfig.namespace);
501
+ }
502
+
503
+ for (const namespace of namespaces) {
504
+ if (!internalFragment) {
505
+ DurableHooksLogger.debug("Durable hooks notifier missing scope for namespace", {
506
+ namespace,
507
+ fields: logContextFields,
508
+ });
509
+ continue;
510
+ }
511
+
512
+ const runtime = getDurableHooksRuntimeByNamespace(namespace, internalFragment);
513
+ if (!runtime) {
514
+ DurableHooksLogger.debug("Durable hooks notifier missing for namespace", {
515
+ namespace,
516
+ fields: logContextFields,
517
+ });
518
+ continue;
519
+ }
520
+ if (runtime.config.autoSchedule === false) {
521
+ DurableHooksLogger.debug("Durable hooks notify skipped (autoSchedule=false)", {
522
+ namespace,
523
+ fields: {
524
+ queued: triggeredCountByNamespace.get(namespace) ?? 0,
525
+ ...logContextFields,
526
+ },
527
+ });
528
+ continue;
529
+ }
530
+ const notifier = runtime.config.notifier;
531
+ if (!notifier) {
532
+ DurableHooksLogger.debug("Durable hooks notifier missing for namespace", {
533
+ namespace,
534
+ fields: logContextFields,
535
+ });
536
+ continue;
537
+ }
538
+ notifyDurableHooks(notifier, namespace, notifyContext, logContextFields, {
539
+ crossNamespace: true,
540
+ });
541
+ }
542
+ }
543
+
544
+ function resolveDbRoundtripGuard(
545
+ options: FragnoPublicConfigWithDatabase,
546
+ ): DbRoundtripGuardState | null {
547
+ const guard = options.dbRoundtripGuard;
548
+ if (!guard) {
549
+ return null;
550
+ }
551
+
552
+ if (guard === true) {
553
+ return { retrieveCount: 0, mutateCount: 0, maxRoundtrips: 1 };
554
+ }
555
+
556
+ return { retrieveCount: 0, mutateCount: 0, maxRoundtrips: guard.maxRoundtrips ?? 1 };
557
+ }
558
+
559
+ function getDbRoundtripGuardState(
560
+ storage: DatabaseContextStorage,
561
+ guard: DbRoundtripGuardState,
562
+ ): DbRoundtripGuardState {
563
+ const storageWithGuard = storage as DatabaseContextStorage & {
564
+ [dbRoundtripGuardStateSymbol]?: DbRoundtripGuardState;
565
+ };
566
+ if (!storageWithGuard[dbRoundtripGuardStateSymbol]) {
567
+ storageWithGuard[dbRoundtripGuardStateSymbol] = { ...guard };
568
+ } else {
569
+ storageWithGuard[dbRoundtripGuardStateSymbol]!.maxRoundtrips = guard.maxRoundtrips;
570
+ }
571
+ return storageWithGuard[dbRoundtripGuardStateSymbol]!;
572
+ }
573
+
574
+ function isRouteRequest(storage: DatabaseContextStorage): boolean {
575
+ const source = (storage as Record<symbol, unknown>)[requestSourceSymbol];
576
+ return source === "route";
577
+ }
578
+
180
579
  /**
181
580
  * Storage type for database fragments - stores the Unit of Work.
182
581
  */
@@ -201,7 +600,7 @@ export class DatabaseFragmentDefinitionBuilder<
201
600
  THooks extends HooksMap = {},
202
601
  TServiceThisContext extends RequestThisContext = DatabaseHandlerContext,
203
602
  THandlerThisContext extends RequestThisContext = DatabaseHandlerContext,
204
- TLinkedFragments extends Record<string, AnyFragnoInstantiatedFragment> = {},
603
+ TInternalRoutes extends readonly AnyRouteOrFactory[] = readonly [],
205
604
  > {
206
605
  // Store the base builder - we'll replace its storage and context setup when building
207
606
  #baseBuilder: FragmentDefinitionBuilder<
@@ -215,10 +614,12 @@ export class DatabaseFragmentDefinitionBuilder<
215
614
  TServiceThisContext,
216
615
  THandlerThisContext,
217
616
  DatabaseRequestStorage,
218
- TLinkedFragments
617
+ TInternalRoutes
219
618
  >;
220
619
  #schema: TSchema;
221
- #hooksFactory?: (context: { config: TConfig; options: FragnoPublicConfigWithDatabase }) => THooks;
620
+ #hooksFactory?: (context: HooksFactoryContext<TConfig>) => THooks;
621
+ #syncRegistry?: SyncCommandRegistry;
622
+ #registryResolver?: RegistryResolver;
222
623
 
223
624
  constructor(
224
625
  baseBuilder: FragmentDefinitionBuilder<
@@ -232,28 +633,28 @@ export class DatabaseFragmentDefinitionBuilder<
232
633
  TServiceThisContext,
233
634
  THandlerThisContext,
234
635
  DatabaseRequestStorage,
235
- TLinkedFragments
636
+ TInternalRoutes
236
637
  >,
237
638
  schema: TSchema,
238
- hooksFactory?: (context: {
239
- config: TConfig;
240
- options: FragnoPublicConfigWithDatabase;
241
- }) => THooks,
639
+ hooksFactory?: (context: HooksFactoryContext<TConfig>) => THooks,
640
+ syncRegistry?: SyncCommandRegistry,
641
+ registryResolver?: RegistryResolver,
242
642
  ) {
243
643
  this.#baseBuilder = baseBuilder;
244
644
  this.#schema = schema;
245
645
  this.#hooksFactory = hooksFactory;
646
+ this.#syncRegistry = syncRegistry;
647
+ this.#registryResolver = registryResolver;
246
648
  }
247
649
 
248
650
  /**
249
651
  * Define dependencies for this database fragment.
250
- * The context includes database adapter and ORM instance.
652
+ * The context includes the database adapter.
251
653
  */
252
654
  withDependencies<TNewDeps>(
253
655
  fn: (context: {
254
656
  config: TConfig;
255
657
  options: FragnoPublicConfigWithDatabase;
256
- db: SimpleQueryInterface<TSchema>;
257
658
  databaseAdapter: DatabaseAdapter<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
258
659
  }) => TNewDeps,
259
660
  ): DatabaseFragmentDefinitionBuilder<
@@ -267,7 +668,7 @@ export class DatabaseFragmentDefinitionBuilder<
267
668
  THooks,
268
669
  TServiceThisContext,
269
670
  THandlerThisContext,
270
- TLinkedFragments
671
+ TInternalRoutes
271
672
  > {
272
673
  // Wrap user function to inject DB context
273
674
  const wrappedFn = (context: { config: TConfig; options: FragnoPublicConfigWithDatabase }) => {
@@ -278,14 +679,13 @@ export class DatabaseFragmentDefinitionBuilder<
278
679
  const userDeps = fn({
279
680
  config: context.config,
280
681
  options: context.options,
281
- db: dbContext.db,
282
682
  databaseAdapter: dbContext.databaseAdapter,
283
683
  });
284
684
 
285
685
  // Create implicit dependencies
286
686
  const createUow = () => dbContext.db.createUnitOfWork();
287
687
  const implicitDeps: ImplicitDatabaseDependencies<TSchema> = {
288
- db: dbContext.db,
688
+ databaseAdapter: dbContext.databaseAdapter,
289
689
  schema: this.#schema,
290
690
  namespace,
291
691
  createUnitOfWork: createUow,
@@ -300,7 +700,13 @@ export class DatabaseFragmentDefinitionBuilder<
300
700
  // Create new base builder with wrapped function
301
701
  const newBaseBuilder = this.#baseBuilder.withDependencies(wrappedFn);
302
702
 
303
- return new DatabaseFragmentDefinitionBuilder(newBaseBuilder, this.#schema, this.#hooksFactory);
703
+ return new DatabaseFragmentDefinitionBuilder(
704
+ newBaseBuilder,
705
+ this.#schema,
706
+ this.#hooksFactory,
707
+ this.#syncRegistry,
708
+ this.#registryResolver,
709
+ );
304
710
  }
305
711
 
306
712
  providesBaseService<TNewService>(
@@ -324,11 +730,17 @@ export class DatabaseFragmentDefinitionBuilder<
324
730
  THooks,
325
731
  TServiceThisContext,
326
732
  THandlerThisContext,
327
- TLinkedFragments
733
+ TInternalRoutes
328
734
  > {
329
735
  const newBaseBuilder = this.#baseBuilder.providesBaseService<TNewService>(fn);
330
736
 
331
- return new DatabaseFragmentDefinitionBuilder(newBaseBuilder, this.#schema, this.#hooksFactory);
737
+ return new DatabaseFragmentDefinitionBuilder(
738
+ newBaseBuilder,
739
+ this.#schema,
740
+ this.#hooksFactory,
741
+ this.#syncRegistry,
742
+ this.#registryResolver,
743
+ );
332
744
  }
333
745
 
334
746
  providesService<TServiceName extends string, TService>(
@@ -353,14 +765,20 @@ export class DatabaseFragmentDefinitionBuilder<
353
765
  THooks,
354
766
  TServiceThisContext,
355
767
  THandlerThisContext,
356
- TLinkedFragments
768
+ TInternalRoutes
357
769
  > {
358
770
  const newBaseBuilder = this.#baseBuilder.providesService<TServiceName, TService>(
359
771
  serviceName,
360
772
  fn,
361
773
  );
362
774
 
363
- return new DatabaseFragmentDefinitionBuilder(newBaseBuilder, this.#schema, this.#hooksFactory);
775
+ return new DatabaseFragmentDefinitionBuilder(
776
+ newBaseBuilder,
777
+ this.#schema,
778
+ this.#hooksFactory,
779
+ this.#syncRegistry,
780
+ this.#registryResolver,
781
+ );
364
782
  }
365
783
 
366
784
  /**
@@ -392,14 +810,20 @@ export class DatabaseFragmentDefinitionBuilder<
392
810
  THooks,
393
811
  TServiceThisContext,
394
812
  THandlerThisContext,
395
- TLinkedFragments
813
+ TInternalRoutes
396
814
  > {
397
815
  const newBaseBuilder = this.#baseBuilder.providesPrivateService<TServiceName, TService>(
398
816
  serviceName,
399
817
  fn,
400
818
  );
401
819
 
402
- return new DatabaseFragmentDefinitionBuilder(newBaseBuilder, this.#schema, this.#hooksFactory);
820
+ return new DatabaseFragmentDefinitionBuilder(
821
+ newBaseBuilder,
822
+ this.#schema,
823
+ this.#hooksFactory,
824
+ this.#syncRegistry,
825
+ this.#registryResolver,
826
+ );
403
827
  }
404
828
 
405
829
  /**
@@ -423,6 +847,9 @@ export class DatabaseFragmentDefinitionBuilder<
423
847
  fn: (context: {
424
848
  config: TConfig;
425
849
  options: FragnoPublicConfigWithDatabase;
850
+ deps: TDeps;
851
+ services: BoundServices<TBaseServices & TServices>;
852
+ serviceDeps: TServiceDependencies;
426
853
  defineHook: <TPayload>(
427
854
  hook: (this: HookContext, payload: TPayload) => void | Promise<void>,
428
855
  ) => HookFn<TPayload>;
@@ -438,7 +865,7 @@ export class DatabaseFragmentDefinitionBuilder<
438
865
  TNewHooks,
439
866
  DatabaseServiceContext<TNewHooks>,
440
867
  THandlerThisContext,
441
- TLinkedFragments
868
+ TInternalRoutes
442
869
  > {
443
870
  const defineHook = <TPayload>(
444
871
  hook: (this: HookContext, payload: TPayload) => void | Promise<void>,
@@ -447,13 +874,13 @@ export class DatabaseFragmentDefinitionBuilder<
447
874
  };
448
875
 
449
876
  // Store the hooks factory - it will be called in build() with config/options
450
- const hooksFactory = (context: {
451
- config: TConfig;
452
- options: FragnoPublicConfigWithDatabase;
453
- }) => {
877
+ const hooksFactory = (context: HooksFactoryContext<TConfig>) => {
454
878
  return fn({
455
879
  config: context.config,
456
880
  options: context.options,
881
+ deps: context.deps as TDeps,
882
+ services: context.services as BoundServices<TBaseServices & TServices>,
883
+ serviceDeps: context.serviceDeps as TServiceDependencies,
457
884
  defineHook,
458
885
  });
459
886
  };
@@ -463,6 +890,9 @@ export class DatabaseFragmentDefinitionBuilder<
463
890
  const newBuilder = new DatabaseFragmentDefinitionBuilder(
464
891
  this.#baseBuilder,
465
892
  this.#schema,
893
+ this.#hooksFactory,
894
+ this.#syncRegistry,
895
+ this.#registryResolver,
466
896
  ) as unknown as DatabaseFragmentDefinitionBuilder<
467
897
  TSchema,
468
898
  TConfig,
@@ -474,7 +904,7 @@ export class DatabaseFragmentDefinitionBuilder<
474
904
  TNewHooks,
475
905
  DatabaseServiceContext<TNewHooks>,
476
906
  THandlerThisContext,
477
- TLinkedFragments
907
+ TInternalRoutes
478
908
  >;
479
909
 
480
910
  newBuilder.#hooksFactory = hooksFactory;
@@ -482,6 +912,39 @@ export class DatabaseFragmentDefinitionBuilder<
482
912
  return newBuilder;
483
913
  }
484
914
 
915
+ /**
916
+ * Register sync command definitions for this fragment.
917
+ */
918
+ withSyncCommands(
919
+ registry: SyncCommandRegistry,
920
+ ): DatabaseFragmentDefinitionBuilder<
921
+ TSchema,
922
+ TConfig,
923
+ TDeps,
924
+ TBaseServices,
925
+ TServices,
926
+ TServiceDependencies,
927
+ TPrivateServices,
928
+ THooks,
929
+ TServiceThisContext,
930
+ THandlerThisContext,
931
+ TInternalRoutes
932
+ > {
933
+ if (registry.schemaName !== this.#schema.name) {
934
+ throw new Error(
935
+ `Sync command registry schema name "${registry.schemaName}" does not match fragment schema "${this.#schema.name}".`,
936
+ );
937
+ }
938
+
939
+ return new DatabaseFragmentDefinitionBuilder(
940
+ this.#baseBuilder,
941
+ this.#schema,
942
+ this.#hooksFactory,
943
+ registry,
944
+ this.#registryResolver,
945
+ );
946
+ }
947
+
485
948
  /**
486
949
  * Declare that this fragment uses a required service provided by the runtime.
487
950
  * Delegates to the base builder.
@@ -499,11 +962,17 @@ export class DatabaseFragmentDefinitionBuilder<
499
962
  THooks,
500
963
  TServiceThisContext,
501
964
  THandlerThisContext,
502
- TLinkedFragments
965
+ TInternalRoutes
503
966
  > {
504
967
  const newBaseBuilder = this.#baseBuilder.usesService<TServiceName, TService>(serviceName);
505
968
 
506
- return new DatabaseFragmentDefinitionBuilder(newBaseBuilder, this.#schema, this.#hooksFactory);
969
+ return new DatabaseFragmentDefinitionBuilder(
970
+ newBaseBuilder,
971
+ this.#schema,
972
+ this.#hooksFactory,
973
+ this.#syncRegistry,
974
+ this.#registryResolver,
975
+ );
507
976
  }
508
977
 
509
978
  /**
@@ -523,13 +992,19 @@ export class DatabaseFragmentDefinitionBuilder<
523
992
  THooks,
524
993
  TServiceThisContext,
525
994
  THandlerThisContext,
526
- TLinkedFragments
995
+ TInternalRoutes
527
996
  > {
528
997
  const newBaseBuilder = this.#baseBuilder.usesOptionalService<TServiceName, TService>(
529
998
  serviceName,
530
999
  );
531
1000
 
532
- return new DatabaseFragmentDefinitionBuilder(newBaseBuilder, this.#schema, this.#hooksFactory);
1001
+ return new DatabaseFragmentDefinitionBuilder(
1002
+ newBaseBuilder,
1003
+ this.#schema,
1004
+ this.#hooksFactory,
1005
+ this.#syncRegistry,
1006
+ this.#registryResolver,
1007
+ );
533
1008
  }
534
1009
 
535
1010
  /**
@@ -548,7 +1023,7 @@ export class DatabaseFragmentDefinitionBuilder<
548
1023
  DatabaseServiceContext<THooks>,
549
1024
  DatabaseHandlerContext<THooks>,
550
1025
  DatabaseRequestStorage,
551
- TLinkedFragments
1026
+ TInternalRoutes
552
1027
  > {
553
1028
  const baseDef = this.#baseBuilder.build();
554
1029
 
@@ -578,11 +1053,42 @@ export class DatabaseFragmentDefinitionBuilder<
578
1053
  }
579
1054
  }
580
1055
 
581
- const { db } = createDatabaseContext(context.options, this.#schema);
1056
+ const dbContext = createDatabaseContext(context.options, this.#schema);
1057
+ const { db } = dbContext;
582
1058
  const namespace = resolveDatabaseNamespace(context.options, this.#schema);
1059
+ const dryRun = process.env["FRAGNO_INIT_DRY_RUN"] === "true";
1060
+ const isInternalFragment = baseDef.name === "$fragno-internal-fragment";
1061
+
1062
+ if (!dryRun && !isInternalFragment && this.#registryResolver) {
1063
+ const registry = this.#registryResolver.getRegistryForAdapterSync(
1064
+ dbContext.databaseAdapter,
1065
+ );
1066
+ const outboxEnabled = context.options.outbox?.enabled ?? false;
1067
+ registry.registerSchema(
1068
+ {
1069
+ name: this.#schema.name,
1070
+ namespace,
1071
+ version: this.#schema.version,
1072
+ tables: Object.keys(this.#schema.tables).sort(),
1073
+ },
1074
+ {
1075
+ name: baseDef.name,
1076
+ mountRoute: resolveMountRoute(baseDef.name, context.options.mountRoute),
1077
+ },
1078
+ { outboxEnabled },
1079
+ );
1080
+ if (this.#syncRegistry) {
1081
+ registry.registerSyncCommands({
1082
+ fragmentName: baseDef.name,
1083
+ schemaName: this.#syncRegistry.schemaName,
1084
+ namespace,
1085
+ commands: this.#syncRegistry.commands,
1086
+ });
1087
+ }
1088
+ }
583
1089
 
584
1090
  const implicitDeps: ImplicitDatabaseDependencies<TSchema> = {
585
- db,
1091
+ databaseAdapter: dbContext.databaseAdapter,
586
1092
  schema: this.#schema,
587
1093
  namespace,
588
1094
  createUnitOfWork: () => db.createUnitOfWork(),
@@ -608,20 +1114,12 @@ export class DatabaseFragmentDefinitionBuilder<
608
1114
  // Create database context - needed here to create the UOW
609
1115
  const dbContextForStorage = createDatabaseContext(options, this.#schema);
610
1116
 
611
- // Create a new Unit of Work for this request
612
- const uow: IUnitOfWork = dbContextForStorage.db.createUnitOfWork();
1117
+ const uow = dbContextForStorage.db.createBaseUnitOfWork();
613
1118
 
614
1119
  return { uow };
615
1120
  },
616
1121
  );
617
1122
 
618
- // Get the internal fragment factory from linked fragments (added by withDatabase)
619
- // Cast is safe: withDatabase() guarantees this fragment exists and has the correct type
620
- const internalFragmentFactory = baseDef.linkedFragments?.["_fragno_internal"] as (context: {
621
- config: TConfig;
622
- options: FragnoPublicConfigWithDatabase;
623
- }) => InternalFragmentInstance;
624
-
625
1123
  // Cache per instantiated fragment (deps object is unique per instantiation).
626
1124
  const hooksConfigCache = new WeakMap<object, HookProcessorConfig<THooks>>();
627
1125
 
@@ -629,6 +1127,8 @@ export class DatabaseFragmentDefinitionBuilder<
629
1127
  config: TConfig;
630
1128
  options: FragnoPublicConfigWithDatabase;
631
1129
  deps: TDeps;
1130
+ services?: BoundServices<TBaseServices & TServices>;
1131
+ serviceDeps?: TServiceDependencies;
632
1132
  }) => {
633
1133
  if (!this.#hooksFactory) {
634
1134
  return undefined;
@@ -637,76 +1137,148 @@ export class DatabaseFragmentDefinitionBuilder<
637
1137
  typeof context.deps === "object" && context.deps !== null
638
1138
  ? (context.deps as object)
639
1139
  : undefined;
1140
+ const namespace = resolveDatabaseNamespace(context.options, this.#schema);
1141
+ const namespaceKey = namespace ?? this.#schema.name;
1142
+ const durableHooksOptions = context.options.durableHooks;
1143
+ DurableHooksLogger.configure(durableHooksOptions?.logging, namespaceKey);
640
1144
  const cachedHooksConfig = depsKey ? hooksConfigCache.get(depsKey) : undefined;
641
1145
  if (cachedHooksConfig) {
1146
+ if (!cachedHooksConfig.hooks && context.services) {
1147
+ cachedHooksConfig.hooks = this.#hooksFactory({
1148
+ config: context.config,
1149
+ options: context.options,
1150
+ deps: context.deps,
1151
+ services: context.services,
1152
+ serviceDeps: context.serviceDeps ?? ({} as TServiceDependencies),
1153
+ });
1154
+ }
642
1155
  return cachedHooksConfig;
643
1156
  }
644
1157
 
645
- const namespace = resolveDatabaseNamespace(context.options, this.#schema);
646
- const namespaceKey = namespace ?? this.#schema.name;
647
- const durableHooksOptions = context.options.durableHooks;
1158
+ const autoSchedule = durableHooksOptions?.autoSchedule !== false;
648
1159
  const baseAdapter = resolveDatabaseAdapter(context.options, this.#schema);
649
1160
  const hookAdapter = baseAdapter.getHookProcessingAdapter?.() ?? baseAdapter;
650
1161
  const hookOptions =
651
1162
  hookAdapter === baseAdapter
652
1163
  ? context.options
653
1164
  : { ...context.options, databaseAdapter: hookAdapter };
1165
+ const registryResolver = this.#registryResolver;
1166
+ if (!registryResolver) {
1167
+ throw new Error("Adapter registry resolver is missing for durable hooks.");
1168
+ }
654
1169
  const dbContextForHooks = createDatabaseContext(hookOptions, this.#schema);
1170
+ const hookContextStorage = dbContextForHooks.databaseAdapter.contextStorage;
655
1171
  const hooksConfig: HookProcessorConfig<THooks> = {
656
- hooks: this.#hooksFactory(context),
1172
+ hooks: context.services
1173
+ ? this.#hooksFactory({
1174
+ config: context.config,
1175
+ options: context.options,
1176
+ deps: context.deps,
1177
+ services: context.services,
1178
+ serviceDeps: context.serviceDeps ?? ({} as TServiceDependencies),
1179
+ })
1180
+ : undefined,
657
1181
  namespace: namespaceKey,
658
- internalFragment: internalFragmentFactory({
659
- config: context.config,
660
- options: hookOptions,
661
- }),
1182
+ internalFragment: registryResolver.getInternalFragment(hookAdapter),
1183
+ autoSchedule,
662
1184
  handlerTx: (execOptions?: Omit<ExecuteTxOptions, "createUnitOfWork">) => {
663
1185
  const userOnBeforeMutate = execOptions?.onBeforeMutate;
664
1186
  const userOnAfterMutate = execOptions?.onAfterMutate;
665
- return createHandlerTxBuilder<THooks>({
666
- ...execOptions,
667
- createUnitOfWork: () => {
668
- const uow = dbContextForHooks.db.createUnitOfWork();
669
- uow.registerSchema(
670
- hooksConfig.internalFragment.$internal.deps.schema,
671
- hooksConfig.internalFragment.$internal.deps.namespace,
672
- );
673
- return uow;
674
- },
675
- onBeforeMutate: (uow) => {
676
- prepareHookMutations(uow, hooksConfig);
677
- if (userOnBeforeMutate) {
678
- userOnBeforeMutate(uow);
679
- }
680
- },
681
- onAfterMutate: async (uow) => {
682
- void hooksConfig.scheduler?.schedule().catch((error) => {
683
- console.error("Durable hooks processing failed", error);
684
- });
685
- if (userOnAfterMutate) {
686
- await userOnAfterMutate(uow);
1187
+ const planMode = execOptions?.planMode ?? false;
1188
+ let storageRef: DatabaseContextStorage | null = null;
1189
+ const getHookWaitUntil = () => {
1190
+ if (!hookContextStorage.hasStore()) {
1191
+ return undefined;
1192
+ }
1193
+ return (
1194
+ hookContextStorage.getStore() as DatabaseContextStorage & {
1195
+ [requestWaitUntilSymbol]?: (promise: Promise<unknown>) => void;
687
1196
  }
1197
+ )[requestWaitUntilSymbol];
1198
+ };
1199
+ return createHandlerTxBuilder<THooks>(
1200
+ {
1201
+ ...execOptions,
1202
+ createUnitOfWork: () => {
1203
+ const baseUow = dbContextForHooks.db.createBaseUnitOfWork();
1204
+ baseUow.registerSchema(
1205
+ hooksConfig.internalFragment.$internal.deps.schema,
1206
+ hooksConfig.internalFragment.$internal.deps.namespace,
1207
+ );
1208
+ if (storageRef) {
1209
+ storageRef.uow = baseUow;
1210
+ }
1211
+ return baseUow;
1212
+ },
1213
+ onBeforeMutate: (uow) => {
1214
+ if (!planMode) {
1215
+ prepareHookMutations(
1216
+ uow,
1217
+ hooksConfig.internalFragment,
1218
+ hooksConfig.defaultRetryPolicy,
1219
+ );
1220
+ }
1221
+ if (userOnBeforeMutate) {
1222
+ userOnBeforeMutate(uow);
1223
+ }
1224
+ },
1225
+ onAfterMutate: async (uow) => {
1226
+ notifyDurableHooksAfterMutate({
1227
+ uow,
1228
+ hooksConfig,
1229
+ internalFragment: hooksConfig.internalFragment,
1230
+ autoSchedule,
1231
+ planMode,
1232
+ logContext: { source: "hook", waitUntil: getHookWaitUntil() },
1233
+ });
1234
+ if (userOnAfterMutate) {
1235
+ await userOnAfterMutate(uow);
1236
+ }
1237
+ },
688
1238
  },
689
- });
1239
+ undefined,
1240
+ (run) =>
1241
+ hookContextStorage.runWithInitializer(() => {
1242
+ const inheritedWaitUntil = getHookWaitUntil();
1243
+ storageRef = { uow: null as unknown as DatabaseContextStorage["uow"] };
1244
+ if (inheritedWaitUntil) {
1245
+ (
1246
+ storageRef as DatabaseContextStorage & {
1247
+ [requestWaitUntilSymbol]?: (promise: Promise<unknown>) => void;
1248
+ }
1249
+ )[requestWaitUntilSymbol] = inheritedWaitUntil;
1250
+ }
1251
+ return storageRef;
1252
+ }, run),
1253
+ );
690
1254
  },
691
1255
  stuckProcessingTimeoutMinutes: durableHooksOptions?.stuckProcessingTimeoutMinutes,
692
1256
  onStuckProcessingHooks: durableHooksOptions?.onStuckProcessingHooks,
693
1257
  };
694
- hooksConfig.scheduler = createHookScheduler(hooksConfig);
1258
+ hooksConfig.runner = createDurableHooksRunner(hooksConfig);
1259
+ registerDurableHooksRuntime(hooksConfig);
695
1260
  if (depsKey) {
696
1261
  hooksConfigCache.set(depsKey, hooksConfig);
697
1262
  }
698
1263
  return hooksConfig;
699
1264
  };
700
1265
 
1266
+ const isInternalFragment = baseDef.name === "$fragno-internal-fragment";
1267
+
701
1268
  const builderWithContext = builderWithStorage.withThisContext<
702
1269
  DatabaseServiceContext<THooks>,
703
1270
  DatabaseHandlerContext<THooks>
704
1271
  >(({ storage, config, options, deps }) => {
705
1272
  // Create hooks config if hooks factory is defined
706
1273
  const hooksConfig = createHooksConfig({ config, options, deps });
707
- const internalFragment =
708
- hooksConfig?.internalFragment ??
709
- (internalFragmentFactory ? internalFragmentFactory({ config, options }) : undefined);
1274
+ const autoSchedule = options.durableHooks?.autoSchedule !== false;
1275
+ const registryResolver = this.#registryResolver;
1276
+ const databaseAdapter =
1277
+ (deps as ImplicitDatabaseDependencies<TSchema>).databaseAdapter ??
1278
+ resolveDatabaseAdapter(options, this.#schema);
1279
+ const internalFragment = isInternalFragment
1280
+ ? undefined
1281
+ : (hooksConfig?.internalFragment ?? registryResolver?.getInternalFragment(databaseAdapter));
710
1282
 
711
1283
  // Builder API: serviceTx using createServiceTxBuilder
712
1284
  function serviceTx<TSchema extends AnySchema>(schema: TSchema) {
@@ -734,8 +1306,75 @@ export class DatabaseFragmentDefinitionBuilder<
734
1306
 
735
1307
  const userOnBeforeMutate = execOptions?.onBeforeMutate;
736
1308
  const userOnAfterMutate = execOptions?.onAfterMutate;
1309
+ const userOnAfterRetrieve = execOptions?.onAfterRetrieve;
1310
+ const planMode = execOptions?.planMode ?? false;
1311
+ const roundtripGuard = isRouteRequest(currentStorage)
1312
+ ? resolveDbRoundtripGuard(options)
1313
+ : null;
1314
+ const roundtripState = roundtripGuard
1315
+ ? getDbRoundtripGuardState(currentStorage, roundtripGuard)
1316
+ : null;
1317
+ const routeInfo = (
1318
+ currentStorage as DatabaseContextStorage & {
1319
+ [requestRouteSymbol]?: { method?: string; path?: string };
1320
+ }
1321
+ )[requestRouteSymbol];
1322
+ const routeWaitUntil = (
1323
+ currentStorage as DatabaseContextStorage & {
1324
+ [requestWaitUntilSymbol]?: (promise: Promise<unknown>) => void;
1325
+ }
1326
+ )[requestWaitUntilSymbol];
1327
+ const routeLabel =
1328
+ routeInfo && routeInfo.method && routeInfo.path
1329
+ ? `${routeInfo.method} ${routeInfo.path}`
1330
+ : null;
1331
+ const routeSuffix = routeLabel ? ` (route: ${routeLabel})` : "";
1332
+ const buildRoundtripError = (kind: "retrieve" | "mutate") =>
1333
+ new FragnoApiError(
1334
+ {
1335
+ message:
1336
+ `[fragno-db] Fragment "${baseDef.name}" executed more than ` +
1337
+ `${roundtripState?.maxRoundtrips ?? 1} ${kind} ` +
1338
+ `database roundtrip(s) in a single request${routeSuffix}. ` +
1339
+ "Combine reads/writes into one handlerTx() or increase dbRoundtripGuard. " +
1340
+ `See ${roundtripGuardDocsUrl}`,
1341
+ code: "DB_ROUNDTRIP_LIMIT_EXCEEDED",
1342
+ },
1343
+ 500,
1344
+ );
1345
+ const roundtripExecutionState = roundtripState
1346
+ ? { countedRetrieve: false, countedMutate: false }
1347
+ : null;
737
1348
 
738
- return createHandlerTxBuilder<THooks>({
1349
+ const resetRoundtripExecutionState = () => {
1350
+ if (!roundtripExecutionState) {
1351
+ return;
1352
+ }
1353
+ roundtripExecutionState.countedRetrieve = false;
1354
+ roundtripExecutionState.countedMutate = false;
1355
+ };
1356
+
1357
+ const guardOnAfterRetrieve = roundtripState
1358
+ ? async (uow: IUnitOfWork, results: unknown[]) => {
1359
+ if (
1360
+ roundtripExecutionState &&
1361
+ !roundtripExecutionState.countedRetrieve &&
1362
+ uow.getRetrievalOperations().length > 0
1363
+ ) {
1364
+ roundtripExecutionState.countedRetrieve = true;
1365
+ roundtripState.retrieveCount += 1;
1366
+ if (roundtripState.retrieveCount > roundtripState.maxRoundtrips) {
1367
+ throw buildRoundtripError("retrieve");
1368
+ }
1369
+ }
1370
+
1371
+ if (userOnAfterRetrieve) {
1372
+ await userOnAfterRetrieve(uow, results);
1373
+ }
1374
+ }
1375
+ : userOnAfterRetrieve;
1376
+
1377
+ const builder = createHandlerTxBuilder<THooks>({
739
1378
  ...execOptions,
740
1379
  createUnitOfWork: () => {
741
1380
  currentStorage.uow.reset();
@@ -748,28 +1387,110 @@ export class DatabaseFragmentDefinitionBuilder<
748
1387
  return currentStorage.uow;
749
1388
  },
750
1389
  onBeforeMutate: (uow) => {
751
- if (hooksConfig) {
752
- prepareHookMutations(uow, hooksConfig);
1390
+ if (internalFragment && !planMode) {
1391
+ prepareHookMutations(uow, internalFragment, hooksConfig?.defaultRetryPolicy);
753
1392
  }
754
1393
  if (userOnBeforeMutate) {
755
1394
  userOnBeforeMutate(uow);
756
1395
  }
1396
+ if (
1397
+ roundtripState &&
1398
+ roundtripExecutionState &&
1399
+ !roundtripExecutionState.countedMutate &&
1400
+ uow.getMutationOperations().length > 0
1401
+ ) {
1402
+ roundtripExecutionState.countedMutate = true;
1403
+ roundtripState.mutateCount += 1;
1404
+ if (roundtripState.mutateCount > roundtripState.maxRoundtrips) {
1405
+ throw buildRoundtripError("mutate");
1406
+ }
1407
+ }
757
1408
  },
1409
+ onAfterRetrieve: guardOnAfterRetrieve,
758
1410
  onAfterMutate: async (uow) => {
759
- if (hooksConfig?.scheduler) {
760
- void hooksConfig.scheduler.schedule().catch((error) => {
761
- console.error("Durable hooks processing failed", error);
762
- });
1411
+ notifyDurableHooksAfterMutate({
1412
+ uow,
1413
+ hooksConfig,
1414
+ internalFragment,
1415
+ autoSchedule,
1416
+ planMode,
1417
+ logContext: { route: routeLabel, source: "request", waitUntil: routeWaitUntil },
1418
+ });
1419
+ if (hooksConfig && !planMode) {
1420
+ const runtimeState = getDurableHooksRuntimeByConfig(hooksConfig);
1421
+ if (
1422
+ runtimeState &&
1423
+ !runtimeState.dispatcherRegistered &&
1424
+ !runtimeState.dispatcherWarningEmitted
1425
+ ) {
1426
+ const hasHooks = uow
1427
+ .getTriggeredHooks()
1428
+ .some((hook) => hook.namespace === hooksConfig.namespace);
1429
+ if (hasHooks) {
1430
+ runtimeState.dispatcherWarningEmitted = true;
1431
+ DurableHooksLogger.warn("Durable hooks dispatcher not configured for fragment", {
1432
+ namespace: hooksConfig.namespace,
1433
+ fields: {
1434
+ guidance:
1435
+ "Hooks will only run during requests; scheduled/retry hooks may stall. " +
1436
+ "Create a dispatcher with createDurableHooksProcessor([...]) from " +
1437
+ "`@fragno-dev/db/dispatchers/node` or `@fragno-dev/db/dispatchers/cloudflare-do`.",
1438
+ },
1439
+ });
1440
+ }
1441
+ }
763
1442
  }
764
1443
  if (userOnAfterMutate) {
765
1444
  await userOnAfterMutate(uow);
766
1445
  }
767
1446
  },
768
1447
  });
1448
+
1449
+ if (roundtripState) {
1450
+ const guardedBuilder = wrapHandlerTxBuilderWithRoundtripGuard(
1451
+ builder,
1452
+ resetRoundtripExecutionState,
1453
+ );
1454
+ return guardedBuilder;
1455
+ }
1456
+
1457
+ return builder;
1458
+ }
1459
+
1460
+ function callServices<
1461
+ TServiceCalls extends
1462
+ | TxResult<unknown, unknown>
1463
+ | undefined
1464
+ | readonly (TxResult<unknown, unknown> | undefined)[],
1465
+ >(
1466
+ serviceCalls: () => TServiceCalls,
1467
+ ): Promise<ExtractServiceFinalResultOrSingle<TServiceCalls>> {
1468
+ let callWasArray = false;
1469
+ const resultPromise = handlerTx()
1470
+ .withServiceCalls(() => {
1471
+ const calls = serviceCalls();
1472
+ callWasArray = Array.isArray(calls);
1473
+ return (callWasArray ? calls : [calls]) as readonly (
1474
+ | TxResult<unknown, unknown>
1475
+ | undefined
1476
+ )[];
1477
+ })
1478
+ .execute();
1479
+
1480
+ return resultPromise.then((result) =>
1481
+ callWasArray
1482
+ ? (result as ExtractServiceFinalResultOrSingle<TServiceCalls>)
1483
+ : (
1484
+ result as AwaitedPromisesInObject<
1485
+ ExtractServiceFinalResults<readonly [TServiceCalls]>
1486
+ >
1487
+ )[0],
1488
+ ) as Promise<ExtractServiceFinalResultOrSingle<TServiceCalls>>;
769
1489
  }
770
1490
 
771
1491
  const handlerContext: DatabaseHandlerContext<THooks> = {
772
1492
  handlerTx,
1493
+ callServices,
773
1494
  };
774
1495
 
775
1496
  return { serviceContext, handlerContext };
@@ -778,13 +1499,39 @@ export class DatabaseFragmentDefinitionBuilder<
778
1499
  // Build the final definition
779
1500
  const finalDef = builderWithContext.build();
780
1501
  if (this.#hooksFactory) {
781
- finalDef.internalDataFactory = ({ config, options, deps }) => ({
782
- durableHooks: createHooksConfig({
1502
+ finalDef.internalDataFactory = ({ config, options, deps, services, serviceDeps }) => {
1503
+ const hooksConfig = createHooksConfig({
783
1504
  config: config as TConfig,
784
1505
  options: options as FragnoPublicConfigWithDatabase,
785
1506
  deps: deps as TDeps,
786
- }),
787
- });
1507
+ services: services as BoundServices<TBaseServices & TServices>,
1508
+ serviceDeps: serviceDeps as TServiceDependencies,
1509
+ });
1510
+ if (!hooksConfig) {
1511
+ return {};
1512
+ }
1513
+ return {
1514
+ durableHooksToken: registerDurableHooksRuntime(hooksConfig),
1515
+ };
1516
+ };
1517
+ }
1518
+ if (this.#registryResolver) {
1519
+ const registryInternalRoutes = ({ deps }: { deps: TDeps }) => {
1520
+ const databaseAdapter = (deps as ImplicitDatabaseDependencies<TSchema>).databaseAdapter;
1521
+ if (!databaseAdapter) {
1522
+ throw new Error("Database adapter is missing for internal routes.");
1523
+ }
1524
+ const internalFragment = this.#registryResolver!.getInternalFragment(databaseAdapter);
1525
+ if (!internalFragment) {
1526
+ return [];
1527
+ }
1528
+ return (internalFragment.routes ?? []) as readonly AnyFragnoRouteConfig[];
1529
+ };
1530
+ const mergedInternalRoutes = [
1531
+ ...(finalDef.internalRoutes ?? []),
1532
+ registryInternalRoutes,
1533
+ ] as readonly AnyRouteOrFactory[];
1534
+ finalDef.internalRoutes = mergedInternalRoutes as TInternalRoutes;
788
1535
  }
789
1536
 
790
1537
  // Return the complete definition with proper typing and dependencies