@effect-app/infra 4.0.0-beta.26 → 4.0.0-beta.261

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 (505) hide show
  1. package/CHANGELOG.md +1973 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +30 -11
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +35 -14
  6. package/dist/ClusterCosmos.d.ts +64 -0
  7. package/dist/ClusterCosmos.d.ts.map +1 -0
  8. package/dist/ClusterCosmos.js +501 -0
  9. package/dist/ClusterServiceBus.d.ts +67 -0
  10. package/dist/ClusterServiceBus.d.ts.map +1 -0
  11. package/dist/ClusterServiceBus.js +82 -0
  12. package/dist/ContextProvider.d.ts +34 -0
  13. package/dist/ContextProvider.d.ts.map +1 -0
  14. package/dist/ContextProvider.js +40 -0
  15. package/dist/Emailer/Sendgrid.d.ts +111 -147
  16. package/dist/Emailer/Sendgrid.d.ts.map +1 -1
  17. package/dist/Emailer/Sendgrid.js +24 -19
  18. package/dist/Emailer/fake.d.ts +2 -2
  19. package/dist/Emailer/fake.d.ts.map +1 -1
  20. package/dist/Emailer/fake.js +4 -4
  21. package/dist/MainFiberSet.d.ts +12 -9
  22. package/dist/MainFiberSet.d.ts.map +1 -1
  23. package/dist/MainFiberSet.js +10 -6
  24. package/dist/QueueMaker/SQLQueue.d.ts +8 -9
  25. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  26. package/dist/QueueMaker/SQLQueue.js +138 -120
  27. package/dist/QueueMaker/errors.d.ts +5 -3
  28. package/dist/QueueMaker/errors.d.ts.map +1 -1
  29. package/dist/QueueMaker/errors.js +4 -2
  30. package/dist/QueueMaker/memQueue.d.ts +10 -6
  31. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  32. package/dist/QueueMaker/memQueue.js +84 -68
  33. package/dist/QueueMaker/sbqueue.d.ts +9 -5
  34. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  35. package/dist/QueueMaker/sbqueue.js +60 -58
  36. package/dist/RequestFiberSet.d.ts +10 -7
  37. package/dist/RequestFiberSet.d.ts.map +1 -1
  38. package/dist/RequestFiberSet.js +13 -8
  39. package/dist/SQL/Model.d.ts +468 -0
  40. package/dist/SQL/Model.d.ts.map +1 -0
  41. package/dist/SQL/Model.js +469 -0
  42. package/dist/SQL.d.ts +2 -0
  43. package/dist/SQL.d.ts.map +1 -0
  44. package/dist/{adapters/SQL.js → SQL.js} +1 -1
  45. package/dist/ServiceBus.d.ts +61 -0
  46. package/dist/ServiceBus.d.ts.map +1 -0
  47. package/dist/ServiceBus.js +108 -0
  48. package/dist/Store/Cosmos/query.d.ts +15 -4
  49. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  50. package/dist/Store/Cosmos/query.js +179 -41
  51. package/dist/Store/Cosmos.d.ts +3 -3
  52. package/dist/Store/Cosmos.d.ts.map +1 -1
  53. package/dist/Store/Cosmos.js +344 -246
  54. package/dist/Store/Disk.d.ts +5 -5
  55. package/dist/Store/Disk.d.ts.map +1 -1
  56. package/dist/Store/Disk.js +78 -38
  57. package/dist/Store/Memory.d.ts +7 -10
  58. package/dist/Store/Memory.d.ts.map +1 -1
  59. package/dist/Store/Memory.js +326 -66
  60. package/dist/Store/SQL/Pg.d.ts +4 -0
  61. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  62. package/dist/Store/SQL/Pg.js +232 -0
  63. package/dist/Store/SQL/query.d.ts +49 -0
  64. package/dist/Store/SQL/query.d.ts.map +1 -0
  65. package/dist/Store/SQL/query.js +527 -0
  66. package/dist/Store/SQL.d.ts +21 -0
  67. package/dist/Store/SQL.d.ts.map +1 -0
  68. package/dist/Store/SQL.js +449 -0
  69. package/dist/Store/codeFilter.d.ts +5 -5
  70. package/dist/Store/codeFilter.d.ts.map +1 -1
  71. package/dist/Store/codeFilter.js +6 -3
  72. package/dist/Store/index.d.ts +7 -5
  73. package/dist/Store/index.d.ts.map +1 -1
  74. package/dist/Store/index.js +18 -5
  75. package/dist/Store/utils.d.ts +4 -3
  76. package/dist/Store/utils.d.ts.map +1 -1
  77. package/dist/Store/utils.js +5 -5
  78. package/dist/WorkflowEngineCosmos.d.ts +29 -0
  79. package/dist/WorkflowEngineCosmos.d.ts.map +1 -0
  80. package/dist/WorkflowEngineCosmos.js +521 -0
  81. package/dist/WorkflowEngineSqlite.d.ts +24 -0
  82. package/dist/WorkflowEngineSqlite.d.ts.map +1 -0
  83. package/dist/WorkflowEngineSqlite.js +550 -0
  84. package/dist/arbs.d.ts +2 -2
  85. package/dist/arbs.d.ts.map +1 -1
  86. package/dist/arbs.js +5 -3
  87. package/dist/codec.d.ts +5 -0
  88. package/dist/codec.d.ts.map +1 -0
  89. package/dist/codec.js +5 -0
  90. package/dist/cosmos-client.d.ts +16 -0
  91. package/dist/cosmos-client.d.ts.map +1 -0
  92. package/dist/cosmos-client.js +11 -0
  93. package/dist/errorReporter.d.ts +7 -5
  94. package/dist/errorReporter.d.ts.map +1 -1
  95. package/dist/errorReporter.js +23 -27
  96. package/dist/errors.d.ts +1 -1
  97. package/dist/fileUtil.d.ts +2 -2
  98. package/dist/fileUtil.d.ts.map +1 -1
  99. package/dist/fileUtil.js +2 -2
  100. package/dist/index.d.ts +3 -2
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +3 -2
  103. package/dist/internal/RequestContextMiddleware.d.ts +5 -0
  104. package/dist/internal/RequestContextMiddleware.d.ts.map +1 -0
  105. package/dist/internal/RequestContextMiddleware.js +45 -0
  106. package/dist/internal/auth.d.ts +53 -0
  107. package/dist/internal/auth.d.ts.map +1 -0
  108. package/dist/internal/auth.js +180 -0
  109. package/dist/internal/events.d.ts +11 -0
  110. package/dist/internal/events.d.ts.map +1 -0
  111. package/dist/internal/events.js +49 -0
  112. package/dist/internal/health.d.ts +3 -0
  113. package/dist/internal/health.d.ts.map +1 -0
  114. package/dist/internal/health.js +5 -0
  115. package/dist/layerUtils.d.ts +32 -0
  116. package/dist/layerUtils.d.ts.map +1 -0
  117. package/dist/layerUtils.js +17 -0
  118. package/dist/logger/jsonLogger.d.ts +2 -2
  119. package/dist/logger/jsonLogger.d.ts.map +1 -1
  120. package/dist/logger/jsonLogger.js +5 -3
  121. package/dist/logger/logFmtLogger.d.ts +2 -2
  122. package/dist/logger/logFmtLogger.d.ts.map +1 -1
  123. package/dist/logger/logFmtLogger.js +3 -3
  124. package/dist/logger/shared.d.ts +3 -3
  125. package/dist/logger/shared.d.ts.map +1 -1
  126. package/dist/logger/shared.js +5 -5
  127. package/dist/logger.d.ts +1 -1
  128. package/dist/logger.d.ts.map +1 -1
  129. package/dist/memQueue.d.ts +15 -0
  130. package/dist/memQueue.d.ts.map +1 -0
  131. package/dist/memQueue.js +21 -0
  132. package/dist/middlewares.d.ts +10 -0
  133. package/dist/middlewares.d.ts.map +1 -0
  134. package/dist/{api/middlewares.js → middlewares.js} +1 -1
  135. package/dist/mongo-client.d.ts +11 -0
  136. package/dist/mongo-client.d.ts.map +1 -0
  137. package/dist/mongo-client.js +15 -0
  138. package/dist/otel.d.ts +75 -0
  139. package/dist/otel.d.ts.map +1 -0
  140. package/dist/otel.js +65 -0
  141. package/dist/rateLimit.d.ts +12 -4
  142. package/dist/rateLimit.d.ts.map +1 -1
  143. package/dist/rateLimit.js +7 -12
  144. package/dist/redis-client.d.ts +42 -0
  145. package/dist/redis-client.d.ts.map +1 -0
  146. package/dist/redis-client.js +98 -0
  147. package/dist/reportError.d.ts +4 -0
  148. package/dist/reportError.d.ts.map +1 -0
  149. package/dist/reportError.js +28 -0
  150. package/dist/routing/middleware/RouterMiddleware.d.ts +16 -0
  151. package/dist/routing/middleware/RouterMiddleware.d.ts.map +1 -0
  152. package/dist/{api/routing → routing}/middleware/RouterMiddleware.js +1 -1
  153. package/dist/routing/middleware/middleware.d.ts +48 -0
  154. package/dist/routing/middleware/middleware.d.ts.map +1 -0
  155. package/dist/routing/middleware/middleware.js +128 -0
  156. package/dist/routing/middleware.d.ts +3 -0
  157. package/dist/routing/middleware.d.ts.map +1 -0
  158. package/dist/{api/routing → routing}/middleware.js +1 -2
  159. package/dist/routing/schema/jwt.d.ts +4 -0
  160. package/dist/routing/schema/jwt.d.ts.map +1 -0
  161. package/dist/routing/schema/jwt.js +13 -0
  162. package/dist/routing/tsort.d.ts +8 -0
  163. package/dist/routing/tsort.d.ts.map +1 -0
  164. package/dist/routing/tsort.js +51 -0
  165. package/dist/routing/utils.d.ts +19 -0
  166. package/dist/routing/utils.d.ts.map +1 -0
  167. package/dist/routing/utils.js +45 -0
  168. package/dist/routing.d.ts +184 -0
  169. package/dist/routing.d.ts.map +1 -0
  170. package/dist/routing.js +236 -0
  171. package/dist/test.d.ts +3 -3
  172. package/dist/test.d.ts.map +1 -1
  173. package/dist/test.js +2 -2
  174. package/dist/util.d.ts +3 -0
  175. package/dist/util.d.ts.map +1 -0
  176. package/dist/util.js +14 -0
  177. package/dist/vitest.d.ts +1 -1
  178. package/docs/cluster-storage.md +26 -0
  179. package/docs/workflow-engine.md +262 -0
  180. package/examples/query.ts +47 -39
  181. package/package.json +31 -345
  182. package/run.sh +1 -0
  183. package/src/CUPS.ts +52 -13
  184. package/src/ClusterCosmos.ts +984 -0
  185. package/src/ClusterServiceBus.ts +242 -0
  186. package/src/{api/ContextProvider.ts → ContextProvider.ts} +19 -16
  187. package/src/Emailer/Sendgrid.ts +82 -59
  188. package/src/Emailer/fake.ts +3 -3
  189. package/src/MainFiberSet.ts +12 -10
  190. package/src/QueueMaker/SQLQueue.ts +153 -156
  191. package/src/QueueMaker/errors.ts +3 -1
  192. package/src/QueueMaker/memQueue.ts +113 -107
  193. package/src/QueueMaker/sbqueue.ts +78 -90
  194. package/src/RequestFiberSet.ts +13 -8
  195. package/src/{adapters/SQL → SQL}/Model.ts +42 -41
  196. package/src/ServiceBus.ts +219 -0
  197. package/src/Store/Cosmos/query.ts +216 -52
  198. package/src/Store/Cosmos.ts +493 -353
  199. package/src/Store/Disk.ts +109 -69
  200. package/src/Store/Memory.ts +365 -96
  201. package/src/Store/SQL/Pg.ts +363 -0
  202. package/src/Store/SQL/query.ts +603 -0
  203. package/src/Store/SQL.ts +735 -0
  204. package/src/Store/codeFilter.ts +8 -5
  205. package/src/Store/index.ts +21 -6
  206. package/src/Store/utils.ts +26 -24
  207. package/src/WorkflowEngineCosmos.ts +719 -0
  208. package/src/WorkflowEngineSqlite.ts +813 -0
  209. package/src/arbs.ts +5 -3
  210. package/src/{adapters/cosmos-client.ts → cosmos-client.ts} +5 -3
  211. package/src/errorReporter.ts +66 -76
  212. package/src/fileUtil.ts +1 -1
  213. package/src/index.ts +2 -1
  214. package/src/{api/internal → internal}/RequestContextMiddleware.ts +23 -6
  215. package/src/internal/auth.ts +272 -0
  216. package/src/{api/internal → internal}/events.ts +22 -13
  217. package/src/{api/layerUtils.ts → layerUtils.ts} +14 -10
  218. package/src/logger/jsonLogger.ts +4 -2
  219. package/src/logger/logFmtLogger.ts +2 -2
  220. package/src/logger/shared.ts +5 -4
  221. package/src/{adapters/memQueue.ts → memQueue.ts} +5 -4
  222. package/src/{adapters/mongo-client.ts → mongo-client.ts} +4 -2
  223. package/src/otel.ts +152 -0
  224. package/src/rateLimit.ts +34 -23
  225. package/src/{adapters/redis-client.ts → redis-client.ts} +7 -3
  226. package/src/{api/reportError.ts → reportError.ts} +3 -2
  227. package/src/{api/routing → routing}/middleware/RouterMiddleware.ts +5 -4
  228. package/src/{api/routing → routing}/middleware/middleware.ts +62 -17
  229. package/src/routing/middleware.ts +4 -0
  230. package/src/{api/routing → routing}/schema/jwt.ts +2 -1
  231. package/src/{api/routing → routing}/utils.ts +2 -1
  232. package/src/routing.ts +768 -0
  233. package/src/test.ts +2 -2
  234. package/test/auth.test.ts +101 -0
  235. package/test/cluster-cosmos.test.ts +590 -0
  236. package/test/cluster-servicebus.test.ts +180 -0
  237. package/test/cluster-sqlite.test.ts +207 -0
  238. package/test/contextProvider.test.ts +15 -12
  239. package/test/controller.test.ts +28 -32
  240. package/test/cosmos-query.test.ts +159 -0
  241. package/test/dist/_check-agg-infer.test-d.d.ts +2 -0
  242. package/test/dist/_check-agg-infer.test-d.d.ts.map +1 -0
  243. package/test/dist/_check-agg-infer.test-d.js +19 -0
  244. package/test/dist/_check-proj-infer.test-d.d.ts +2 -0
  245. package/test/dist/_check-proj-infer.test-d.d.ts.map +1 -0
  246. package/test/dist/_check-proj-infer.test-d.js +16 -0
  247. package/test/dist/_check-tighten.test-d.d.ts +2 -0
  248. package/test/dist/_check-tighten.test-d.d.ts.map +1 -0
  249. package/test/dist/_check-tighten.test-d.js +21 -0
  250. package/test/dist/auth.test.d.ts.map +1 -0
  251. package/test/dist/cluster-cosmos.test.d.ts.map +1 -0
  252. package/test/dist/cluster-servicebus.test.d.ts.map +1 -0
  253. package/test/dist/cluster-sqlite.test.d.ts.map +1 -0
  254. package/test/dist/contextProvider.test.d.ts.map +1 -1
  255. package/test/dist/controller.test.d.ts.map +1 -1
  256. package/test/dist/cosmos-query.test.d.ts.map +1 -0
  257. package/test/dist/date-query.test.d.ts.map +1 -0
  258. package/test/dist/fixtures.d.ts +30 -12
  259. package/test/dist/fixtures.d.ts.map +1 -1
  260. package/test/dist/fixtures.js +17 -10
  261. package/test/dist/query.test.d.ts.map +1 -1
  262. package/test/dist/rawQuery.test.d.ts.map +1 -1
  263. package/test/dist/repository-ext.test.d.ts.map +1 -0
  264. package/test/dist/requires.test.d.ts.map +1 -1
  265. package/test/dist/router-generator.test.d.ts.map +1 -0
  266. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  267. package/test/dist/rpc-context-map-streaming.test.d.ts.map +1 -0
  268. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  269. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  270. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  271. package/test/dist/sql-store.test.d.ts.map +1 -0
  272. package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
  273. package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
  274. package/test/fixtures.ts +16 -9
  275. package/test/layerUtils.test.ts +2 -2
  276. package/test/query.test.ts +905 -40
  277. package/test/rawQuery.test.ts +340 -22
  278. package/test/repository-ext.test.ts +62 -0
  279. package/test/requires.test.ts +10 -5
  280. package/test/router-generator.test.ts +187 -0
  281. package/test/routing-interruptibility.test.ts +66 -0
  282. package/test/rpc-context-map-streaming.test.ts +262 -0
  283. package/test/rpc-e2e-invalidation.test.ts +256 -0
  284. package/test/rpc-multi-middleware.test.ts +85 -10
  285. package/test/rpc-stream-fullstack.test.ts +304 -0
  286. package/test/sql-store.test.ts +1711 -0
  287. package/test/validateSample.test.ts +26 -14
  288. package/test/workflow-engine-cosmos.test.ts +354 -0
  289. package/test/workflow-engine-sqlite.test.ts +299 -0
  290. package/tsconfig.examples.json +1 -1
  291. package/tsconfig.json +2 -1
  292. package/tsconfig.json.bak +2 -2
  293. package/tsconfig.src.json +35 -35
  294. package/tsconfig.test.json +2 -2
  295. package/dist/Emailer/service.d.ts +0 -55
  296. package/dist/Emailer/service.d.ts.map +0 -1
  297. package/dist/Emailer/service.js +0 -6
  298. package/dist/Emailer.d.ts +0 -2
  299. package/dist/Emailer.d.ts.map +0 -1
  300. package/dist/Emailer.js +0 -2
  301. package/dist/Model/Repository/ext.d.ts +0 -41
  302. package/dist/Model/Repository/ext.d.ts.map +0 -1
  303. package/dist/Model/Repository/ext.js +0 -65
  304. package/dist/Model/Repository/internal/internal.d.ts +0 -59
  305. package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
  306. package/dist/Model/Repository/internal/internal.js +0 -316
  307. package/dist/Model/Repository/legacy.d.ts +0 -19
  308. package/dist/Model/Repository/legacy.d.ts.map +0 -1
  309. package/dist/Model/Repository/legacy.js +0 -2
  310. package/dist/Model/Repository/makeRepo.d.ts +0 -49
  311. package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
  312. package/dist/Model/Repository/makeRepo.js +0 -24
  313. package/dist/Model/Repository/service.d.ts +0 -89
  314. package/dist/Model/Repository/service.d.ts.map +0 -1
  315. package/dist/Model/Repository/service.js +0 -2
  316. package/dist/Model/Repository/validation.d.ts +0 -42
  317. package/dist/Model/Repository/validation.d.ts.map +0 -1
  318. package/dist/Model/Repository/validation.js +0 -32
  319. package/dist/Model/Repository.d.ts +0 -6
  320. package/dist/Model/Repository.d.ts.map +0 -1
  321. package/dist/Model/Repository.js +0 -6
  322. package/dist/Model/dsl.d.ts +0 -32
  323. package/dist/Model/dsl.d.ts.map +0 -1
  324. package/dist/Model/dsl.js +0 -44
  325. package/dist/Model/filter/filterApi.d.ts +0 -30
  326. package/dist/Model/filter/filterApi.d.ts.map +0 -1
  327. package/dist/Model/filter/filterApi.js +0 -2
  328. package/dist/Model/filter/types/errors.d.ts +0 -29
  329. package/dist/Model/filter/types/errors.d.ts.map +0 -1
  330. package/dist/Model/filter/types/errors.js +0 -2
  331. package/dist/Model/filter/types/fields.d.ts +0 -15
  332. package/dist/Model/filter/types/fields.d.ts.map +0 -1
  333. package/dist/Model/filter/types/fields.js +0 -2
  334. package/dist/Model/filter/types/path/common.d.ts +0 -316
  335. package/dist/Model/filter/types/path/common.d.ts.map +0 -1
  336. package/dist/Model/filter/types/path/common.js +0 -2
  337. package/dist/Model/filter/types/path/eager.d.ts +0 -95
  338. package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
  339. package/dist/Model/filter/types/path/eager.js +0 -31
  340. package/dist/Model/filter/types/path/index.d.ts +0 -4
  341. package/dist/Model/filter/types/path/index.d.ts.map +0 -1
  342. package/dist/Model/filter/types/path/index.js +0 -3
  343. package/dist/Model/filter/types/utils.d.ts +0 -79
  344. package/dist/Model/filter/types/utils.d.ts.map +0 -1
  345. package/dist/Model/filter/types/utils.js +0 -2
  346. package/dist/Model/filter/types/validator.d.ts +0 -30
  347. package/dist/Model/filter/types/validator.d.ts.map +0 -1
  348. package/dist/Model/filter/types/validator.js +0 -2
  349. package/dist/Model/filter/types.d.ts +0 -5
  350. package/dist/Model/filter/types.d.ts.map +0 -1
  351. package/dist/Model/filter/types.js +0 -7
  352. package/dist/Model/query/dsl.d.ts +0 -248
  353. package/dist/Model/query/dsl.d.ts.map +0 -1
  354. package/dist/Model/query/dsl.js +0 -104
  355. package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
  356. package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
  357. package/dist/Model/query/new-kid-interpreter.js +0 -165
  358. package/dist/Model/query.d.ts +0 -15
  359. package/dist/Model/query.d.ts.map +0 -1
  360. package/dist/Model/query.js +0 -3
  361. package/dist/Model.d.ts +0 -4
  362. package/dist/Model.d.ts.map +0 -1
  363. package/dist/Model.js +0 -4
  364. package/dist/Operations.d.ts +0 -55
  365. package/dist/Operations.d.ts.map +0 -1
  366. package/dist/Operations.js +0 -102
  367. package/dist/OperationsRepo.d.ts +0 -41
  368. package/dist/OperationsRepo.d.ts.map +0 -1
  369. package/dist/OperationsRepo.js +0 -14
  370. package/dist/QueueMaker/service.d.ts +0 -11
  371. package/dist/QueueMaker/service.d.ts.map +0 -1
  372. package/dist/QueueMaker/service.js +0 -4
  373. package/dist/RequestContext.d.ts +0 -63
  374. package/dist/RequestContext.d.ts.map +0 -1
  375. package/dist/RequestContext.js +0 -49
  376. package/dist/Store/ContextMapContainer.d.ts +0 -14
  377. package/dist/Store/ContextMapContainer.d.ts.map +0 -1
  378. package/dist/Store/ContextMapContainer.js +0 -16
  379. package/dist/Store/service.d.ts +0 -108
  380. package/dist/Store/service.d.ts.map +0 -1
  381. package/dist/Store/service.js +0 -71
  382. package/dist/Store.d.ts +0 -2
  383. package/dist/Store.d.ts.map +0 -1
  384. package/dist/Store.js +0 -2
  385. package/dist/adapters/SQL/Model.d.ts +0 -479
  386. package/dist/adapters/SQL/Model.d.ts.map +0 -1
  387. package/dist/adapters/SQL/Model.js +0 -478
  388. package/dist/adapters/SQL.d.ts +0 -2
  389. package/dist/adapters/SQL.d.ts.map +0 -1
  390. package/dist/adapters/ServiceBus.d.ts +0 -58
  391. package/dist/adapters/ServiceBus.d.ts.map +0 -1
  392. package/dist/adapters/ServiceBus.js +0 -99
  393. package/dist/adapters/cosmos-client.d.ts +0 -14
  394. package/dist/adapters/cosmos-client.d.ts.map +0 -1
  395. package/dist/adapters/cosmos-client.js +0 -9
  396. package/dist/adapters/index.d.ts +0 -2
  397. package/dist/adapters/index.d.ts.map +0 -1
  398. package/dist/adapters/index.js +0 -2
  399. package/dist/adapters/logger.d.ts +0 -9
  400. package/dist/adapters/logger.d.ts.map +0 -1
  401. package/dist/adapters/logger.js +0 -3
  402. package/dist/adapters/memQueue.d.ts +0 -13
  403. package/dist/adapters/memQueue.d.ts.map +0 -1
  404. package/dist/adapters/memQueue.js +0 -20
  405. package/dist/adapters/mongo-client.d.ts +0 -10
  406. package/dist/adapters/mongo-client.d.ts.map +0 -1
  407. package/dist/adapters/mongo-client.js +0 -13
  408. package/dist/adapters/redis-client.d.ts +0 -39
  409. package/dist/adapters/redis-client.d.ts.map +0 -1
  410. package/dist/adapters/redis-client.js +0 -94
  411. package/dist/api/ContextProvider.d.ts +0 -31
  412. package/dist/api/ContextProvider.d.ts.map +0 -1
  413. package/dist/api/ContextProvider.js +0 -38
  414. package/dist/api/codec.d.ts +0 -5
  415. package/dist/api/codec.d.ts.map +0 -1
  416. package/dist/api/codec.js +0 -5
  417. package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
  418. package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
  419. package/dist/api/internal/RequestContextMiddleware.js +0 -35
  420. package/dist/api/internal/auth.d.ts +0 -15
  421. package/dist/api/internal/auth.d.ts.map +0 -1
  422. package/dist/api/internal/auth.js +0 -47
  423. package/dist/api/internal/events.d.ts +0 -9
  424. package/dist/api/internal/events.d.ts.map +0 -1
  425. package/dist/api/internal/events.js +0 -42
  426. package/dist/api/internal/health.d.ts +0 -3
  427. package/dist/api/internal/health.d.ts.map +0 -1
  428. package/dist/api/internal/health.js +0 -5
  429. package/dist/api/layerUtils.d.ts +0 -24
  430. package/dist/api/layerUtils.d.ts.map +0 -1
  431. package/dist/api/layerUtils.js +0 -16
  432. package/dist/api/middlewares.d.ts +0 -10
  433. package/dist/api/middlewares.d.ts.map +0 -1
  434. package/dist/api/reportError.d.ts +0 -4
  435. package/dist/api/reportError.d.ts.map +0 -1
  436. package/dist/api/reportError.js +0 -27
  437. package/dist/api/routing/middleware/RouterMiddleware.d.ts +0 -15
  438. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
  439. package/dist/api/routing/middleware/middleware.d.ts +0 -9
  440. package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
  441. package/dist/api/routing/middleware/middleware.js +0 -92
  442. package/dist/api/routing/middleware.d.ts +0 -4
  443. package/dist/api/routing/middleware.d.ts.map +0 -1
  444. package/dist/api/routing/schema/jwt.d.ts +0 -4
  445. package/dist/api/routing/schema/jwt.d.ts.map +0 -1
  446. package/dist/api/routing/schema/jwt.js +0 -12
  447. package/dist/api/routing/tsort.d.ts +0 -8
  448. package/dist/api/routing/tsort.d.ts.map +0 -1
  449. package/dist/api/routing/tsort.js +0 -51
  450. package/dist/api/routing/utils.d.ts +0 -19
  451. package/dist/api/routing/utils.d.ts.map +0 -1
  452. package/dist/api/routing/utils.js +0 -44
  453. package/dist/api/routing.d.ts +0 -138
  454. package/dist/api/routing.d.ts.map +0 -1
  455. package/dist/api/routing.js +0 -166
  456. package/dist/api/setupRequest.d.ts +0 -12
  457. package/dist/api/setupRequest.d.ts.map +0 -1
  458. package/dist/api/setupRequest.js +0 -44
  459. package/dist/api/util.d.ts +0 -3
  460. package/dist/api/util.d.ts.map +0 -1
  461. package/dist/api/util.js +0 -14
  462. package/eslint.config.mjs +0 -24
  463. package/src/Emailer/service.ts +0 -52
  464. package/src/Emailer.ts +0 -1
  465. package/src/Model/Repository/ext.ts +0 -283
  466. package/src/Model/Repository/internal/internal.ts +0 -577
  467. package/src/Model/Repository/legacy.ts +0 -27
  468. package/src/Model/Repository/makeRepo.ts +0 -139
  469. package/src/Model/Repository/service.ts +0 -627
  470. package/src/Model/Repository/validation.ts +0 -31
  471. package/src/Model/Repository.ts +0 -5
  472. package/src/Model/dsl.ts +0 -128
  473. package/src/Model/filter/filterApi.ts +0 -60
  474. package/src/Model/filter/types/errors.ts +0 -47
  475. package/src/Model/filter/types/fields.ts +0 -50
  476. package/src/Model/filter/types/path/common.ts +0 -404
  477. package/src/Model/filter/types/path/eager.ts +0 -298
  478. package/src/Model/filter/types/path/index.ts +0 -4
  479. package/src/Model/filter/types/utils.ts +0 -128
  480. package/src/Model/filter/types/validator.ts +0 -46
  481. package/src/Model/filter/types.ts +0 -6
  482. package/src/Model/query/dsl.ts +0 -2110
  483. package/src/Model/query/new-kid-interpreter.ts +0 -210
  484. package/src/Model/query.ts +0 -13
  485. package/src/Model.ts +0 -3
  486. package/src/Operations.ts +0 -235
  487. package/src/OperationsRepo.ts +0 -16
  488. package/src/QueueMaker/service.ts +0 -17
  489. package/src/RequestContext.ts +0 -63
  490. package/src/Store/ContextMapContainer.ts +0 -20
  491. package/src/Store/service.ts +0 -184
  492. package/src/Store.ts +0 -1
  493. package/src/adapters/ServiceBus.ts +0 -209
  494. package/src/adapters/index.ts +0 -0
  495. package/src/adapters/logger.ts +0 -3
  496. package/src/api/internal/auth.ts +0 -68
  497. package/src/api/routing/middleware.ts +0 -6
  498. package/src/api/routing.ts +0 -598
  499. package/src/api/setupRequest.ts +0 -84
  500. /package/src/{adapters/SQL.ts → SQL.ts} +0 -0
  501. /package/src/{api/codec.ts → codec.ts} +0 -0
  502. /package/src/{api/internal → internal}/health.ts +0 -0
  503. /package/src/{api/middlewares.ts → middlewares.ts} +0 -0
  504. /package/src/{api/routing → routing}/tsort.ts +0 -0
  505. /package/src/{api/util.ts → util.ts} +0 -0
