@effect-app/infra 4.0.0-beta.31 → 4.0.0-beta.310

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