@effect-app/infra 4.0.0-beta.98 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (486) hide show
  1. package/CHANGELOG.md +2891 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +28 -9
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +35 -14
  6. package/dist/ClusterCosmos.d.ts +67 -0
  7. package/dist/ClusterCosmos.d.ts.map +1 -0
  8. package/dist/ClusterCosmos.js +588 -0
  9. package/dist/ClusterServiceBus.d.ts +67 -0
  10. package/dist/ClusterServiceBus.d.ts.map +1 -0
  11. package/dist/ClusterServiceBus.js +84 -0
  12. package/dist/{api/ContextProvider.d.ts → ContextProvider.d.ts} +7 -4
  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 +11 -7
  22. package/dist/MainFiberSet.d.ts.map +1 -1
  23. package/dist/MainFiberSet.js +22 -6
  24. package/dist/QueueMaker/errors.d.ts +3 -1
  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 +34 -45
  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 +16 -4
  33. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  34. package/dist/Store/Cosmos/query.js +238 -42
  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 +394 -319
  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 +91 -39
  41. package/dist/Store/Memory.d.ts +7 -10
  42. package/dist/Store/Memory.d.ts.map +1 -1
  43. package/dist/Store/Memory.js +348 -78
  44. package/dist/Store/SQL/Pg.d.ts +2 -2
  45. package/dist/Store/SQL/Pg.d.ts.map +1 -1
  46. package/dist/Store/SQL/Pg.js +229 -154
  47. package/dist/Store/SQL/query.d.ts +19 -3
  48. package/dist/Store/SQL/query.d.ts.map +1 -1
  49. package/dist/Store/SQL/query.js +306 -12
  50. package/dist/Store/SQL.d.ts +22 -5
  51. package/dist/Store/SQL.d.ts.map +1 -1
  52. package/dist/Store/SQL.js +369 -98
  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 +71 -22
  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 +9 -6
  59. package/dist/Store/jsonDocument.d.ts +67 -0
  60. package/dist/Store/jsonDocument.d.ts.map +1 -0
  61. package/dist/Store/jsonDocument.js +107 -0
  62. package/dist/Store/utils.d.ts +21 -2
  63. package/dist/Store/utils.d.ts.map +1 -1
  64. package/dist/Store/utils.js +253 -10
  65. package/dist/WorkflowEngineCosmos.d.ts +30 -0
  66. package/dist/WorkflowEngineCosmos.d.ts.map +1 -0
  67. package/dist/WorkflowEngineCosmos.js +542 -0
  68. package/dist/WorkflowEngineSqlite.d.ts +24 -0
  69. package/dist/WorkflowEngineSqlite.d.ts.map +1 -0
  70. package/dist/WorkflowEngineSqlite.js +550 -0
  71. package/dist/arbs.d.ts +6 -3
  72. package/dist/arbs.d.ts.map +1 -1
  73. package/dist/arbs.js +20 -4
  74. package/dist/codec.d.ts.map +1 -0
  75. package/dist/codec.js +5 -0
  76. package/dist/cosmos-client.d.ts +40 -0
  77. package/dist/cosmos-client.d.ts.map +1 -0
  78. package/dist/cosmos-client.js +41 -0
  79. package/dist/errorReporter.d.ts +6 -4
  80. package/dist/errorReporter.d.ts.map +1 -1
  81. package/dist/errorReporter.js +23 -27
  82. package/dist/fileUtil.d.ts +1 -1
  83. package/dist/fileUtil.d.ts.map +1 -1
  84. package/dist/fileUtil.js +2 -2
  85. package/dist/index.d.ts +2 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +3 -2
  88. package/dist/internal/RequestContextMiddleware.d.ts +5 -0
  89. package/dist/internal/RequestContextMiddleware.d.ts.map +1 -0
  90. package/dist/internal/RequestContextMiddleware.js +45 -0
  91. package/dist/internal/auth.d.ts +53 -0
  92. package/dist/internal/auth.d.ts.map +1 -0
  93. package/dist/internal/auth.js +180 -0
  94. package/dist/internal/events.d.ts +11 -0
  95. package/dist/internal/events.d.ts.map +1 -0
  96. package/dist/internal/events.js +49 -0
  97. package/dist/{api/internal → internal}/health.d.ts +1 -1
  98. package/dist/internal/health.d.ts.map +1 -0
  99. package/dist/internal/health.js +5 -0
  100. package/dist/{api/layerUtils.d.ts → layerUtils.d.ts} +10 -2
  101. package/dist/layerUtils.d.ts.map +1 -0
  102. package/dist/layerUtils.js +17 -0
  103. package/dist/logger/jsonLogger.d.ts +1 -1
  104. package/dist/logger/jsonLogger.d.ts.map +1 -1
  105. package/dist/logger/jsonLogger.js +5 -3
  106. package/dist/logger/logFmtLogger.d.ts +1 -1
  107. package/dist/logger/logFmtLogger.d.ts.map +1 -1
  108. package/dist/logger/logFmtLogger.js +3 -3
  109. package/dist/logger/shared.d.ts +2 -2
  110. package/dist/logger/shared.d.ts.map +1 -1
  111. package/dist/logger/shared.js +6 -6
  112. package/dist/logger.d.ts.map +1 -1
  113. package/dist/middlewares.d.ts.map +1 -0
  114. package/dist/{api/middlewares.js → middlewares.js} +1 -1
  115. package/dist/{adapters/mongo-client.d.ts → mongo-client.d.ts} +2 -1
  116. package/dist/mongo-client.d.ts.map +1 -0
  117. package/dist/mongo-client.js +15 -0
  118. package/dist/otel.d.ts +75 -0
  119. package/dist/otel.d.ts.map +1 -0
  120. package/dist/otel.js +84 -0
  121. package/dist/rateLimit.d.ts +11 -3
  122. package/dist/rateLimit.d.ts.map +1 -1
  123. package/dist/rateLimit.js +7 -12
  124. package/dist/{adapters/redis-client.d.ts → redis-client.d.ts} +4 -1
  125. package/dist/redis-client.d.ts.map +1 -0
  126. package/dist/redis-client.js +98 -0
  127. package/dist/{api/reportError.d.ts → reportError.d.ts} +1 -1
  128. package/dist/reportError.d.ts.map +1 -0
  129. package/dist/reportError.js +28 -0
  130. package/dist/{api/routing → routing}/middleware/RouterMiddleware.d.ts +2 -1
  131. package/dist/routing/middleware/RouterMiddleware.d.ts.map +1 -0
  132. package/dist/{api/routing → routing}/middleware/RouterMiddleware.js +1 -1
  133. package/dist/{api/routing → routing}/middleware/middleware.d.ts +11 -6
  134. package/dist/routing/middleware/middleware.d.ts.map +1 -0
  135. package/dist/routing/middleware/middleware.js +152 -0
  136. package/dist/{api/routing → routing}/middleware.d.ts +0 -1
  137. package/dist/routing/middleware.d.ts.map +1 -0
  138. package/dist/{api/routing → routing}/middleware.js +1 -2
  139. package/dist/routing/schema/jwt.d.ts.map +1 -0
  140. package/dist/routing/schema/jwt.js +12 -0
  141. package/dist/routing/tsort.d.ts.map +1 -0
  142. package/dist/routing/tsort.js +51 -0
  143. package/dist/{api/routing → routing}/utils.d.ts +3 -3
  144. package/dist/routing/utils.d.ts.map +1 -0
  145. package/dist/routing/utils.js +45 -0
  146. package/dist/routing.d.ts +182 -0
  147. package/dist/routing.d.ts.map +1 -0
  148. package/dist/routing.js +256 -0
  149. package/dist/test.d.ts +2 -2
  150. package/dist/test.d.ts.map +1 -1
  151. package/dist/test.js +5 -5
  152. package/dist/util.d.ts.map +1 -0
  153. package/dist/util.js +14 -0
  154. package/docs/cluster-storage.md +26 -0
  155. package/docs/workflow-engine.md +262 -0
  156. package/examples/query.ts +23 -19
  157. package/package.json +36 -364
  158. package/src/CUPS.ts +53 -14
  159. package/src/ClusterCosmos.ts +1170 -0
  160. package/src/ClusterServiceBus.ts +244 -0
  161. package/src/{api/ContextProvider.ts → ContextProvider.ts} +10 -7
  162. package/src/Emailer/Sendgrid.ts +83 -60
  163. package/src/Emailer/fake.ts +4 -4
  164. package/src/MainFiberSet.ts +31 -12
  165. package/src/QueueMaker/errors.ts +3 -1
  166. package/src/{adapters/SQL → SQL}/Model.ts +48 -47
  167. package/src/SQL.ts +1 -0
  168. package/src/Store/Cosmos/query.ts +275 -55
  169. package/src/Store/Cosmos.ts +597 -515
  170. package/src/Store/Disk.ts +145 -71
  171. package/src/Store/Memory.ts +420 -115
  172. package/src/Store/SQL/Pg.ts +352 -268
  173. package/src/Store/SQL/query.ts +366 -20
  174. package/src/Store/SQL.ts +742 -293
  175. package/src/Store/codeFilter.ts +78 -28
  176. package/src/Store/index.ts +18 -13
  177. package/src/Store/jsonDocument.ts +143 -0
  178. package/src/Store/utils.ts +307 -30
  179. package/src/WorkflowEngineCosmos.ts +749 -0
  180. package/src/WorkflowEngineSqlite.ts +813 -0
  181. package/src/arbs.ts +25 -5
  182. package/src/cosmos-client.ts +72 -0
  183. package/src/errorReporter.ts +68 -78
  184. package/src/fileUtil.ts +1 -1
  185. package/src/index.ts +2 -1
  186. package/src/{api/internal → internal}/RequestContextMiddleware.ts +23 -6
  187. package/src/internal/auth.ts +272 -0
  188. package/src/{api/internal → internal}/events.ts +18 -9
  189. package/src/{api/layerUtils.ts → layerUtils.ts} +7 -3
  190. package/src/logger/jsonLogger.ts +5 -3
  191. package/src/logger/logFmtLogger.ts +3 -3
  192. package/src/logger/shared.ts +6 -5
  193. package/src/middlewares.ts +10 -0
  194. package/src/{adapters/mongo-client.ts → mongo-client.ts} +3 -1
  195. package/src/otel.ts +177 -0
  196. package/src/rateLimit.ts +35 -24
  197. package/src/{adapters/redis-client.ts → redis-client.ts} +6 -2
  198. package/src/{api/reportError.ts → reportError.ts} +3 -2
  199. package/src/{api/routing → routing}/middleware/RouterMiddleware.ts +2 -1
  200. package/src/{api/routing → routing}/middleware/middleware.ts +75 -50
  201. package/src/routing/middleware.ts +4 -0
  202. package/src/{api/routing → routing}/schema/jwt.ts +2 -2
  203. package/src/{api/routing → routing}/utils.ts +2 -1
  204. package/src/routing.ts +823 -0
  205. package/src/test.ts +6 -6
  206. package/src/vitest.ts +1 -1
  207. package/test/arbs-magnitude.test.ts +33 -0
  208. package/test/arbs.test.ts +41 -0
  209. package/test/auth.test.ts +101 -0
  210. package/test/cluster-cosmos.test.ts +634 -0
  211. package/test/cluster-servicebus.test.ts +180 -0
  212. package/test/cluster-sqlite.test.ts +207 -0
  213. package/test/contextProvider.test.ts +11 -8
  214. package/test/controller.test.ts +23 -28
  215. package/test/cosmos-container-throughput.test.ts +80 -0
  216. package/test/cosmos-query.test.ts +368 -0
  217. package/test/dist/arbs-magnitude.test.d.ts.map +1 -0
  218. package/test/dist/arbs.test.d.ts.map +1 -0
  219. package/test/dist/auth.test.d.ts.map +1 -0
  220. package/test/dist/cluster-cosmos.test.d.ts.map +1 -0
  221. package/test/dist/cluster-servicebus.test.d.ts.map +1 -0
  222. package/test/dist/cluster-sqlite.test.d.ts.map +1 -0
  223. package/test/dist/contextProvider.test.d.ts.map +1 -1
  224. package/test/dist/controller.test.d.ts.map +1 -1
  225. package/test/dist/cosmos-container-throughput.test.d.ts.map +1 -0
  226. package/test/dist/cosmos-query.test.d.ts.map +1 -0
  227. package/test/dist/fixtures.d.ts +17 -9
  228. package/test/dist/fixtures.d.ts.map +1 -1
  229. package/test/dist/fixtures.js +8 -3
  230. package/test/dist/json-lower.test.d.ts.map +1 -0
  231. package/test/dist/query.test.d.ts.map +1 -1
  232. package/test/dist/rawQuery.test.d.ts.map +1 -1
  233. package/test/dist/repository-ext.test.d.ts.map +1 -0
  234. package/test/dist/repository-legacy-document.test.d.ts.map +1 -0
  235. package/test/dist/requires.test.d.ts.map +1 -1
  236. package/test/dist/router-generator.test.d.ts.map +1 -0
  237. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  238. package/test/dist/rpc-context-map-streaming.test.d.ts.map +1 -0
  239. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  240. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  241. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  242. package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
  243. package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
  244. package/test/fixtures.ts +7 -2
  245. package/test/json-lower.test.ts +129 -0
  246. package/test/layerUtils.test.ts +2 -2
  247. package/test/query.test.ts +1232 -42
  248. package/test/rawQuery.test.ts +432 -7
  249. package/test/repository-ext.test.ts +456 -0
  250. package/test/repository-legacy-document.test.ts +394 -0
  251. package/test/requires.test.ts +7 -3
  252. package/test/router-generator.test.ts +187 -0
  253. package/test/routing-interruptibility.test.ts +66 -0
  254. package/test/rpc-context-map-streaming.test.ts +265 -0
  255. package/test/rpc-e2e-invalidation.test.ts +421 -0
  256. package/test/rpc-multi-middleware.test.ts +17 -11
  257. package/test/rpc-stream-fullstack.test.ts +307 -0
  258. package/test/sql-store.test.ts +1202 -47
  259. package/test/validateSample.test.ts +70 -42
  260. package/test/workflow-engine-cosmos.test.ts +345 -0
  261. package/test/workflow-engine-sqlite.test.ts +297 -0
  262. package/tsconfig.examples.json +1 -1
  263. package/tsconfig.json +9 -1
  264. package/tsconfig.src.json +35 -35
  265. package/tsconfig.test.json +2 -2
  266. package/dist/Emailer/service.d.ts +0 -55
  267. package/dist/Emailer/service.d.ts.map +0 -1
  268. package/dist/Emailer/service.js +0 -6
  269. package/dist/Emailer.d.ts +0 -2
  270. package/dist/Emailer.d.ts.map +0 -1
  271. package/dist/Emailer.js +0 -2
  272. package/dist/Model/Repository/ext.d.ts +0 -41
  273. package/dist/Model/Repository/ext.d.ts.map +0 -1
  274. package/dist/Model/Repository/ext.js +0 -65
  275. package/dist/Model/Repository/internal/internal.d.ts +0 -59
  276. package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
  277. package/dist/Model/Repository/internal/internal.js +0 -320
  278. package/dist/Model/Repository/legacy.d.ts +0 -19
  279. package/dist/Model/Repository/legacy.d.ts.map +0 -1
  280. package/dist/Model/Repository/legacy.js +0 -2
  281. package/dist/Model/Repository/makeRepo.d.ts +0 -49
  282. package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
  283. package/dist/Model/Repository/makeRepo.js +0 -24
  284. package/dist/Model/Repository/service.d.ts +0 -89
  285. package/dist/Model/Repository/service.d.ts.map +0 -1
  286. package/dist/Model/Repository/service.js +0 -2
  287. package/dist/Model/Repository/validation.d.ts +0 -43
  288. package/dist/Model/Repository/validation.d.ts.map +0 -1
  289. package/dist/Model/Repository/validation.js +0 -32
  290. package/dist/Model/Repository.d.ts +0 -6
  291. package/dist/Model/Repository.d.ts.map +0 -1
  292. package/dist/Model/Repository.js +0 -6
  293. package/dist/Model/dsl.d.ts +0 -32
  294. package/dist/Model/dsl.d.ts.map +0 -1
  295. package/dist/Model/dsl.js +0 -44
  296. package/dist/Model/filter/filterApi.d.ts +0 -30
  297. package/dist/Model/filter/filterApi.d.ts.map +0 -1
  298. package/dist/Model/filter/filterApi.js +0 -2
  299. package/dist/Model/filter/types/errors.d.ts +0 -29
  300. package/dist/Model/filter/types/errors.d.ts.map +0 -1
  301. package/dist/Model/filter/types/errors.js +0 -2
  302. package/dist/Model/filter/types/fields.d.ts +0 -15
  303. package/dist/Model/filter/types/fields.d.ts.map +0 -1
  304. package/dist/Model/filter/types/fields.js +0 -2
  305. package/dist/Model/filter/types/path/common.d.ts +0 -316
  306. package/dist/Model/filter/types/path/common.d.ts.map +0 -1
  307. package/dist/Model/filter/types/path/common.js +0 -2
  308. package/dist/Model/filter/types/path/eager.d.ts +0 -95
  309. package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
  310. package/dist/Model/filter/types/path/eager.js +0 -31
  311. package/dist/Model/filter/types/path/index.d.ts +0 -4
  312. package/dist/Model/filter/types/path/index.d.ts.map +0 -1
  313. package/dist/Model/filter/types/path/index.js +0 -3
  314. package/dist/Model/filter/types/utils.d.ts +0 -79
  315. package/dist/Model/filter/types/utils.d.ts.map +0 -1
  316. package/dist/Model/filter/types/utils.js +0 -2
  317. package/dist/Model/filter/types/validator.d.ts +0 -30
  318. package/dist/Model/filter/types/validator.d.ts.map +0 -1
  319. package/dist/Model/filter/types/validator.js +0 -2
  320. package/dist/Model/filter/types.d.ts +0 -5
  321. package/dist/Model/filter/types.d.ts.map +0 -1
  322. package/dist/Model/filter/types.js +0 -7
  323. package/dist/Model/query/dsl.d.ts +0 -248
  324. package/dist/Model/query/dsl.d.ts.map +0 -1
  325. package/dist/Model/query/dsl.js +0 -104
  326. package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
  327. package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
  328. package/dist/Model/query/new-kid-interpreter.js +0 -165
  329. package/dist/Model/query.d.ts +0 -15
  330. package/dist/Model/query.d.ts.map +0 -1
  331. package/dist/Model/query.js +0 -3
  332. package/dist/Model.d.ts +0 -4
  333. package/dist/Model.d.ts.map +0 -1
  334. package/dist/Model.js +0 -4
  335. package/dist/Operations.d.ts +0 -55
  336. package/dist/Operations.d.ts.map +0 -1
  337. package/dist/Operations.js +0 -102
  338. package/dist/OperationsRepo.d.ts +0 -41
  339. package/dist/OperationsRepo.d.ts.map +0 -1
  340. package/dist/OperationsRepo.js +0 -14
  341. package/dist/QueueMaker/SQLQueue.d.ts +0 -14
  342. package/dist/QueueMaker/SQLQueue.d.ts.map +0 -1
  343. package/dist/QueueMaker/SQLQueue.js +0 -132
  344. package/dist/QueueMaker/memQueue.d.ts +0 -11
  345. package/dist/QueueMaker/memQueue.d.ts.map +0 -1
  346. package/dist/QueueMaker/memQueue.js +0 -76
  347. package/dist/QueueMaker/sbqueue.d.ts +0 -12
  348. package/dist/QueueMaker/sbqueue.d.ts.map +0 -1
  349. package/dist/QueueMaker/sbqueue.js +0 -91
  350. package/dist/QueueMaker/service.d.ts +0 -11
  351. package/dist/QueueMaker/service.d.ts.map +0 -1
  352. package/dist/QueueMaker/service.js +0 -4
  353. package/dist/RequestContext.d.ts +0 -68
  354. package/dist/RequestContext.d.ts.map +0 -1
  355. package/dist/RequestContext.js +0 -49
  356. package/dist/RequestFiberSet.d.ts +0 -36
  357. package/dist/RequestFiberSet.d.ts.map +0 -1
  358. package/dist/RequestFiberSet.js +0 -78
  359. package/dist/Store/ContextMapContainer.d.ts +0 -25
  360. package/dist/Store/ContextMapContainer.d.ts.map +0 -1
  361. package/dist/Store/ContextMapContainer.js +0 -77
  362. package/dist/Store/service.d.ts +0 -111
  363. package/dist/Store/service.d.ts.map +0 -1
  364. package/dist/Store/service.js +0 -79
  365. package/dist/Store.d.ts +0 -2
  366. package/dist/Store.d.ts.map +0 -1
  367. package/dist/Store.js +0 -2
  368. package/dist/adapters/SQL/Model.d.ts.map +0 -1
  369. package/dist/adapters/SQL/Model.js +0 -478
  370. package/dist/adapters/SQL.d.ts.map +0 -1
  371. package/dist/adapters/ServiceBus.d.ts +0 -58
  372. package/dist/adapters/ServiceBus.d.ts.map +0 -1
  373. package/dist/adapters/ServiceBus.js +0 -99
  374. package/dist/adapters/cosmos-client.d.ts +0 -14
  375. package/dist/adapters/cosmos-client.d.ts.map +0 -1
  376. package/dist/adapters/cosmos-client.js +0 -9
  377. package/dist/adapters/index.d.ts +0 -2
  378. package/dist/adapters/index.d.ts.map +0 -1
  379. package/dist/adapters/index.js +0 -2
  380. package/dist/adapters/logger.d.ts +0 -9
  381. package/dist/adapters/logger.d.ts.map +0 -1
  382. package/dist/adapters/logger.js +0 -3
  383. package/dist/adapters/memQueue.d.ts +0 -13
  384. package/dist/adapters/memQueue.d.ts.map +0 -1
  385. package/dist/adapters/memQueue.js +0 -20
  386. package/dist/adapters/mongo-client.d.ts.map +0 -1
  387. package/dist/adapters/mongo-client.js +0 -13
  388. package/dist/adapters/redis-client.d.ts.map +0 -1
  389. package/dist/adapters/redis-client.js +0 -94
  390. package/dist/api/ContextProvider.d.ts.map +0 -1
  391. package/dist/api/ContextProvider.js +0 -38
  392. package/dist/api/codec.d.ts.map +0 -1
  393. package/dist/api/codec.js +0 -5
  394. package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
  395. package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
  396. package/dist/api/internal/RequestContextMiddleware.js +0 -35
  397. package/dist/api/internal/auth.d.ts +0 -15
  398. package/dist/api/internal/auth.d.ts.map +0 -1
  399. package/dist/api/internal/auth.js +0 -47
  400. package/dist/api/internal/events.d.ts +0 -9
  401. package/dist/api/internal/events.d.ts.map +0 -1
  402. package/dist/api/internal/events.js +0 -42
  403. package/dist/api/internal/health.d.ts.map +0 -1
  404. package/dist/api/internal/health.js +0 -5
  405. package/dist/api/layerUtils.d.ts.map +0 -1
  406. package/dist/api/layerUtils.js +0 -16
  407. package/dist/api/middlewares.d.ts.map +0 -1
  408. package/dist/api/reportError.d.ts.map +0 -1
  409. package/dist/api/reportError.js +0 -27
  410. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
  411. package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
  412. package/dist/api/routing/middleware/middleware.js +0 -130
  413. package/dist/api/routing/middleware.d.ts.map +0 -1
  414. package/dist/api/routing/schema/jwt.d.ts.map +0 -1
  415. package/dist/api/routing/schema/jwt.js +0 -12
  416. package/dist/api/routing/tsort.d.ts.map +0 -1
  417. package/dist/api/routing/tsort.js +0 -51
  418. package/dist/api/routing/utils.d.ts.map +0 -1
  419. package/dist/api/routing/utils.js +0 -44
  420. package/dist/api/routing.d.ts +0 -134
  421. package/dist/api/routing.d.ts.map +0 -1
  422. package/dist/api/routing.js +0 -167
  423. package/dist/api/setupRequest.d.ts +0 -15
  424. package/dist/api/setupRequest.d.ts.map +0 -1
  425. package/dist/api/setupRequest.js +0 -49
  426. package/dist/api/util.d.ts.map +0 -1
  427. package/dist/api/util.js +0 -14
  428. package/eslint.config.mjs +0 -24
  429. package/src/Emailer/service.ts +0 -52
  430. package/src/Emailer.ts +0 -1
  431. package/src/Model/Repository/ext.ts +0 -283
  432. package/src/Model/Repository/internal/internal.ts +0 -580
  433. package/src/Model/Repository/legacy.ts +0 -27
  434. package/src/Model/Repository/makeRepo.ts +0 -139
  435. package/src/Model/Repository/service.ts +0 -627
  436. package/src/Model/Repository/validation.ts +0 -31
  437. package/src/Model/Repository.ts +0 -5
  438. package/src/Model/dsl.ts +0 -128
  439. package/src/Model/filter/filterApi.ts +0 -60
  440. package/src/Model/filter/types/errors.ts +0 -47
  441. package/src/Model/filter/types/fields.ts +0 -50
  442. package/src/Model/filter/types/path/common.ts +0 -404
  443. package/src/Model/filter/types/path/eager.ts +0 -298
  444. package/src/Model/filter/types/path/index.ts +0 -4
  445. package/src/Model/filter/types/utils.ts +0 -128
  446. package/src/Model/filter/types/validator.ts +0 -46
  447. package/src/Model/filter/types.ts +0 -6
  448. package/src/Model/query/dsl.ts +0 -2110
  449. package/src/Model/query/new-kid-interpreter.ts +0 -210
  450. package/src/Model/query.ts +0 -13
  451. package/src/Model.ts +0 -3
  452. package/src/Operations.ts +0 -235
  453. package/src/OperationsRepo.ts +0 -16
  454. package/src/QueueMaker/SQLQueue.ts +0 -190
  455. package/src/QueueMaker/memQueue.ts +0 -130
  456. package/src/QueueMaker/sbqueue.ts +0 -137
  457. package/src/QueueMaker/service.ts +0 -17
  458. package/src/RequestContext.ts +0 -63
  459. package/src/RequestFiberSet.ts +0 -105
  460. package/src/Store/ContextMapContainer.ts +0 -116
  461. package/src/Store/index.test.ts.bak +0 -112
  462. package/src/Store/service.ts +0 -193
  463. package/src/Store.ts +0 -1
  464. package/src/adapters/SQL.ts +0 -1
  465. package/src/adapters/ServiceBus.ts +0 -209
  466. package/src/adapters/cosmos-client.ts +0 -16
  467. package/src/adapters/index.ts +0 -0
  468. package/src/adapters/logger.ts +0 -3
  469. package/src/adapters/memQueue.ts +0 -21
  470. package/src/api/internal/auth.ts +0 -68
  471. package/src/api/middlewares.ts +0 -10
  472. package/src/api/routing/middleware.ts +0 -6
  473. package/src/api/routing.ts +0 -598
  474. package/src/api/setupRequest.ts +0 -104
  475. package/test/dist/date-query.test.d.ts.map +0 -1
  476. package/tsconfig.json.bak +0 -12
  477. /package/dist/{adapters/SQL.d.ts → SQL.d.ts} +0 -0
  478. /package/dist/{api/codec.d.ts → codec.d.ts} +0 -0
  479. /package/dist/{api/middlewares.d.ts → middlewares.d.ts} +0 -0
  480. /package/dist/{api/routing → routing}/schema/jwt.d.ts +0 -0
  481. /package/dist/{api/routing → routing}/tsort.d.ts +0 -0
  482. /package/dist/{api/util.d.ts → util.d.ts} +0 -0
  483. /package/src/{api/codec.ts → codec.ts} +0 -0
  484. /package/src/{api/internal → internal}/health.ts +0 -0
  485. /package/src/{api/routing → routing}/tsort.ts +0 -0
  486. /package/src/{api/util.ts → util.ts} +0 -0