@@ -0,0 +1,719 @@
1
+ /**
2
+ * Cosmos DB backed {@link WorkflowEngine} implementation.
3
+ *
4
+ * Persists workflow state in a single container partitioned by `executionId`
5
+ * so per-execution writes share a partition key (eligible for Cosmos
6
+ * TransactionalBatch). Optimistic concurrency is enforced with `_etag` +
7
+ * `IfMatch` on Replace, and create-only batch ops give first-writer-wins
8
+ * semantics for activity results and durable-deferred completions.
9
+ *
10
+ * Durability — everything that crosses the storage boundary is round-tripped
11
+ * through schema codecs (`S.fromJsonString(S.toCodecJson(...))`), exactly like
12
+ * the cluster engine, instead of dumping live runtime objects as JSON:
13
+ *
14
+ * - The workflow payload and the top-level workflow result are encoded with the
15
+ * workflow's own `payloadSchema` / `successSchema` / `errorSchema`, so typed
16
+ * values (dates, branded ids, schema classes) survive a restart.
17
+ * - Activity results flow through the engine already encoded, so they are
18
+ * persisted with an opaque `Workflow.Result({ success: AnyOrVoid, error:
19
+ * AnyOrVoid })` codec — same trick the cluster `ActivityRpc` uses.
20
+ * - Durable-deferred exits and clock completions use an opaque `Exit` codec.
21
+ *
22
+ * Crash recovery: each driver holds a time-bound lease (`worker` +
23
+ * `leaseExpiresAt`) on the exec doc and renews it via a heartbeat fiber. A
24
+ * scope-bound recovery poller queries for exec docs whose lease has lapsed and
25
+ * re-drives them in the local process, decoding the persisted payload and
26
+ * picking up persisted activity results from where the crashed driver left off.
27
+ *
28
+ * Durable clocks: `scheduleClock` writes a clock doc (`fireAt`, `deferredName`)
29
+ * and arms an in-process timer. A cross-partition clock poller fires any clock
30
+ * whose `fireAt` is due, completing the deferred idempotently (create-only) and
31
+ * deleting the doc. Survives restarts.
32
+ */
33
+ import * as Effect from "effect-app/Effect"
34
+ import * as Layer from "effect-app/Layer"
35
+ import * as Option from "effect-app/Option"
36
+ import * as S from "effect-app/Schema"
37
+ import * as Duration from "effect/Duration"
38
+ import * as Exit from "effect/Exit"
39
+ import * as Fiber from "effect/Fiber"
40
+ import * as FiberMap from "effect/FiberMap"
41
+ import * as Redacted from "effect/Redacted"
42
+ import * as Schedule from "effect/Schedule"
43
+ import type * as Scope from "effect/Scope"
44
+ import * as Workflow from "effect/unstable/workflow/Workflow"
45
+ import { type Encoded, makeUnsafe, WorkflowEngine, WorkflowInstance } from "effect/unstable/workflow/WorkflowEngine"
46
+ import { randomUUID } from "node:crypto"
47
+ import { CosmosClient, CosmosClientLayer } from "./cosmos-client.js"
48
+ import { OptimisticConcurrencyException } from "./errors.js"
49
+ import { annotateCosmosResponse, annotateDb } from "./otel.js"
50
+
51
+ export interface WorkflowEngineCosmosConfig {
52
+ readonly url: Redacted.Redacted<string>
53
+ readonly dbName: string
54
+ readonly prefix?: string
55
+ /** Lease duration before claim considered stale. Default 30s. */
56
+ readonly leaseTtl?: Duration.Duration
57
+ /** Renewal cadence — should be < leaseTtl. Default 10s. */
58
+ readonly heartbeatInterval?: Duration.Duration
59
+ /** Cadence for scanning stale leases. Default 15s. Set to `Duration.zero` to disable. */
60
+ readonly recoveryInterval?: Duration.Duration
61
+ /** Cadence for scanning due clocks. Default 5s. Set to `Duration.zero` to disable. */
62
+ readonly clockPollInterval?: Duration.Duration
63
+ /** Stable worker identity; defaults to a random UUID per process. */
64
+ readonly workerId?: string
65
+ }
66
+
67
+ type ExecStatus = "running" | "complete" | "interrupted"
68
+
69
+ interface ExecDoc {
70
+ readonly id: "exec"
71
+ readonly _partitionKey: string
72
+ readonly type: "exec"
73
+ readonly workflowName: string
74
+ /** Schema-encoded (JSON string) workflow payload. */
75
+ readonly payload: string
76
+ readonly parent: string | undefined
77
+ status: ExecStatus
78
+ suspended: boolean
79
+ interrupted: boolean
80
+ /** Schema-encoded (JSON string) top-level `Workflow.Result`, set on completion. */
81
+ completedResult?: string | undefined
82
+ worker?: string | undefined
83
+ leaseExpiresAt?: string | undefined
84
+ readonly _etag?: string
85
+ }
86
+
87
+ interface ActivityDoc {
88
+ readonly id: string
89
+ readonly _partitionKey: string
90
+ readonly type: "activity"
91
+ /** Schema-encoded (JSON string) `Workflow.Result`. */
92
+ readonly result: string
93
+ }
94
+
95
+ interface DeferredDoc {
96
+ readonly id: string
97
+ readonly _partitionKey: string
98
+ readonly type: "deferred"
99
+ /** Schema-encoded (JSON string) `Exit`. */
100
+ readonly exit: string
101
+ }
102
+
103
+ interface ClockDoc {
104
+ readonly id: string
105
+ readonly _partitionKey: string
106
+ readonly type: "clock"
107
+ readonly workflowName: string
108
+ readonly deferredName: string
109
+ readonly fireAt: string
110
+ }
111
+
112
+ const execId = "exec" as const
113
+ const activityKey = (name: string, attempt: number) => `activity::${name}::${attempt}`
114
+ const deferredKey = (name: string) => `deferred::${name}`
115
+ const clockKey = (name: string) => `clock::${name}`
116
+
117
+ const isOptimisticStatus = (code: number) => code === 409 || code === 412 || code === 404
118
+
119
+ // --- Storage codecs ----------------------------------------------------------
120
+ // Values flowing through the engine's activity / deferred boundary are already
121
+ // schema-encoded, so the structure is round-tripped while the payload stays
122
+ // opaque (mirrors the cluster engine's `AnyOrVoid` usage).
123
+ const AnyOrVoid = S.Union([S.Any, S.Void])
124
+ const ActivityResultCodec = S.fromJsonString(S.toCodecJson(Workflow.Result({ success: AnyOrVoid, error: AnyOrVoid })))
125
+ const DeferredExitCodec = S.fromJsonString(S.toCodecJson(S.Exit(AnyOrVoid, AnyOrVoid, S.Defect)))
126
+
127
+ const encodeActivityResult = (r: Workflow.Result<unknown, unknown>) =>
128
+ Effect.orDie(S.encodeEffect(ActivityResultCodec)(r))
129
+ const decodeActivityResult = (s: string) => Effect.orDie(S.decodeEffect(ActivityResultCodec)(s))
130
+ const encodeDeferredExit = (e: Exit.Exit<unknown, unknown>) => Effect.orDie(S.encodeEffect(DeferredExitCodec)(e))
131
+ const decodeDeferredExit = (s: string) => Effect.orDie(S.decodeEffect(DeferredExitCodec)(s))
132
+
133
+ const makeCosmosWorkflowEngine = Effect.fnUntraced(function*(cfg: WorkflowEngineCosmosConfig) {
134
+ const { db } = yield* CosmosClient
135
+ const containerId = `${cfg.prefix ?? ""}workflow-engine`
136
+ yield* Effect.promise(() =>
137
+ db.containers.createIfNotExists({
138
+ id: containerId,
139
+ partitionKey: { paths: ["/_partitionKey"], version: 2 }
140
+ })
141
+ )
142
+ const container = db.container(containerId)
143
+ const scope = yield* Effect.scope
144
+
145
+ const workerId = cfg.workerId ?? randomUUID()
146
+ const leaseTtl = cfg.leaseTtl ?? Duration.seconds(30)
147
+ const heartbeatInterval = cfg.heartbeatInterval ?? Duration.seconds(10)
148
+ const recoveryInterval = cfg.recoveryInterval ?? Duration.seconds(15)
149
+ const clockPollInterval = cfg.clockPollInterval ?? Duration.seconds(5)
150
+
151
+ const annotate = (operation: string, executionId?: string) =>
152
+ annotateDb({
153
+ operation,
154
+ system: "cosmosdb",
155
+ collection: containerId,
156
+ entity: "workflow",
157
+ extra: executionId !== undefined
158
+ ? { "azure.cosmosdb.operation.partition_key": executionId, "app.entity.id": executionId }
159
+ : undefined
160
+ })
161
+
162
+ type Registered = {
163
+ readonly workflow: Workflow.Any
164
+ readonly execute: (
165
+ payload: object,
166
+ executionId: string
167
+ ) => Effect.Effect<unknown, unknown, WorkflowInstance | WorkflowEngine>
168
+ readonly scope: Scope.Scope
169
+ }
170
+ const workflows = new Map<string, Registered>()
171
+
172
+ type LocalExec = {
173
+ instance: WorkflowInstance["Service"]
174
+ fiber: Fiber.Fiber<Workflow.Result<unknown, unknown>> | undefined
175
+ parent: string | undefined
176
+ }
177
+ const locals = new Map<string, LocalExec>()
178
+ const clocks = yield* FiberMap.make<string>()
179
+
180
+ // Per-workflow codecs for the typed payload + top-level result. Cached by
181
+ // workflow name; derived from the workflow's own schemas so typed values
182
+ // (dates, branded ids, schema classes) survive the storage round-trip.
183
+ const makePayloadCodec = (workflow: Workflow.Any) => S.fromJsonString(S.toCodecJson(workflow.payloadSchema))
184
+ const payloadCodecCache = new Map<string, ReturnType<typeof makePayloadCodec>>()
185
+ const payloadCodecFor = (workflow: Workflow.Any) => {
186
+ let c = payloadCodecCache.get(workflow.name)
187
+ if (!c) {
188
+ c = makePayloadCodec(workflow)
189
+ payloadCodecCache.set(workflow.name, c)
190
+ }
191
+ return c
192
+ }
193
+
194
+ const makeResultCodec = (workflow: Workflow.Any) =>
195
+ S.fromJsonString(S.toCodecJson(Workflow.Result({ success: workflow.successSchema, error: workflow.errorSchema })))
196
+ const resultCodecCache = new Map<string, ReturnType<typeof makeResultCodec>>()
197
+ const resultCodecFor = (workflow: Workflow.Any) => {
198
+ let c = resultCodecCache.get(workflow.name)
199
+ if (!c) {
200
+ c = makeResultCodec(workflow)
201
+ resultCodecCache.set(workflow.name, c)
202
+ }
203
+ return c
204
+ }
205
+
206
+ const encodePayload = (workflow: Workflow.Any, payload: object) =>
207
+ Effect.orDie(S.encodeEffect(payloadCodecFor(workflow))(payload)) as Effect.Effect<string>
208
+ const decodePayload = (workflow: Workflow.Any, s: string) =>
209
+ Effect.orDie(S.decodeEffect(payloadCodecFor(workflow))(s)) as Effect.Effect<object>
210
+ const encodeResult = (workflow: Workflow.Any, r: Workflow.Result<unknown, unknown>) =>
211
+ Effect.orDie(S.encodeEffect(resultCodecFor(workflow))(r)) as Effect.Effect<string>
212
+ const decodeResult = (workflow: Workflow.Any, s: string) =>
213
+ Effect.orDie(S.decodeEffect(resultCodecFor(workflow))(s)) as Effect.Effect<Workflow.Result<unknown, unknown>>
214
+
215
+ // --- Cosmos primitives -------------------------------------------------
216
+
217
+ const readExec = (executionId: string) =>
218
+ Effect
219
+ .gen(function*() {
220
+ const resp = yield* Effect.promise(() => container.item(execId, executionId).read<ExecDoc>())
221
+ yield* annotateCosmosResponse({ requestCharge: resp.requestCharge, statusCode: resp.statusCode })
222
+ return Option.fromNullishOr(resp.resource).pipe(
223
+ Option.map((r) => ({ ...r, _etag: resp.etag }))
224
+ )
225
+ })
226
+ .pipe(annotate("readExec", executionId))
227
+
228
+ const replaceExec = (doc: ExecDoc) =>
229
+ Effect
230
+ .gen(function*() {
231
+ const resp = yield* Effect.promise(() =>
232
+ container.item(execId, doc._partitionKey).replace<ExecDoc>(doc, {
233
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
234
+ })
235
+ )
236
+ yield* annotateCosmosResponse({ requestCharge: resp.requestCharge, statusCode: resp.statusCode })
237
+ if (isOptimisticStatus(resp.statusCode)) {
238
+ return yield* new OptimisticConcurrencyException({
239
+ type: "workflow.exec",
240
+ id: doc._partitionKey,
241
+ code: resp.statusCode
242
+ })
243
+ }
244
+ return { ...doc, _etag: resp.etag }
245
+ })
246
+ .pipe(annotate("replaceExec", doc._partitionKey))
247
+
248
+ // Atomic create-or-noop using a single-op batch — returns true if created.
249
+ const createIfMissing = <T extends { readonly id: string; readonly _partitionKey: string }>(
250
+ body: T
251
+ ): Effect.Effect<boolean> =>
252
+ Effect.gen(function*() {
253
+ const resp = yield* Effect.promise(() =>
254
+ container.items.batch(
255
+ [{ operationType: "Create" as const, resourceBody: body }],
256
+ body._partitionKey
257
+ )
258
+ )
259
+ const r = resp.result?.[0]
260
+ const code = r?.statusCode ?? 0
261
+ if (code === 201) return true
262
+ if (code === 409) return false
263
+ return yield* Effect.die(
264
+ new Error(`workflow-engine cosmos createIfMissing for ${body.id} failed: ${code}`)
265
+ )
266
+ })
267
+
268
+ // Last-writer-wins upsert — used to overwrite a persisted *suspended* activity
269
+ // result once it finally completes (create-only ops can't transition it).
270
+ const upsert = <T extends { readonly id: string; readonly _partitionKey: string }>(
271
+ body: T
272
+ ): Effect.Effect<void> =>
273
+ Effect.gen(function*() {
274
+ const resp = yield* Effect.promise(() => container.items.upsert<T>(body))
275
+ yield* annotateCosmosResponse({ requestCharge: resp.requestCharge, statusCode: resp.statusCode })
276
+ })
277
+
278
+ const readPoint = <T extends { id: string }>(id: string, executionId: string) =>
279
+ Effect.promise(() => container.item(id, executionId).read<T>()).pipe(
280
+ Effect.map((r) => Option.fromNullishOr(r.resource))
281
+ )
282
+
283
+ // --- Workflow result helpers ------------------------------------------
284
+
285
+ const completeResult = (
286
+ workflow: Workflow.Any,
287
+ state: ExecDoc
288
+ ): Effect.Effect<Option.Option<Workflow.Result<unknown, unknown>>> =>
289
+ state.status === "complete" && state.completedResult
290
+ ? Effect.map(decodeResult(workflow, state.completedResult), Option.some)
291
+ : Effect.succeedNone
292
+
293
+ // --- Lease / claim ----------------------------------------------------
294
+
295
+ const leaseActive = (state: ExecDoc, now: number): boolean =>
296
+ state.worker !== undefined
297
+ && state.worker !== workerId
298
+ && state.leaseExpiresAt !== undefined
299
+ && Date.parse(state.leaseExpiresAt) > now
300
+
301
+ /**
302
+ * Try to claim a lease on `state`. Returns the updated doc on success, `None`
303
+ * if another worker holds an active lease, or on OCC conflict (caller may
304
+ * retry by re-reading).
305
+ */
306
+ const tryClaim = (state: ExecDoc): Effect.Effect<Option.Option<ExecDoc>> =>
307
+ Effect.gen(function*() {
308
+ const now = Date.now()
309
+ if (leaseActive(state, now)) return Option.none<ExecDoc>()
310
+ const updated: ExecDoc = {
311
+ ...state,
312
+ worker: workerId,
313
+ leaseExpiresAt: new Date(now + Duration.toMillis(leaseTtl)).toISOString()
314
+ }
315
+ return yield* replaceExec(updated).pipe(
316
+ Effect.map(Option.some),
317
+ Effect.catchTag("OptimisticConcurrencyException", () => Effect.succeed(Option.none<ExecDoc>()))
318
+ )
319
+ })
320
+
321
+ /**
322
+ * Renew lease until the local fiber stops or another worker takes the claim.
323
+ * Best-effort: failures are swallowed; loop simply retries on next tick.
324
+ */
325
+ const heartbeat = (executionId: string): Effect.Effect<void> =>
326
+ Effect.gen(function*() {
327
+ while (true) {
328
+ yield* Effect.sleep(heartbeatInterval)
329
+ const local = locals.get(executionId)
330
+ const polled = local?.fiber?.pollUnsafe()
331
+ if (!local?.fiber || polled) return
332
+ const cur = yield* readExec(executionId).pipe(
333
+ Effect.catchCause(() => Effect.succeed(Option.none<ExecDoc>()))
334
+ )
335
+ if (Option.isNone(cur)) continue
336
+ const state = cur.value
337
+ if (state.status === "complete" || state.worker !== workerId) return
338
+ yield* replaceExec({
339
+ ...state,
340
+ leaseExpiresAt: new Date(Date.now() + Duration.toMillis(leaseTtl)).toISOString()
341
+ })
342
+ .pipe(
343
+ Effect.catchTag("OptimisticConcurrencyException", () => Effect.void),
344
+ Effect.catchCause(() => Effect.void)
345
+ )
346
+ }
347
+ })
348
+
349
+ // --- Drive logic -------------------------------------------------------
350
+
351
+ const drive = (
352
+ executionId: string,
353
+ payload: object,
354
+ parent: string | undefined,
355
+ entry: Registered
356
+ ): Effect.Effect<void> =>
357
+ Effect.gen(function*() {
358
+ let local = locals.get(executionId)
359
+ if (local?.fiber) {
360
+ const polled = local.fiber.pollUnsafe()
361
+ const stillRunning = !polled
362
+ const completedNotResume = polled && polled._tag === "Success" && polled.value._tag === "Complete"
363
+ if (stillRunning || completedNotResume) return
364
+ }
365
+
366
+ const stateOpt = yield* readExec(executionId)
367
+ if (Option.isNone(stateOpt) || stateOpt.value.status === "complete") return
368
+
369
+ // Best-effort claim: takes lease so recovery poller leaves us alone.
370
+ // Failure is tolerated — local fiber still drives; OCC guards persisted
371
+ // state so split-brain stays correct.
372
+ const claimed = yield* tryClaim(stateOpt.value)
373
+ const state = Option.isSome(claimed) ? claimed.value : stateOpt.value
374
+
375
+ const instance = WorkflowInstance.initial(entry.workflow, executionId)
376
+ instance.interrupted = state.interrupted
377
+ if (!local) {
378
+ local = { instance, fiber: undefined, parent }
379
+ locals.set(executionId, local)
380
+ } else {
381
+ local.instance = instance
382
+ }
383
+
384
+ const onComplete = Effect.fnUntraced(function*(result: Workflow.Result<unknown, unknown>) {
385
+ const current = yield* readExec(executionId)
386
+ if (Option.isNone(current) || current.value.status === "complete") return
387
+ const isComplete = result._tag === "Complete"
388
+ const completedResult = isComplete ? yield* encodeResult(entry.workflow, result) : undefined
389
+ yield* replaceExec({
390
+ ...current.value,
391
+ status: isComplete ? "complete" : current.value.status,
392
+ suspended: result._tag === "Suspended",
393
+ interrupted: instance.interrupted,
394
+ completedResult,
395
+ // Release lease on completion so the doc isn't seen as orphaned.
396
+ worker: isComplete ? undefined : current.value.worker,
397
+ leaseExpiresAt: isComplete ? undefined : current.value.leaseExpiresAt
398
+ })
399
+ .pipe(Effect.catchTag("OptimisticConcurrencyException", () => Effect.void))
400
+ if (parent && isComplete) {
401
+ yield* Effect.forkIn(driveById(parent), scope)
402
+ }
403
+ })
404
+
405
+ local.fiber = yield* entry.execute(payload, executionId).pipe(
406
+ Effect.onExit(() => {
407
+ if (!instance.interrupted) return Effect.void
408
+ instance.suspended = false
409
+ return Effect.withFiber((fiber) => Effect.interruptible(Fiber.interrupt(fiber)))
410
+ }),
411
+ Workflow.intoResult,
412
+ Effect.provideService(WorkflowInstance, instance),
413
+ Effect.provideService(WorkflowEngine, engine),
414
+ Effect.tap(onComplete),
415
+ Effect.forkIn(entry.scope)
416
+ )
417
+
418
+ if (Option.isSome(claimed)) {
419
+ yield* Effect.forkIn(heartbeat(executionId), scope)
420
+ }
421
+ })
422
+
423
+ const driveById = (executionId: string): Effect.Effect<void> =>
424
+ Effect.gen(function*() {
425
+ const stateOpt = yield* readExec(executionId)
426
+ if (Option.isNone(stateOpt)) return
427
+ const state = stateOpt.value
428
+ const entry = workflows.get(state.workflowName)
429
+ if (!entry) return
430
+ const payload = yield* decodePayload(entry.workflow, state.payload)
431
+ yield* drive(executionId, payload, state.parent, entry)
432
+ })
433
+
434
+ // --- Clock firing -----------------------------------------------------
435
+ // Persist deferred completion (first-writer-wins via createIfMissing),
436
+ // resume the workflow, then clean up the clock doc.
437
+ const fireClock = (doc: ClockDoc): Effect.Effect<void> =>
438
+ Effect.gen(function*() {
439
+ const created = yield* createIfMissing<DeferredDoc>({
440
+ id: deferredKey(doc.deferredName),
441
+ _partitionKey: doc._partitionKey,
442
+ type: "deferred",
443
+ exit: yield* encodeDeferredExit(Exit.void)
444
+ })
445
+ .pipe(annotate("clockFire", doc._partitionKey))
446
+ if (created) yield* driveById(doc._partitionKey)
447
+ yield* Effect.promise(() => container.item(doc.id, doc._partitionKey).delete()).pipe(
448
+ Effect.catchCause(() => Effect.void)
449
+ )
450
+ })
451
+
452
+ // --- Encoded engine ----------------------------------------------------
453
+
454
+ const encoded: Encoded = {
455
+ register: Effect.fnUntraced(function*(workflow, execute) {
456
+ workflows.set(workflow.name, {
457
+ workflow,
458
+ execute,
459
+ scope: yield* Effect.scope
460
+ })
461
+ }),
462
+ execute: Effect.fnUntraced(function*(workflow, options) {
463
+ const entry = workflows.get(workflow.name)
464
+ if (!entry) {
465
+ return yield* Effect.orDie(Effect.fail(`Workflow ${workflow.name} is not registered`))
466
+ }
467
+
468
+ const initial: ExecDoc = {
469
+ id: execId,
470
+ _partitionKey: options.executionId,
471
+ type: "exec",
472
+ workflowName: workflow.name,
473
+ payload: yield* encodePayload(workflow, options.payload),
474
+ parent: options.parent?.executionId,
475
+ status: "running",
476
+ suspended: false,
477
+ interrupted: false
478
+ }
479
+ const created = yield* createIfMissing(initial).pipe(annotate("execute.claim", options.executionId))
480
+
481
+ if (created || !locals.has(options.executionId)) {
482
+ yield* drive(options.executionId, options.payload, options.parent?.executionId, entry)
483
+ }
484
+
485
+ if (options.discard) return undefined as any
486
+
487
+ const local = locals.get(options.executionId)
488
+ if (local?.fiber) {
489
+ return (yield* Fiber.join(local.fiber)) as any
490
+ }
491
+
492
+ // Foreign-owned execution: poll until exec doc reports complete.
493
+ while (true) {
494
+ const cur = yield* readExec(options.executionId)
495
+ if (Option.isSome(cur)) {
496
+ const c = yield* completeResult(workflow, cur.value)
497
+ if (Option.isSome(c)) return c.value as any
498
+ }
499
+ yield* Effect.sleep(Duration.millis(500))
500
+ }
501
+ }),
502
+ poll: (workflow, executionId) =>
503
+ Effect.gen(function*() {
504
+ const local = locals.get(executionId)
505
+ if (local?.fiber) {
506
+ const exit = local.fiber.pollUnsafe()
507
+ if (!exit) return Option.none<Workflow.Result<unknown, unknown>>()
508
+ if (exit._tag !== "Success") return yield* Effect.die(exit.cause)
509
+ return Option.some(exit.value)
510
+ }
511
+ const state = yield* readExec(executionId)
512
+ if (Option.isNone(state)) return Option.none<Workflow.Result<unknown, unknown>>()
513
+ return yield* completeResult(workflow, state.value)
514
+ }),
515
+ interrupt: Effect.fnUntraced(function*(_workflow, executionId) {
516
+ const local = locals.get(executionId)
517
+ if (local) local.instance.interrupted = true
518
+ const current = yield* readExec(executionId)
519
+ if (Option.isNone(current) || current.value.status === "complete") return
520
+ yield* replaceExec({ ...current.value, interrupted: true }).pipe(
521
+ Effect.catchTag("OptimisticConcurrencyException", () => Effect.void)
522
+ )
523
+ yield* driveById(executionId)
524
+ }),
525
+ interruptUnsafe: Effect.fnUntraced(function*(_workflow, executionId) {
526
+ const local = locals.get(executionId)
527
+ if (local) local.instance.interrupted = true
528
+ const current = yield* readExec(executionId)
529
+ if (Option.isSome(current) && current.value.status !== "complete") {
530
+ yield* replaceExec({ ...current.value, interrupted: true }).pipe(
531
+ Effect.catchTag("OptimisticConcurrencyException", () => Effect.void)
532
+ )
533
+ }
534
+ if (local?.fiber) yield* Fiber.interrupt(local.fiber)
535
+ }),
536
+ resume: (_workflow, executionId) => driveById(executionId),
537
+ activityExecute: Effect.fnUntraced(function*(activity, attempt) {
538
+ const instance = yield* WorkflowInstance
539
+ const id = activityKey(activity.name, attempt)
540
+ const existing = yield* readPoint<ActivityDoc>(id, instance.executionId).pipe(
541
+ annotate("activityRead", instance.executionId)
542
+ )
543
+ if (Option.isSome(existing)) {
544
+ const prev = yield* decodeActivityResult(existing.value.result)
545
+ // A completed activity is replayed from its persisted result; a
546
+ // suspended one must re-run (it parked on a clock/deferred).
547
+ if (prev._tag === "Complete") return prev
548
+ }
549
+
550
+ const activityInstance = WorkflowInstance.initial(instance.workflow, instance.executionId)
551
+ activityInstance.interrupted = instance.interrupted
552
+
553
+ const result = yield* activity.executeEncoded.pipe(
554
+ Workflow.intoResult,
555
+ Effect.provideService(WorkflowInstance, activityInstance)
556
+ )
557
+ const doc: ActivityDoc = {
558
+ id,
559
+ _partitionKey: instance.executionId,
560
+ type: "activity",
561
+ result: yield* encodeActivityResult(result)
562
+ }
563
+
564
+ if (Option.isSome(existing)) {
565
+ // Overwrite the previously persisted *suspended* doc with the new result.
566
+ yield* upsert(doc).pipe(annotate("activityPersist", instance.executionId))
567
+ return result
568
+ }
569
+
570
+ // First-writer-wins: if persistence loses the race, use the persisted result.
571
+ const persisted = yield* createIfMissing(doc).pipe(annotate("activityPersist", instance.executionId))
572
+ if (persisted) return result
573
+ const winner = yield* readPoint<ActivityDoc>(id, instance.executionId)
574
+ if (Option.isSome(winner)) {
575
+ const w = yield* decodeActivityResult(winner.value.result)
576
+ if (w._tag === "Complete") return w
577
+ }
578
+ return result
579
+ }),
580
+ deferredResult: Effect.fnUntraced(function*(deferred) {
581
+ const instance = yield* WorkflowInstance
582
+ const got = yield* readPoint<DeferredDoc>(deferredKey(deferred.name), instance.executionId).pipe(
583
+ annotate("deferredRead", instance.executionId)
584
+ )
585
+ if (Option.isNone(got)) return Option.none<Exit.Exit<unknown, unknown>>()
586
+ return Option.some(yield* decodeDeferredExit(got.value.exit))
587
+ }),
588
+ deferredDone: Effect.fnUntraced(function*(options) {
589
+ const created = yield* createIfMissing<DeferredDoc>({
590
+ id: deferredKey(options.deferredName),
591
+ _partitionKey: options.executionId,
592
+ type: "deferred",
593
+ exit: yield* encodeDeferredExit(options.exit)
594
+ })
595
+ .pipe(annotate("deferredPersist", options.executionId))
596
+ if (!created) return
597
+ yield* driveById(options.executionId)
598
+ }),
599
+ scheduleClock: (workflow, options) => {
600
+ const fireAt = new Date(Date.now() + Duration.toMillis(options.clock.duration)).toISOString()
601
+ const clockDoc: ClockDoc = {
602
+ id: clockKey(options.clock.name),
603
+ _partitionKey: options.executionId,
604
+ type: "clock",
605
+ workflowName: workflow.name,
606
+ deferredName: options.clock.deferred.name,
607
+ fireAt
608
+ }
609
+ return Effect.gen(function*() {
610
+ yield* createIfMissing(clockDoc).pipe(annotate("clockPersist", options.executionId))
611
+ // Fast-path in-process timer. If this process dies, the clock poller
612
+ // picks up the persisted doc and fires the deferred.
613
+ yield* fireClock(clockDoc).pipe(
614
+ Effect.delay(options.clock.duration),
615
+ FiberMap.run(clocks, `${options.executionId}/${options.clock.name}`, { onlyIfMissing: true }),
616
+ Effect.asVoid
617
+ )
618
+ })
619
+ }
620
+ }
621
+
622
+ const engine = makeUnsafe(encoded)
623
+
624
+ // --- Recovery poller --------------------------------------------------
625
+ // Scan for executions whose lease has lapsed (or was never set) and
626
+ // re-drive them locally. driveById will go through claim → fork fiber,
627
+ // resuming activities from persisted results.
628
+ if (Duration.toMillis(recoveryInterval) > 0) {
629
+ type StaleRow = { readonly _partitionKey: string; readonly workflowName: string }
630
+ const recoverStep = Effect
631
+ .gen(function*() {
632
+ const nowIso = new Date().toISOString()
633
+ const stale = yield* Effect.promise(() =>
634
+ container
635
+ .items
636
+ .query<StaleRow>({
637
+ query:
638
+ "SELECT c._partitionKey, c.workflowName FROM c WHERE c.type = 'exec' AND c.status = 'running' AND (NOT IS_DEFINED(c.leaseExpiresAt) OR c.leaseExpiresAt <= @now)",
639
+ parameters: [{ name: "@now", value: nowIso }]
640
+ })
641
+ .fetchAll()
642
+ )
643
+ for (const row of stale.resources) {
644
+ if (!workflows.has(row.workflowName)) continue
645
+ const local = locals.get(row._partitionKey)
646
+ if (local?.fiber && !local.fiber.pollUnsafe()) continue
647
+ yield* Effect.forkIn(driveById(row._partitionKey), scope)
648
+ }
649
+ })
650
+ .pipe(annotate("recoveryScan"), Effect.catchCause(() => Effect.void))
651
+
652
+ yield* recoverStep.pipe(
653
+ Effect.repeat(Schedule.spaced(recoveryInterval)),
654
+ Effect.forkIn(scope)
655
+ )
656
+ }
657
+
658
+ // --- Clock poller -----------------------------------------------------
659
+ // Cross-partition scan for clocks whose fireAt is due. Fires the deferred
660
+ // via createIfMissing (idempotent) so multiple pollers across processes
661
+ // converge. Also acts as the restart recovery path for clocks scheduled
662
+ // before a crash.
663
+ if (Duration.toMillis(clockPollInterval) > 0) {
664
+ type DueClock = {
665
+ readonly id: string
666
+ readonly _partitionKey: string
667
+ readonly workflowName: string
668
+ readonly deferredName: string
669
+ }
670
+ const clockStep = Effect
671
+ .gen(function*() {
672
+ const nowIso = new Date().toISOString()
673
+ const due = yield* Effect.promise(() =>
674
+ container
675
+ .items
676
+ .query<DueClock>({
677
+ query:
678
+ "SELECT c.id, c._partitionKey, c.workflowName, c.deferredName FROM c WHERE c.type = 'clock' AND c.fireAt <= @now",
679
+ parameters: [{ name: "@now", value: nowIso }]
680
+ })
681
+ .fetchAll()
682
+ )
683
+ for (const row of due.resources) {
684
+ yield* Effect.forkIn(
685
+ fireClock({
686
+ id: row.id,
687
+ _partitionKey: row._partitionKey,
688
+ type: "clock",
689
+ workflowName: row.workflowName,
690
+ deferredName: row.deferredName,
691
+ fireAt: nowIso
692
+ }),
693
+ scope
694
+ )
695
+ }
696
+ })
697
+ .pipe(annotate("clockScan"), Effect.catchCause(() => Effect.void))
698
+
699
+ yield* clockStep.pipe(
700
+ Effect.repeat(Schedule.spaced(clockPollInterval)),
701
+ Effect.forkIn(scope)
702
+ )
703
+ }
704
+
705
+ return engine
706
+ })
707
+
708
+ /**
709
+ * Cosmos DB backed `WorkflowEngine` layer.
710
+ *
711
+ * Per-execution writes share a partition key (TransactionalBatch-eligible) and
712
+ * use OCC via `_etag`/IfMatch, giving first-writer-wins semantics for activity
713
+ * results, durable-deferred completions, and exec-state transitions. All
714
+ * persisted payloads/results/exits are round-tripped through schema codecs.
715
+ */
716
+ export const layerCosmos = (cfg: WorkflowEngineCosmosConfig): Layer.Layer<WorkflowEngine> =>
717
+ Layer
718
+ .effect(WorkflowEngine)(makeCosmosWorkflowEngine(cfg))
719
+ .pipe(Layer.provide(CosmosClientLayer(Redacted.value(cfg.url), cfg.dbName)))