@@ -0,0 +1,1170 @@
1
+ import type { OperationInput, PatchRequestBody } from "@azure/cosmos"
2
+ import * as Arr from "effect-app/Array"
3
+ import * as Effect from "effect-app/Effect"
4
+ import * as Layer from "effect-app/Layer"
5
+ import * as Option from "effect-app/Option"
6
+ import * as Cause from "effect/Cause"
7
+ import { PersistenceError } from "effect/cluster/ClusterError"
8
+ import type * as Envelope from "effect/cluster/Envelope"
9
+ import * as MessageStorage from "effect/cluster/MessageStorage"
10
+ import { SaveResultEncoded } from "effect/cluster/MessageStorage"
11
+ import type * as Reply from "effect/cluster/Reply"
12
+ import * as RunnerStorage from "effect/cluster/RunnerStorage"
13
+ import * as ShardId from "effect/cluster/ShardId"
14
+ import * as ShardingConfig from "effect/cluster/ShardingConfig"
15
+ import * as Snowflake from "effect/cluster/Snowflake"
16
+ import * as Duration from "effect/Duration"
17
+ import * as Redacted from "effect/Redacted"
18
+ import { CosmosClient, CosmosClientLayer, type CosmosContainerThroughput, createContainerIfNotExists } from "./cosmos-client.ts"
19
+ import { annotateCosmosResponse, annotateDb } from "./otel.ts"
20
+
21
+ export interface ClusterCosmosConfig extends CosmosContainerThroughput {
22
+ readonly url: Redacted.Redacted<string>
23
+ readonly dbName: string
24
+ readonly prefix?: string
25
+ }
26
+
27
+ type MessageKind = "Request" | "AckChunk" | "Interrupt"
28
+ type CosmosQueryValue = string | number | boolean | null | Array<string | number | boolean | null>
29
+ type CosmosParameter = { readonly name: string; readonly value: CosmosQueryValue }
30
+
31
+ interface MessageDoc {
32
+ readonly id: string
33
+ readonly _partitionKey: string
34
+ readonly type: "message"
35
+ readonly rowid: string
36
+ readonly messageId: string | null
37
+ readonly shardId: string
38
+ readonly entityType: string
39
+ readonly entityId: string
40
+ readonly kind: MessageKind
41
+ readonly tag: string | null
42
+ readonly payload: unknown
43
+ readonly headers: Record<string, string> | null
44
+ readonly traceId?: string | undefined
45
+ readonly spanId?: string | undefined
46
+ readonly sampled?: boolean | undefined
47
+ processed: boolean
48
+ readonly requestId: string
49
+ readonly replyId: string | null
50
+ lastReplyId: string | null
51
+ lastRead: number | null
52
+ readonly deliverAt: number | null
53
+ readonly _etag?: string
54
+ }
55
+
56
+ type ReplyDoc = WithExitReplyDoc | ChunkReplyDoc
57
+
58
+ interface ReplyDocBase {
59
+ readonly id: string
60
+ readonly _partitionKey: string
61
+ readonly type: "reply"
62
+ readonly rowid: string
63
+ readonly requestId: string
64
+ acked: boolean
65
+ }
66
+
67
+ interface WithExitReplyDoc extends ReplyDocBase {
68
+ readonly kind: "WithExit"
69
+ readonly payload: Reply.WithExitEncoded["exit"]
70
+ readonly sequence: null
71
+ }
72
+
73
+ interface ChunkReplyDoc extends ReplyDocBase {
74
+ readonly kind: "Chunk"
75
+ readonly payload: Reply.ChunkEncoded["values"]
76
+ readonly sequence: number
77
+ }
78
+
79
+ interface RunnerDoc {
80
+ readonly id: string
81
+ readonly _partitionKey: "runner"
82
+ readonly type: "runner"
83
+ readonly address: string
84
+ runner: string
85
+ healthy: boolean
86
+ lastHeartbeat: number
87
+ /** Unique, stable machine id (mirrors SQL's auto-increment `machine_id`). */
88
+ readonly machineId: number
89
+ readonly _etag?: string
90
+ }
91
+
92
+ interface MachineIdCounterDoc {
93
+ readonly id: string
94
+ readonly _partitionKey: "runner"
95
+ readonly type: "machine-id-counter"
96
+ value: number
97
+ readonly _etag?: string
98
+ }
99
+
100
+ interface LockDoc {
101
+ readonly id: string
102
+ readonly _partitionKey: "lock"
103
+ readonly type: "lock"
104
+ readonly shardId: string
105
+ address: string
106
+ acquiredAt: number
107
+ readonly _etag?: string
108
+ }
109
+
110
+ const withTracerDisabled = Effect.withTracerEnabled(false)
111
+ const refailPersistence = <A, E, R>(effect: Effect.Effect<A, E, R>) => PersistenceError.refail(effect)
112
+ const cosmosId = (id: string) => encodeURIComponent(id)
113
+ const messagePartition = (shardId: string) => `message::${shardId}`
114
+ const messageDocId = (envelope: Envelope.Encoded, primaryKey: string | null) =>
115
+ cosmosId(primaryKey === null ? envelopeId(envelope) : `primary::${primaryKey}`)
116
+ const replyPartition = (requestId: string) => `reply::${requestId}`
117
+ const runnerDocId = (address: string) => cosmosId(`runner::${address}`)
118
+ const lockDocId = (shardId: string) => cosmosId(`lock::${shardId}`)
119
+ const tenMinutes = Duration.toMillis(Duration.minutes(10))
120
+ const maxCosmosBatchOperations = 100
121
+ const isSuccessfulStatus = (statusCode: number | undefined): boolean =>
122
+ statusCode !== undefined && statusCode >= 200 && statusCode < 300
123
+
124
+ const isCosmosStatus = (u: unknown, code: number): boolean =>
125
+ Cause.isUnknownError(u)
126
+ ? isCosmosStatus(u.cause, code)
127
+ : typeof u === "object" && u !== null && "code" in u && u.code === code
128
+
129
+ const isConflict = (u: unknown) => isCosmosStatus(u, 409)
130
+ const isNotFound = (u: unknown) => isCosmosStatus(u, 404)
131
+ const isPreconditionFailed = (u: unknown) => isCosmosStatus(u, 412)
132
+
133
+ const respBytes = (
134
+ resp: { diagnostics?: { clientSideRequestStatistics?: { totalResponsePayloadLengthInBytes?: number } } }
135
+ ) => resp.diagnostics?.clientSideRequestStatistics?.totalResponsePayloadLengthInBytes ?? 0
136
+
137
+ const annotateItem = (resp: {
138
+ readonly requestCharge?: number
139
+ readonly statusCode?: number
140
+ readonly diagnostics?: {
141
+ readonly clientSideRequestStatistics?: { readonly totalResponsePayloadLengthInBytes?: number }
142
+ }
143
+ }) =>
144
+ annotateCosmosResponse({
145
+ requestCharge: resp.requestCharge,
146
+ statusCode: resp.statusCode,
147
+ responseBytes: respBytes(resp)
148
+ })
149
+
150
+ const annotateFeed = (resp: {
151
+ readonly resources: readonly unknown[]
152
+ readonly requestCharge?: number
153
+ readonly diagnostics?: {
154
+ readonly clientSideRequestStatistics?: { readonly totalResponsePayloadLengthInBytes?: number }
155
+ }
156
+ }) =>
157
+ annotateCosmosResponse({
158
+ requestCharge: resp.requestCharge,
159
+ returnedRows: resp.resources.length,
160
+ responseBytes: respBytes(resp)
161
+ })
162
+
163
+ const envelopeId = (envelope: Envelope.Encoded) => envelope._tag === "Request" ? envelope.requestId : envelope.id
164
+
165
+ const envelopeToDoc = (
166
+ envelope: Envelope.Encoded,
167
+ primaryKey: string | null,
168
+ deliverAt: number | null
169
+ ): MessageDoc => {
170
+ switch (envelope._tag) {
171
+ case "Request":
172
+ return {
173
+ id: messageDocId(envelope, primaryKey),
174
+ _partitionKey: messagePartition(ShardId.toString(envelope.address.shardId)),
175
+ type: "message",
176
+ rowid: envelope.requestId,
177
+ messageId: primaryKey,
178
+ shardId: ShardId.toString(envelope.address.shardId),
179
+ entityType: envelope.address.entityType,
180
+ entityId: envelope.address.entityId,
181
+ kind: "Request",
182
+ tag: envelope.tag,
183
+ payload: envelope.payload,
184
+ headers: envelope.headers,
185
+ traceId: envelope.traceId,
186
+ spanId: envelope.spanId,
187
+ sampled: envelope.sampled,
188
+ processed: false,
189
+ requestId: envelope.requestId,
190
+ replyId: null,
191
+ lastReplyId: null,
192
+ lastRead: null,
193
+ deliverAt
194
+ }
195
+ case "AckChunk":
196
+ return {
197
+ id: cosmosId(envelope.id),
198
+ _partitionKey: messagePartition(ShardId.toString(envelope.address.shardId)),
199
+ type: "message",
200
+ rowid: envelope.id,
201
+ messageId: primaryKey,
202
+ shardId: ShardId.toString(envelope.address.shardId),
203
+ entityType: envelope.address.entityType,
204
+ entityId: envelope.address.entityId,
205
+ kind: "AckChunk",
206
+ tag: null,
207
+ payload: null,
208
+ headers: null,
209
+ processed: false,
210
+ requestId: envelope.requestId,
211
+ replyId: envelope.replyId,
212
+ lastReplyId: null,
213
+ lastRead: null,
214
+ deliverAt
215
+ }
216
+ case "Interrupt":
217
+ return {
218
+ id: cosmosId(envelope.id),
219
+ _partitionKey: messagePartition(ShardId.toString(envelope.address.shardId)),
220
+ type: "message",
221
+ rowid: envelope.id,
222
+ messageId: primaryKey,
223
+ shardId: ShardId.toString(envelope.address.shardId),
224
+ entityType: envelope.address.entityType,
225
+ entityId: envelope.address.entityId,
226
+ kind: "Interrupt",
227
+ tag: null,
228
+ payload: null,
229
+ headers: null,
230
+ processed: false,
231
+ requestId: envelope.requestId,
232
+ replyId: null,
233
+ lastReplyId: null,
234
+ lastRead: null,
235
+ deliverAt
236
+ }
237
+ }
238
+ }
239
+
240
+ const envelopeFromDoc = (
241
+ doc: MessageDoc,
242
+ lastSentReply: Option.Option<Reply.Encoded>
243
+ ): {
244
+ readonly envelope: Envelope.Encoded
245
+ readonly lastSentReply: Option.Option<Reply.Encoded>
246
+ } => {
247
+ switch (doc.kind) {
248
+ case "Request": {
249
+ const envelope: Envelope.PartialRequestEncoded = {
250
+ _tag: "Request",
251
+ requestId: doc.requestId,
252
+ address: {
253
+ shardId: shardIdFromString(doc.shardId),
254
+ entityType: doc.entityType,
255
+ entityId: doc.entityId
256
+ },
257
+ tag: doc.tag ?? "",
258
+ payload: doc.payload,
259
+ headers: doc.headers ?? {},
260
+ ...(doc.traceId !== undefined && { traceId: doc.traceId }),
261
+ ...(doc.spanId !== undefined && { spanId: doc.spanId }),
262
+ ...(doc.sampled !== undefined && { sampled: doc.sampled })
263
+ }
264
+ return {
265
+ envelope,
266
+ lastSentReply
267
+ }
268
+ }
269
+ case "AckChunk":
270
+ return {
271
+ envelope: {
272
+ _tag: "AckChunk",
273
+ id: doc.rowid,
274
+ requestId: doc.requestId,
275
+ replyId: doc.replyId ?? "",
276
+ address: {
277
+ shardId: shardIdFromString(doc.shardId),
278
+ entityType: doc.entityType,
279
+ entityId: doc.entityId
280
+ }
281
+ },
282
+ lastSentReply: Option.none()
283
+ }
284
+ case "Interrupt":
285
+ return {
286
+ envelope: {
287
+ _tag: "Interrupt",
288
+ id: doc.rowid,
289
+ requestId: doc.requestId,
290
+ address: {
291
+ shardId: shardIdFromString(doc.shardId),
292
+ entityType: doc.entityType,
293
+ entityId: doc.entityId
294
+ }
295
+ },
296
+ lastSentReply: Option.none()
297
+ }
298
+ }
299
+ }
300
+
301
+ const replyToDoc = (reply: Reply.Encoded): ReplyDoc =>
302
+ reply._tag === "WithExit"
303
+ ? {
304
+ id: cosmosId(reply.id),
305
+ _partitionKey: replyPartition(reply.requestId),
306
+ type: "reply",
307
+ rowid: reply.id,
308
+ kind: "WithExit",
309
+ requestId: reply.requestId,
310
+ payload: reply.exit,
311
+ sequence: null,
312
+ acked: false
313
+ }
314
+ : {
315
+ id: cosmosId(reply.id),
316
+ _partitionKey: replyPartition(reply.requestId),
317
+ type: "reply",
318
+ rowid: reply.id,
319
+ kind: "Chunk",
320
+ requestId: reply.requestId,
321
+ payload: reply.values,
322
+ sequence: reply.sequence,
323
+ acked: false
324
+ }
325
+
326
+ const replyFromDoc = (doc: ReplyDoc): Reply.Encoded =>
327
+ doc.kind === "WithExit"
328
+ ? {
329
+ _tag: "WithExit",
330
+ id: doc.rowid,
331
+ requestId: doc.requestId,
332
+ exit: doc.payload
333
+ }
334
+ : {
335
+ _tag: "Chunk",
336
+ id: doc.rowid,
337
+ requestId: doc.requestId,
338
+ values: doc.payload,
339
+ sequence: doc.sequence ?? 0
340
+ }
341
+
342
+ const shardIdFromString = (shardId: string): Envelope.Encoded["address"]["shardId"] =>
343
+ ShardId.fromStringEncoded(shardId)
344
+
345
+ const machineIdCounterDocId = cosmosId("machine-id-counter")
346
+
347
+ type StorageOptions = { readonly prefix?: string | undefined } & CosmosContainerThroughput
348
+
349
+ const createContainer = (prefix: string, throughput?: CosmosContainerThroughput) =>
350
+ Effect.fnUntraced(function*() {
351
+ const { db } = yield* CosmosClient
352
+ const containerId = `${prefix}cluster`
353
+ yield* Effect
354
+ .tryPromise(() =>
355
+ createContainerIfNotExists(db, {
356
+ id: containerId,
357
+ partitionKey: { paths: ["/_partitionKey"], version: 2 }
358
+ }, throughput)
359
+ )
360
+ .pipe(Effect.catchIf(isConflict, () => Effect.void))
361
+ return db.container(containerId)
362
+ })
363
+
364
+ export const makeMessageStorage = Effect.fnUntraced(function*(options?: StorageOptions) {
365
+ const prefix = options?.prefix ?? "cluster-"
366
+ const container = yield* createContainer(prefix, options)().pipe(Effect.orDie)
367
+ const containerId = `${prefix}cluster`
368
+ const annotate = (operation: string) =>
369
+ annotateDb({ operation, system: "cosmosdb", collection: containerId, entity: "cluster-message-storage" })
370
+
371
+ const readMessage = (id: string, partitionKey: string) =>
372
+ Effect.tryPromise(() => container.item(id, partitionKey).read<MessageDoc>()).pipe(
373
+ Effect.tap(annotateItem),
374
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
375
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
376
+ )
377
+
378
+ const queryMessages = (query: string, parameters: ReadonlyArray<CosmosParameter>) =>
379
+ Effect
380
+ .tryPromise(() => container.items.query<MessageDoc>({ query, parameters: Array.from(parameters) }).fetchAll())
381
+ .pipe(
382
+ Effect.tap(annotateFeed),
383
+ Effect.map((resp) => resp.resources)
384
+ )
385
+
386
+ const queryReplies = (query: string, parameters: ReadonlyArray<CosmosParameter>) =>
387
+ Effect
388
+ .tryPromise(() => container.items.query<ReplyDoc>({ query, parameters: Array.from(parameters) }).fetchAll())
389
+ .pipe(
390
+ Effect.tap(annotateFeed),
391
+ Effect.map((resp) => resp.resources)
392
+ )
393
+
394
+ const lastReply = (replyId: string | null) =>
395
+ replyId === null
396
+ ? Effect.succeed(Option.none<Reply.Encoded>())
397
+ : queryReplies("SELECT * FROM c WHERE c.type = 'reply' AND c.rowid = @id", [{ name: "@id", value: replyId }])
398
+ .pipe(
399
+ Effect.map((docs) => Option.map(Option.fromNullishOr(docs[0]), replyFromDoc))
400
+ )
401
+
402
+ const markReplyAcked = (requestId: string, replyId: string) =>
403
+ Effect
404
+ .tryPromise(() =>
405
+ container.item(cosmosId(replyId), replyPartition(requestId)).patch<ReplyDoc>([
406
+ { op: "set", path: "/acked", value: true }
407
+ ])
408
+ )
409
+ .pipe(
410
+ Effect.tap(annotateItem),
411
+ Effect.asVoid,
412
+ Effect.catchIf(isNotFound, () => Effect.void),
413
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
414
+ )
415
+
416
+ const claimMessageRead = (doc: MessageDoc, now: number) =>
417
+ Effect
418
+ .tryPromise(() =>
419
+ container.item(doc.id, doc._partitionKey).patch<MessageDoc>(
420
+ [{ op: "set", path: "/lastRead", value: now }],
421
+ { accessCondition: { type: "IfMatch", condition: doc._etag ?? "" } }
422
+ )
423
+ )
424
+ .pipe(
425
+ Effect.tap(annotateItem),
426
+ Effect.as(true),
427
+ Effect.catchIf(isPreconditionFailed, () => Effect.succeed(false))
428
+ )
429
+
430
+ const batchDocs = <A extends { readonly id: string; readonly _partitionKey: string }>(
431
+ docs: ReadonlyArray<A>,
432
+ operation: (doc: A) => OperationInput,
433
+ fallback: (doc: A) => Effect.Effect<void, unknown, never>
434
+ ): Effect.Effect<void, unknown, never> =>
435
+ Effect.forEach(
436
+ Arr.groupByT(docs, (doc) => doc._partitionKey),
437
+ ([partitionKey, partitionDocs]) =>
438
+ Effect.forEach(
439
+ Arr.chunksOf(partitionDocs, maxCosmosBatchOperations),
440
+ (chunk) => {
441
+ const operations: Array<OperationInput> = chunk.map(operation)
442
+ return Effect
443
+ .tryPromise(() => container.items.batch(operations, partitionKey))
444
+ .pipe(
445
+ Effect.tap(annotateItem),
446
+ Effect.flatMap((resp) => {
447
+ const failed = resp.result?.find((result) => !isSuccessfulStatus(result.statusCode))
448
+ return failed === undefined ? Effect.void : Effect.fail(failed.statusCode)
449
+ }),
450
+ Effect.catchIf(() => true, () => Effect.forEach(chunk, fallback, { discard: true }))
451
+ )
452
+ },
453
+ { discard: true }
454
+ ),
455
+ { discard: true }
456
+ )
457
+
458
+ const patchDoc = <A extends { readonly id: string; readonly _partitionKey: string }>(
459
+ doc: A,
460
+ resourceBody: PatchRequestBody
461
+ ): Effect.Effect<void, unknown, never> =>
462
+ Effect.tryPromise(() => container.item(doc.id, doc._partitionKey).patch(resourceBody)).pipe(
463
+ Effect.tap(annotateItem),
464
+ Effect.asVoid,
465
+ Effect.catchIf(isNotFound, () => Effect.void)
466
+ )
467
+
468
+ const patchDocs = <A extends { readonly id: string; readonly _partitionKey: string }>(
469
+ docs: ReadonlyArray<A>,
470
+ resourceBody: (doc: A) => PatchRequestBody
471
+ ): Effect.Effect<void, unknown, never> =>
472
+ batchDocs(
473
+ docs,
474
+ (doc) => ({
475
+ operationType: "Patch" as const,
476
+ id: doc.id,
477
+ resourceBody: resourceBody(doc)
478
+ }),
479
+ (doc) => patchDoc(doc, resourceBody(doc))
480
+ )
481
+
482
+ const deleteDoc = <A extends { readonly id: string; readonly _partitionKey: string }>(
483
+ doc: A
484
+ ): Effect.Effect<void, unknown, never> =>
485
+ Effect.tryPromise(() => container.item(doc.id, doc._partitionKey).delete()).pipe(
486
+ Effect.tap(annotateItem),
487
+ Effect.asVoid,
488
+ Effect.catchIf(isNotFound, () => Effect.void)
489
+ )
490
+
491
+ const deleteDocs = <A extends { readonly id: string; readonly _partitionKey: string }>(
492
+ docs: ReadonlyArray<A>
493
+ ): Effect.Effect<void, unknown, never> =>
494
+ batchDocs(
495
+ docs,
496
+ (doc) => ({
497
+ operationType: "Delete" as const,
498
+ id: doc.id
499
+ }),
500
+ deleteDoc
501
+ )
502
+
503
+ return yield* MessageStorage.makeEncoded({
504
+ saveEnvelope: ({ deliverAt, envelope, primaryKey }) =>
505
+ Effect
506
+ .gen(function*() {
507
+ const doc = envelopeToDoc(envelope, primaryKey, deliverAt)
508
+ if (envelope._tag === "AckChunk") {
509
+ yield* markReplyAcked(envelope.requestId, envelope.replyId)
510
+ const pendingAcks = yield* queryMessages(
511
+ "SELECT * FROM c WHERE c.type = 'message' AND c.kind = 'AckChunk' AND c.processed = false AND c.requestId = @requestId",
512
+ [{ name: "@requestId", value: envelope.requestId }]
513
+ )
514
+ yield* patchDocs(pendingAcks, () => [{ op: "set", path: "/processed", value: true }])
515
+ }
516
+ return yield* Effect.tryPromise(() => container.items.create(doc)).pipe(
517
+ Effect.tap(annotateItem),
518
+ Effect.as<MessageStorage.SaveResult.Encoded>(SaveResultEncoded.Success()),
519
+ Effect.catchIf(isConflict, () =>
520
+ readMessage(doc.id, doc._partitionKey).pipe(
521
+ Effect.flatMap((found) =>
522
+ Option.match(found, {
523
+ onNone: () => Effect.succeed<MessageStorage.SaveResult.Encoded>(SaveResultEncoded.Success()),
524
+ onSome: (existing) =>
525
+ lastReply(existing.lastReplyId).pipe(
526
+ Effect.map((lastReceivedReply) =>
527
+ SaveResultEncoded.Duplicate({
528
+ originalId: Snowflake.Snowflake(existing.requestId),
529
+ lastReceivedReply
530
+ })
531
+ )
532
+ )
533
+ })
534
+ )
535
+ ))
536
+ )
537
+ })
538
+ .pipe(annotate("saveEnvelope"), refailPersistence, withTracerDisabled),
539
+
540
+ saveReply: (reply) =>
541
+ Effect
542
+ .gen(function*() {
543
+ const doc = replyToDoc(reply)
544
+ yield* Effect.tryPromise(() => container.items.create(doc)).pipe(
545
+ Effect.tap(annotateItem),
546
+ Effect.catchIf(isConflict, () => Effect.void)
547
+ )
548
+ const messages = yield* queryMessages(
549
+ "SELECT * FROM c WHERE c.type = 'message' AND c.requestId = @requestId",
550
+ [{ name: "@requestId", value: reply.requestId }]
551
+ )
552
+ const updatedMessages = reply._tag === "WithExit"
553
+ ? messages
554
+ : messages.filter((message) => message.id === reply.requestId || message.kind === "Request")
555
+ yield* patchDocs(updatedMessages, () =>
556
+ reply._tag === "WithExit"
557
+ ? [
558
+ { op: "set", path: "/processed", value: true },
559
+ { op: "set", path: "/lastReplyId", value: reply.id }
560
+ ]
561
+ : [{ op: "set", path: "/lastReplyId", value: reply.id }])
562
+ })
563
+ .pipe(annotate("saveReply"), refailPersistence, withTracerDisabled),
564
+
565
+ clearReplies: (requestId, options) =>
566
+ Effect
567
+ .gen(function*() {
568
+ const id = String(requestId)
569
+ const messages = yield* queryMessages(
570
+ "SELECT * FROM c WHERE c.type = 'message' AND c.requestId = @requestId",
571
+ [{ name: "@requestId", value: id }]
572
+ )
573
+ const request = messages.find((message) => message.kind === "Request")
574
+ if (!request) return
575
+ if (
576
+ options?.expectedReplyId !== undefined
577
+ && request.lastReplyId !== String(options.expectedReplyId)
578
+ ) return
579
+ const replies = yield* queryReplies(
580
+ "SELECT * FROM c WHERE c.type = 'reply' AND c.requestId = @requestId AND c.kind = 'WithExit'",
581
+ [
582
+ { name: "@requestId", value: id }
583
+ ]
584
+ )
585
+ yield* deleteDocs(replies)
586
+ yield* deleteDocs(messages.filter((message) => message.kind === "Interrupt"))
587
+ yield* patchDocs(
588
+ messages.filter((message) => message.kind !== "Interrupt"),
589
+ () => [
590
+ { op: "set", path: "/processed", value: false },
591
+ { op: "set", path: "/lastReplyId", value: null },
592
+ { op: "set", path: "/lastRead", value: null }
593
+ ]
594
+ )
595
+ })
596
+ .pipe(annotate("clearReplies"), refailPersistence, withTracerDisabled),
597
+
598
+ requestIdForPrimaryKey: (primaryKey) =>
599
+ queryMessages("SELECT * FROM c WHERE c.type = 'message' AND c.messageId = @primaryKey", [
600
+ { name: "@primaryKey", value: primaryKey }
601
+ ])
602
+ .pipe(
603
+ Effect.map((docs) => Option.map(Option.fromNullishOr(docs[0]?.requestId), Snowflake.Snowflake)),
604
+ annotate("requestIdForPrimaryKey"),
605
+ refailPersistence,
606
+ withTracerDisabled
607
+ ),
608
+
609
+ repliesFor: (requestIds) =>
610
+ queryReplies(
611
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId) AND (c.kind = 'WithExit' OR (c.kind = 'Chunk' AND c.acked = false)) ORDER BY c.rowid",
612
+ [{ name: "@requestIds", value: Array.from(requestIds) }]
613
+ )
614
+ .pipe(
615
+ Effect.map(Arr.map(replyFromDoc)),
616
+ annotate("repliesFor"),
617
+ refailPersistence,
618
+ withTracerDisabled
619
+ ),
620
+
621
+ repliesForUnfiltered: (requestIds) =>
622
+ queryReplies(
623
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId) ORDER BY c.rowid",
624
+ [{ name: "@requestIds", value: Array.from(requestIds) }]
625
+ )
626
+ .pipe(
627
+ Effect.map(Arr.map(replyFromDoc)),
628
+ annotate("repliesForUnfiltered"),
629
+ refailPersistence,
630
+ withTracerDisabled
631
+ ),
632
+
633
+ unprocessedMessages: (shardIds, now, options) => {
634
+ if (options?.addresses !== undefined && options.addresses.length === 0) {
635
+ return Effect.succeed([])
636
+ }
637
+ const parameters: Array<CosmosParameter> = [
638
+ { name: "@shardIds", value: Array.from(shardIds) },
639
+ { name: "@lastReadBefore", value: now - tenMinutes },
640
+ { name: "@now", value: now }
641
+ ]
642
+ let query =
643
+ "SELECT * FROM c WHERE c.type = 'message' AND ARRAY_CONTAINS(@shardIds, c.shardId) AND c.processed = false AND (NOT IS_DEFINED(c.lastRead) OR IS_NULL(c.lastRead) OR c.lastRead < @lastReadBefore) AND (NOT IS_DEFINED(c.deliverAt) OR IS_NULL(c.deliverAt) OR c.deliverAt <= @now)"
644
+ if (options?.addresses !== undefined) {
645
+ const match = addressClauses(options.addresses, "a")
646
+ query += ` AND (${match.sql})`
647
+ parameters.push(...match.parameters)
648
+ }
649
+ query += " ORDER BY c.rowid"
650
+ if (options?.limit !== undefined) {
651
+ query += ` OFFSET 0 LIMIT ${Math.floor(options.limit)}`
652
+ }
653
+ return queryMessages(query, parameters)
654
+ .pipe(
655
+ Effect.flatMap((docs) => collectUnprocessed(docs, now, claimMessageRead, queryReplies)),
656
+ annotate("unprocessedMessages"),
657
+ refailPersistence,
658
+ withTracerDisabled
659
+ )
660
+ },
661
+
662
+ unprocessedMessagesById: (messageIds, now) =>
663
+ queryMessages(
664
+ "SELECT * FROM c WHERE c.type = 'message' AND (ARRAY_CONTAINS(@messageIds, c.id) OR ARRAY_CONTAINS(@messageIds, c.requestId)) AND c.processed = false AND (NOT IS_DEFINED(c.deliverAt) OR IS_NULL(c.deliverAt) OR c.deliverAt <= @now) ORDER BY c.rowid",
665
+ [
666
+ { name: "@messageIds", value: Array.from(messageIds, String) },
667
+ { name: "@now", value: now }
668
+ ]
669
+ )
670
+ .pipe(
671
+ Effect.flatMap((docs) => collectUnprocessedById(docs, queryReplies)),
672
+ annotate("unprocessedMessagesById"),
673
+ refailPersistence,
674
+ withTracerDisabled
675
+ ),
676
+
677
+ resetRequests: (requestIds) => {
678
+ if (requestIds.length === 0) return Effect.void
679
+ return queryMessages(
680
+ "SELECT * FROM c WHERE c.type = 'message' AND c.processed = false AND (ARRAY_CONTAINS(@requestIds, c.id) OR ARRAY_CONTAINS(@requestIds, c.requestId))",
681
+ [{ name: "@requestIds", value: Array.from(requestIds, String) }]
682
+ )
683
+ .pipe(
684
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
685
+ annotate("resetRequests"),
686
+ refailPersistence,
687
+ withTracerDisabled
688
+ )
689
+ },
690
+
691
+ resetAddresses: (addresses) => {
692
+ if (addresses.length === 0) return Effect.void
693
+ const match = addressClauses(addresses, "r")
694
+ return queryMessages(
695
+ `SELECT * FROM c WHERE c.type = 'message' AND c.processed = false AND (${match.sql})`,
696
+ match.parameters
697
+ )
698
+ .pipe(
699
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
700
+ annotate("resetAddresses"),
701
+ refailPersistence,
702
+ withTracerDisabled
703
+ )
704
+ },
705
+
706
+ clearAddress: (address) =>
707
+ queryMessages(
708
+ "SELECT * FROM c WHERE c.type = 'message' AND c.entityType = @entityType AND c.entityId = @entityId",
709
+ [
710
+ { name: "@entityType", value: address.entityType },
711
+ { name: "@entityId", value: address.entityId }
712
+ ]
713
+ )
714
+ .pipe(
715
+ Effect.flatMap((messages) => {
716
+ if (!Arr.isArrayNonEmpty(messages)) return Effect.void
717
+ const requestIds = Array.from(new Set(messages.map((message) => message.requestId)))
718
+ return queryReplies(
719
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId)",
720
+ [{ name: "@requestIds", value: requestIds }]
721
+ )
722
+ .pipe(
723
+ Effect.flatMap((replies) => deleteDocs(replies)),
724
+ Effect.andThen(deleteDocs(messages))
725
+ )
726
+ }),
727
+ annotate("clearAddress"),
728
+ refailPersistence,
729
+ withTracerDisabled
730
+ ),
731
+
732
+ resetShards: (shardIds) =>
733
+ queryMessages(
734
+ "SELECT * FROM c WHERE c.type = 'message' AND c.processed = false AND ARRAY_CONTAINS(@shardIds, c.shardId)",
735
+ [{ name: "@shardIds", value: Array.from(shardIds) }]
736
+ )
737
+ .pipe(
738
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
739
+ annotate("resetShards"),
740
+ refailPersistence,
741
+ withTracerDisabled
742
+ ),
743
+
744
+ withTransaction: (effect) => effect
745
+ })
746
+ })
747
+
748
+ const addressClauses = (
749
+ addresses: ReadonlyArray<{
750
+ readonly shardId: Parameters<typeof ShardId.toString>[0]
751
+ readonly entityType: string
752
+ readonly entityId: string
753
+ }>,
754
+ prefix: string
755
+ ): { readonly sql: string; readonly parameters: Array<CosmosParameter> } => {
756
+ const parameters: Array<CosmosParameter> = []
757
+ const clauses: Array<string> = []
758
+ for (const [i, address] of addresses.entries()) {
759
+ const shard = `@${prefix}s${i}`
760
+ const entityType = `@${prefix}t${i}`
761
+ const entityId = `@${prefix}e${i}`
762
+ clauses.push(`(c.shardId = ${shard} AND c.entityType = ${entityType} AND c.entityId = ${entityId})`)
763
+ parameters.push(
764
+ { name: shard, value: ShardId.toString(address.shardId) },
765
+ { name: entityType, value: address.entityType },
766
+ { name: entityId, value: address.entityId }
767
+ )
768
+ }
769
+ return { sql: clauses.join(" OR "), parameters }
770
+ }
771
+
772
+ const collectUnprocessed = <E>(
773
+ docs: ReadonlyArray<MessageDoc>,
774
+ now: number,
775
+ claimMessageRead: (doc: MessageDoc, now: number) => Effect.Effect<boolean, E>,
776
+ queryReplies: (
777
+ query: string,
778
+ parameters: ReadonlyArray<CosmosParameter>
779
+ ) => Effect.Effect<Array<ReplyDoc>, E>
780
+ ) =>
781
+ Effect.gen(function*() {
782
+ const messages: Array<{
783
+ readonly envelope: Envelope.Encoded
784
+ readonly lastSentReply: Option.Option<Reply.Encoded>
785
+ }> = []
786
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
787
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
788
+ for (const doc of docs) {
789
+ if (activeRequestIds.has(doc.requestId)) continue
790
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
791
+ const claimed = yield* claimMessageRead(doc, now)
792
+ if (claimed) {
793
+ messages.push(envelopeFromDoc({ ...doc, lastRead: now }, sentReply))
794
+ }
795
+ }
796
+ return messages
797
+ })
798
+
799
+ const collectUnprocessedById = <E>(
800
+ docs: ReadonlyArray<MessageDoc>,
801
+ queryReplies: (
802
+ query: string,
803
+ parameters: ReadonlyArray<CosmosParameter>
804
+ ) => Effect.Effect<Array<ReplyDoc>, E>
805
+ ) =>
806
+ Effect.gen(function*() {
807
+ const messages: Array<{
808
+ readonly envelope: Envelope.Encoded
809
+ readonly lastSentReply: Option.Option<Reply.Encoded>
810
+ }> = []
811
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
812
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
813
+ for (const doc of docs) {
814
+ if (activeRequestIds.has(doc.requestId)) continue
815
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
816
+ messages.push(envelopeFromDoc(doc, sentReply))
817
+ }
818
+ return messages
819
+ })
820
+
821
+ const activeReplyRequestIds = <E>(
822
+ docs: ReadonlyArray<MessageDoc>,
823
+ queryReplies: (
824
+ query: string,
825
+ parameters: ReadonlyArray<CosmosParameter>
826
+ ) => Effect.Effect<Array<ReplyDoc>, E>
827
+ ) => {
828
+ const requestIds = Array.from(new Set(docs.map((doc) => doc.requestId)))
829
+ if (!Arr.isArrayNonEmpty(requestIds)) return Effect.succeed(new Set<string>())
830
+ return queryReplies(
831
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId) AND (c.kind = 'WithExit' OR (c.kind = 'Chunk' AND c.acked = false))",
832
+ [{ name: "@requestIds", value: requestIds }]
833
+ )
834
+ .pipe(Effect.map((replies) => new Set(replies.map((reply) => reply.requestId))))
835
+ }
836
+
837
+ const lastRepliesById = <E>(
838
+ docs: ReadonlyArray<MessageDoc>,
839
+ activeRequestIds: ReadonlySet<string>,
840
+ queryReplies: (
841
+ query: string,
842
+ parameters: ReadonlyArray<CosmosParameter>
843
+ ) => Effect.Effect<Array<ReplyDoc>, E>
844
+ ) => {
845
+ const replyIds = Array.from(
846
+ new Set(docs
847
+ .flatMap((doc) => activeRequestIds.has(doc.requestId) || doc.lastReplyId === null ? [] : [doc.lastReplyId]))
848
+ )
849
+ if (!Arr.isArrayNonEmpty(replyIds)) return Effect.succeed(new Map<string, Reply.Encoded>())
850
+ return queryReplies(
851
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@replyIds, c.rowid)",
852
+ [{ name: "@replyIds", value: replyIds }]
853
+ )
854
+ .pipe(Effect.map((replies) => new Map(replies.map((reply) => [reply.rowid, replyFromDoc(reply)]))))
855
+ }
856
+
857
+ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: StorageOptions) {
858
+ const prefix = options?.prefix ?? "cluster-"
859
+ const container = yield* createContainer(prefix, options)().pipe(Effect.orDie)
860
+ const config = yield* ShardingConfig.ShardingConfig
861
+ const expires = Duration.toMillis(Duration.fromInputUnsafe(config.shardLockExpiration))
862
+ const containerId = `${prefix}cluster`
863
+ const annotate = (operation: string) =>
864
+ annotateDb({ operation, system: "cosmosdb", collection: containerId, entity: "cluster-runner-storage" })
865
+
866
+ const queryRunners = (query: string, parameters: ReadonlyArray<CosmosParameter>) =>
867
+ Effect
868
+ .tryPromise(() =>
869
+ container
870
+ .items
871
+ .query<RunnerDoc>({ query, parameters: Array.from(parameters) }, { partitionKey: "runner" })
872
+ .fetchAll()
873
+ )
874
+ .pipe(Effect.tap(annotateFeed), Effect.map((resp) => resp.resources))
875
+
876
+ const deleteRunner = (doc: RunnerDoc) =>
877
+ Effect
878
+ .tryPromise(() =>
879
+ container.item(doc.id, "runner").delete({
880
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
881
+ })
882
+ )
883
+ .pipe(
884
+ Effect.tap(annotateItem),
885
+ Effect.asVoid,
886
+ Effect.catchIf(isNotFound, () => Effect.void),
887
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
888
+ )
889
+
890
+ const readRunner = (address: string) =>
891
+ Effect.tryPromise(() => container.item(runnerDocId(address), "runner").read<RunnerDoc>()).pipe(
892
+ Effect.tap(annotateItem),
893
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
894
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
895
+ )
896
+
897
+ // Allocate a unique, monotonically increasing machine id via an atomic
898
+ // server-side `incr` on a single counter doc — mirrors SQL's auto-increment
899
+ // `machine_id` PK. A per-address string hash (the previous approach) could
900
+ // collide mod 1024 across distinct runners, yielding duplicate Snowflake ids.
901
+ // The counter doc is seeded at init below, so it always exists here.
902
+ const allocateMachineId = Effect
903
+ .tryPromise(() =>
904
+ container.item(machineIdCounterDocId, "runner").patch<MachineIdCounterDoc>([
905
+ { op: "incr", path: "/value", value: 1 }
906
+ ])
907
+ )
908
+ .pipe(Effect.tap(annotateItem), Effect.map((resp) => resp.resource!.value))
909
+
910
+ // Seed the counter once (create-if-missing); a lost create race is fine.
911
+ yield* Effect
912
+ .tryPromise(() =>
913
+ container.items.create<MachineIdCounterDoc>({
914
+ id: machineIdCounterDocId,
915
+ _partitionKey: "runner",
916
+ type: "machine-id-counter",
917
+ value: 0
918
+ })
919
+ )
920
+ .pipe(Effect.catchIf(isConflict, () => Effect.void), Effect.orDie)
921
+
922
+ const readLock = (shardId: string) =>
923
+ Effect.tryPromise(() => container.item(lockDocId(shardId), "lock").read<LockDoc>()).pipe(
924
+ Effect.tap(annotateItem),
925
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
926
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
927
+ )
928
+
929
+ const writeLock = (doc: LockDoc) =>
930
+ Effect
931
+ .tryPromise(() =>
932
+ container.item(doc.id, "lock").replace(doc, {
933
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
934
+ })
935
+ )
936
+ .pipe(
937
+ Effect.tap(annotateItem),
938
+ Effect.as(true),
939
+ Effect.catchIf(isPreconditionFailed, () => Effect.succeed(false))
940
+ )
941
+
942
+ const deleteLock = (doc: LockDoc) =>
943
+ Effect
944
+ .tryPromise(() =>
945
+ container.item(doc.id, "lock").delete({
946
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
947
+ })
948
+ )
949
+ .pipe(
950
+ Effect.tap(annotateItem),
951
+ Effect.asVoid,
952
+ Effect.catchIf(isNotFound, () => Effect.void),
953
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
954
+ )
955
+
956
+ const createLock = (address: string, shardId: string, now: number) =>
957
+ Effect
958
+ .tryPromise(() =>
959
+ container.items.create<LockDoc>({
960
+ id: lockDocId(shardId),
961
+ _partitionKey: "lock",
962
+ type: "lock",
963
+ shardId,
964
+ address,
965
+ acquiredAt: now
966
+ })
967
+ )
968
+ .pipe(
969
+ Effect.tap(annotateItem),
970
+ Effect.as(true),
971
+ Effect.catchIf(isConflict, () => Effect.succeed(false))
972
+ )
973
+
974
+ const tryAcquire = (address: string, shardId: string, now: number) =>
975
+ readLock(shardId).pipe(
976
+ Effect.flatMap((lock) =>
977
+ Option.match(lock, {
978
+ onNone: () => createLock(address, shardId, now),
979
+ onSome: (doc) => {
980
+ if (doc.address !== address && now - doc.acquiredAt <= expires) {
981
+ return Effect.succeed(false)
982
+ }
983
+ doc.address = address
984
+ doc.acquiredAt = now
985
+ return writeLock(doc)
986
+ }
987
+ })
988
+ ),
989
+ Effect.map((acquired) => acquired ? Option.some(shardId) : Option.none())
990
+ )
991
+
992
+ return RunnerStorage.makeEncoded({
993
+ getRunners: Effect.sync(() => Date.now()).pipe(
994
+ Effect.flatMap((now) =>
995
+ queryRunners("SELECT * FROM c WHERE c.type = 'runner' AND c.lastHeartbeat > @expiresAt", [
996
+ { name: "@expiresAt", value: now - expires }
997
+ ])
998
+ ),
999
+ Effect.map((docs) => docs.map((doc) => [doc.runner, doc.healthy] as const)),
1000
+ annotate("getRunners"),
1001
+ refailPersistence,
1002
+ withTracerDisabled
1003
+ ),
1004
+
1005
+ register: (address, runner, healthy) =>
1006
+ Effect
1007
+ .gen(function*() {
1008
+ const now = Date.now()
1009
+ // Reuse a previously-allocated machine id for this address (stable per
1010
+ // address, like SQL's RETURNING on conflict); allocate a fresh unique
1011
+ // one on first registration.
1012
+ const existing = yield* readRunner(address)
1013
+ const machineId = Option.isSome(existing) && existing.value.machineId !== undefined
1014
+ ? existing.value.machineId
1015
+ : yield* allocateMachineId
1016
+ yield* Effect
1017
+ .tryPromise(() =>
1018
+ container.items.upsert<RunnerDoc>({
1019
+ id: runnerDocId(address),
1020
+ _partitionKey: "runner",
1021
+ type: "runner",
1022
+ address,
1023
+ runner,
1024
+ healthy,
1025
+ lastHeartbeat: now,
1026
+ machineId
1027
+ })
1028
+ )
1029
+ .pipe(Effect.tap(annotateItem))
1030
+ return machineId
1031
+ })
1032
+ .pipe(annotate("register"), refailPersistence, withTracerDisabled),
1033
+
1034
+ unregister: (address) =>
1035
+ Effect.sync(() => Date.now()).pipe(
1036
+ Effect.flatMap((now) =>
1037
+ queryRunners(
1038
+ "SELECT * FROM c WHERE c.type = 'runner' AND (c.address = @address OR c.lastHeartbeat < @expiresAt)",
1039
+ [
1040
+ { name: "@address", value: address },
1041
+ { name: "@expiresAt", value: now - expires }
1042
+ ]
1043
+ )
1044
+ ),
1045
+ Effect.flatMap((docs) => Effect.forEach(docs, deleteRunner, { discard: true })),
1046
+ annotate("unregister"),
1047
+ refailPersistence,
1048
+ withTracerDisabled
1049
+ ),
1050
+
1051
+ setRunnerHealth: (address, healthy) =>
1052
+ Effect
1053
+ .tryPromise(() =>
1054
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1055
+ { op: "set", path: "/healthy", value: healthy }
1056
+ ])
1057
+ )
1058
+ .pipe(
1059
+ Effect.tap(annotateItem),
1060
+ Effect.asVoid,
1061
+ Effect.catchIf(isNotFound, () => Effect.void),
1062
+ annotate("setRunnerHealth"),
1063
+ refailPersistence,
1064
+ withTracerDisabled
1065
+ ),
1066
+
1067
+ acquire: (address, shardIds) =>
1068
+ Effect.sync(() => Date.now()).pipe(
1069
+ Effect.flatMap((now) => Effect.forEach(shardIds, (shardId) => tryAcquire(address, shardId, now))),
1070
+ Effect.map(Arr.getSomes),
1071
+ annotate("acquire"),
1072
+ refailPersistence,
1073
+ withTracerDisabled
1074
+ ),
1075
+
1076
+ refresh: (address, shardIds) =>
1077
+ Effect
1078
+ .gen(function*() {
1079
+ const now = Date.now()
1080
+ yield* Effect
1081
+ .tryPromise(() =>
1082
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1083
+ { op: "set", path: "/lastHeartbeat", value: now }
1084
+ ])
1085
+ )
1086
+ .pipe(
1087
+ Effect.tap(annotateItem),
1088
+ Effect.asVoid,
1089
+ Effect.catchIf(isNotFound, () => Effect.void)
1090
+ )
1091
+ const refreshed = yield* Effect.forEach(shardIds, (shardId) =>
1092
+ readLock(shardId).pipe(
1093
+ Effect.flatMap((lock) =>
1094
+ Option.match(lock, {
1095
+ onNone: () => Effect.succeed(Option.none<string>()),
1096
+ onSome: (doc) => {
1097
+ if (doc.address !== address) return Effect.succeed(Option.none<string>())
1098
+ doc.acquiredAt = now
1099
+ return writeLock(doc).pipe(Effect.map((ok) => ok ? Option.some(shardId) : Option.none<string>()))
1100
+ }
1101
+ })
1102
+ )
1103
+ ))
1104
+ return Arr.getSomes(refreshed)
1105
+ })
1106
+ .pipe(annotate("refresh"), refailPersistence, withTracerDisabled),
1107
+
1108
+ release: (address, shardId) =>
1109
+ readLock(shardId).pipe(
1110
+ Effect.flatMap((lock) =>
1111
+ Option.match(lock, {
1112
+ onNone: () => Effect.void,
1113
+ onSome: (doc) =>
1114
+ doc.address === address
1115
+ ? deleteLock(doc)
1116
+ : Effect.void
1117
+ })
1118
+ ),
1119
+ annotate("release"),
1120
+ refailPersistence,
1121
+ withTracerDisabled
1122
+ ),
1123
+
1124
+ releaseAll: (address) =>
1125
+ Effect
1126
+ .tryPromise(() =>
1127
+ container
1128
+ .items
1129
+ .query<LockDoc>({
1130
+ query: "SELECT * FROM c WHERE c.type = 'lock' AND c.address = @address",
1131
+ parameters: [{ name: "@address", value: address }]
1132
+ }, { partitionKey: "lock" })
1133
+ .fetchAll()
1134
+ )
1135
+ .pipe(
1136
+ Effect.tap(annotateFeed),
1137
+ Effect.flatMap((resp) => Effect.forEach(resp.resources, deleteLock, { discard: true })),
1138
+ annotate("releaseAll"),
1139
+ refailPersistence,
1140
+ withTracerDisabled
1141
+ )
1142
+ })
1143
+ })
1144
+
1145
+ export const layerMessageStorage = (
1146
+ options?: StorageOptions
1147
+ ): Layer.Layer<MessageStorage.MessageStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1148
+ Layer.effect(MessageStorage.MessageStorage, makeMessageStorage(options)).pipe(
1149
+ Layer.provide(Snowflake.layerGenerator)
1150
+ )
1151
+
1152
+ export const layerRunnerStorage = (
1153
+ options?: StorageOptions
1154
+ ): Layer.Layer<RunnerStorage.RunnerStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1155
+ Layer.effect(RunnerStorage.RunnerStorage, makeRunnerStorage(options))
1156
+
1157
+ export const layerStorage = (options?: StorageOptions): Layer.Layer<
1158
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1159
+ never,
1160
+ CosmosClient | ShardingConfig.ShardingConfig
1161
+ > => Layer.merge(layerMessageStorage(options), layerRunnerStorage(options))
1162
+
1163
+ export const layerCosmos = (config: ClusterCosmosConfig): Layer.Layer<
1164
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1165
+ never,
1166
+ ShardingConfig.ShardingConfig
1167
+ > =>
1168
+ layerStorage({ prefix: config.prefix, autoscaleMaxThroughput: config.autoscaleMaxThroughput }).pipe(
1169
+ Layer.provide(CosmosClientLayer(Redacted.value(config.url), config.dbName))
1170
+ )