@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,1118 @@
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 * as Duration from "effect/Duration"
8
+ import * as Redacted from "effect/Redacted"
9
+ import { PersistenceError } from "effect/unstable/cluster/ClusterError"
10
+ import type * as Envelope from "effect/unstable/cluster/Envelope"
11
+ import * as MessageStorage from "effect/unstable/cluster/MessageStorage"
12
+ import { SaveResultEncoded } from "effect/unstable/cluster/MessageStorage"
13
+ import type * as Reply from "effect/unstable/cluster/Reply"
14
+ import * as RunnerStorage from "effect/unstable/cluster/RunnerStorage"
15
+ import * as ShardId from "effect/unstable/cluster/ShardId"
16
+ import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig"
17
+ import * as Snowflake from "effect/unstable/cluster/Snowflake"
18
+ import { CosmosClient, CosmosClientLayer } from "./cosmos-client.ts"
19
+ import { annotateCosmosResponse, annotateDb } from "./otel.ts"
20
+
21
+ export interface ClusterCosmosConfig {
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
+ const createContainer = (prefix: string) =>
348
+ Effect.fnUntraced(function*() {
349
+ const { db } = yield* CosmosClient
350
+ const containerId = `${prefix}cluster`
351
+ yield* Effect
352
+ .tryPromise(() =>
353
+ db.containers.create({
354
+ id: containerId,
355
+ partitionKey: { paths: ["/_partitionKey"], version: 2 }
356
+ })
357
+ )
358
+ .pipe(Effect.catchIf(isConflict, () => Effect.void))
359
+ return db.container(containerId)
360
+ })
361
+
362
+ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
363
+ readonly prefix?: string | undefined
364
+ }) {
365
+ const prefix = options?.prefix ?? "cluster-"
366
+ const container = yield* createContainer(prefix)().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) =>
566
+ Effect
567
+ .gen(function*() {
568
+ const id = String(requestId)
569
+ const replies = yield* queryReplies(
570
+ "SELECT * FROM c WHERE c.type = 'reply' AND c.requestId = @requestId AND c.kind = 'WithExit'",
571
+ [
572
+ { name: "@requestId", value: id }
573
+ ]
574
+ )
575
+ yield* deleteDocs(replies)
576
+ const messages = yield* queryMessages(
577
+ "SELECT * FROM c WHERE c.type = 'message' AND c.requestId = @requestId",
578
+ [{ name: "@requestId", value: id }]
579
+ )
580
+ yield* deleteDocs(messages.filter((message) => message.kind === "Interrupt"))
581
+ yield* patchDocs(
582
+ messages.filter((message) => message.kind !== "Interrupt"),
583
+ () => [
584
+ { op: "set", path: "/processed", value: false },
585
+ { op: "set", path: "/lastReplyId", value: null },
586
+ { op: "set", path: "/lastRead", value: null }
587
+ ]
588
+ )
589
+ })
590
+ .pipe(annotate("clearReplies"), refailPersistence, withTracerDisabled),
591
+
592
+ requestIdForPrimaryKey: (primaryKey) =>
593
+ queryMessages("SELECT * FROM c WHERE c.type = 'message' AND c.messageId = @primaryKey", [
594
+ { name: "@primaryKey", value: primaryKey }
595
+ ])
596
+ .pipe(
597
+ Effect.map((docs) => Option.map(Option.fromNullishOr(docs[0]?.requestId), Snowflake.Snowflake)),
598
+ annotate("requestIdForPrimaryKey"),
599
+ refailPersistence,
600
+ withTracerDisabled
601
+ ),
602
+
603
+ repliesFor: (requestIds) =>
604
+ queryReplies(
605
+ "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",
606
+ [{ name: "@requestIds", value: Array.from(requestIds) }]
607
+ )
608
+ .pipe(
609
+ Effect.map(Arr.map(replyFromDoc)),
610
+ annotate("repliesFor"),
611
+ refailPersistence,
612
+ withTracerDisabled
613
+ ),
614
+
615
+ repliesForUnfiltered: (requestIds) =>
616
+ queryReplies(
617
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId) ORDER BY c.rowid",
618
+ [{ name: "@requestIds", value: Array.from(requestIds) }]
619
+ )
620
+ .pipe(
621
+ Effect.map(Arr.map(replyFromDoc)),
622
+ annotate("repliesForUnfiltered"),
623
+ refailPersistence,
624
+ withTracerDisabled
625
+ ),
626
+
627
+ unprocessedMessages: (shardIds, now) =>
628
+ queryMessages(
629
+ "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) ORDER BY c.rowid",
630
+ [
631
+ { name: "@shardIds", value: Array.from(shardIds) },
632
+ { name: "@lastReadBefore", value: now - tenMinutes },
633
+ { name: "@now", value: now }
634
+ ]
635
+ )
636
+ .pipe(
637
+ Effect.flatMap((docs) => collectUnprocessed(docs, now, claimMessageRead, queryReplies)),
638
+ annotate("unprocessedMessages"),
639
+ refailPersistence,
640
+ withTracerDisabled
641
+ ),
642
+
643
+ unprocessedMessagesById: (messageIds, now) =>
644
+ queryMessages(
645
+ "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",
646
+ [
647
+ { name: "@messageIds", value: Array.from(messageIds, String) },
648
+ { name: "@now", value: now }
649
+ ]
650
+ )
651
+ .pipe(
652
+ Effect.flatMap((docs) => collectUnprocessedById(docs, queryReplies)),
653
+ annotate("unprocessedMessagesById"),
654
+ refailPersistence,
655
+ withTracerDisabled
656
+ ),
657
+
658
+ resetAddress: (address) =>
659
+ queryMessages(
660
+ "SELECT * FROM c WHERE c.type = 'message' AND c.processed = false AND c.shardId = @shardId AND c.entityType = @entityType AND c.entityId = @entityId",
661
+ [
662
+ { name: "@shardId", value: ShardId.toString(address.shardId) },
663
+ { name: "@entityType", value: address.entityType },
664
+ { name: "@entityId", value: address.entityId }
665
+ ]
666
+ )
667
+ .pipe(
668
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
669
+ annotate("resetAddress"),
670
+ refailPersistence,
671
+ withTracerDisabled
672
+ ),
673
+
674
+ clearAddress: (address) =>
675
+ queryMessages(
676
+ "SELECT * FROM c WHERE c.type = 'message' AND c.entityType = @entityType AND c.entityId = @entityId",
677
+ [
678
+ { name: "@entityType", value: address.entityType },
679
+ { name: "@entityId", value: address.entityId }
680
+ ]
681
+ )
682
+ .pipe(
683
+ Effect.flatMap((messages) => {
684
+ if (!Arr.isArrayNonEmpty(messages)) return Effect.void
685
+ const requestIds = Array.from(new Set(messages.map((message) => message.requestId)))
686
+ return queryReplies(
687
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId)",
688
+ [{ name: "@requestIds", value: requestIds }]
689
+ )
690
+ .pipe(
691
+ Effect.flatMap((replies) => deleteDocs(replies)),
692
+ Effect.andThen(deleteDocs(messages))
693
+ )
694
+ }),
695
+ annotate("clearAddress"),
696
+ refailPersistence,
697
+ withTracerDisabled
698
+ ),
699
+
700
+ resetShards: (shardIds) =>
701
+ queryMessages(
702
+ "SELECT * FROM c WHERE c.type = 'message' AND c.processed = false AND ARRAY_CONTAINS(@shardIds, c.shardId)",
703
+ [{ name: "@shardIds", value: Array.from(shardIds) }]
704
+ )
705
+ .pipe(
706
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
707
+ annotate("resetShards"),
708
+ refailPersistence,
709
+ withTracerDisabled
710
+ ),
711
+
712
+ withTransaction: (effect) => effect
713
+ })
714
+ })
715
+
716
+ const collectUnprocessed = <E>(
717
+ docs: ReadonlyArray<MessageDoc>,
718
+ now: number,
719
+ claimMessageRead: (doc: MessageDoc, now: number) => Effect.Effect<boolean, E>,
720
+ queryReplies: (
721
+ query: string,
722
+ parameters: ReadonlyArray<CosmosParameter>
723
+ ) => Effect.Effect<Array<ReplyDoc>, E>
724
+ ) =>
725
+ Effect.gen(function*() {
726
+ const messages: Array<{
727
+ readonly envelope: Envelope.Encoded
728
+ readonly lastSentReply: Option.Option<Reply.Encoded>
729
+ }> = []
730
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
731
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
732
+ for (const doc of docs) {
733
+ if (activeRequestIds.has(doc.requestId)) continue
734
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
735
+ const claimed = yield* claimMessageRead(doc, now)
736
+ if (claimed) {
737
+ messages.push(envelopeFromDoc({ ...doc, lastRead: now }, sentReply))
738
+ }
739
+ }
740
+ return messages
741
+ })
742
+
743
+ const collectUnprocessedById = <E>(
744
+ docs: ReadonlyArray<MessageDoc>,
745
+ queryReplies: (
746
+ query: string,
747
+ parameters: ReadonlyArray<CosmosParameter>
748
+ ) => Effect.Effect<Array<ReplyDoc>, E>
749
+ ) =>
750
+ Effect.gen(function*() {
751
+ const messages: Array<{
752
+ readonly envelope: Envelope.Encoded
753
+ readonly lastSentReply: Option.Option<Reply.Encoded>
754
+ }> = []
755
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
756
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
757
+ for (const doc of docs) {
758
+ if (activeRequestIds.has(doc.requestId)) continue
759
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
760
+ messages.push(envelopeFromDoc(doc, sentReply))
761
+ }
762
+ return messages
763
+ })
764
+
765
+ const activeReplyRequestIds = <E>(
766
+ docs: ReadonlyArray<MessageDoc>,
767
+ queryReplies: (
768
+ query: string,
769
+ parameters: ReadonlyArray<CosmosParameter>
770
+ ) => Effect.Effect<Array<ReplyDoc>, E>
771
+ ) => {
772
+ const requestIds = Array.from(new Set(docs.map((doc) => doc.requestId)))
773
+ if (!Arr.isArrayNonEmpty(requestIds)) return Effect.succeed(new Set<string>())
774
+ return queryReplies(
775
+ "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))",
776
+ [{ name: "@requestIds", value: requestIds }]
777
+ )
778
+ .pipe(Effect.map((replies) => new Set(replies.map((reply) => reply.requestId))))
779
+ }
780
+
781
+ const lastRepliesById = <E>(
782
+ docs: ReadonlyArray<MessageDoc>,
783
+ activeRequestIds: ReadonlySet<string>,
784
+ queryReplies: (
785
+ query: string,
786
+ parameters: ReadonlyArray<CosmosParameter>
787
+ ) => Effect.Effect<Array<ReplyDoc>, E>
788
+ ) => {
789
+ const replyIds = Array.from(
790
+ new Set(docs
791
+ .flatMap((doc) => activeRequestIds.has(doc.requestId) || doc.lastReplyId === null ? [] : [doc.lastReplyId]))
792
+ )
793
+ if (!Arr.isArrayNonEmpty(replyIds)) return Effect.succeed(new Map<string, Reply.Encoded>())
794
+ return queryReplies(
795
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@replyIds, c.rowid)",
796
+ [{ name: "@replyIds", value: replyIds }]
797
+ )
798
+ .pipe(Effect.map((replies) => new Map(replies.map((reply) => [reply.rowid, replyFromDoc(reply)]))))
799
+ }
800
+
801
+ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
802
+ readonly prefix?: string | undefined
803
+ }) {
804
+ const prefix = options?.prefix ?? "cluster-"
805
+ const container = yield* createContainer(prefix)().pipe(Effect.orDie)
806
+ const config = yield* ShardingConfig.ShardingConfig
807
+ const expires = Duration.toMillis(Duration.fromInputUnsafe(config.shardLockExpiration))
808
+ const containerId = `${prefix}cluster`
809
+ const annotate = (operation: string) =>
810
+ annotateDb({ operation, system: "cosmosdb", collection: containerId, entity: "cluster-runner-storage" })
811
+
812
+ const queryRunners = (query: string, parameters: ReadonlyArray<CosmosParameter>) =>
813
+ Effect
814
+ .tryPromise(() =>
815
+ container
816
+ .items
817
+ .query<RunnerDoc>({ query, parameters: Array.from(parameters) }, { partitionKey: "runner" })
818
+ .fetchAll()
819
+ )
820
+ .pipe(Effect.tap(annotateFeed), Effect.map((resp) => resp.resources))
821
+
822
+ const deleteRunner = (doc: RunnerDoc) =>
823
+ Effect
824
+ .tryPromise(() =>
825
+ container.item(doc.id, "runner").delete({
826
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
827
+ })
828
+ )
829
+ .pipe(
830
+ Effect.tap(annotateItem),
831
+ Effect.asVoid,
832
+ Effect.catchIf(isNotFound, () => Effect.void),
833
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
834
+ )
835
+
836
+ const readRunner = (address: string) =>
837
+ Effect.tryPromise(() => container.item(runnerDocId(address), "runner").read<RunnerDoc>()).pipe(
838
+ Effect.tap(annotateItem),
839
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
840
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
841
+ )
842
+
843
+ // Allocate a unique, monotonically increasing machine id via an atomic
844
+ // server-side `incr` on a single counter doc — mirrors SQL's auto-increment
845
+ // `machine_id` PK. A per-address string hash (the previous approach) could
846
+ // collide mod 1024 across distinct runners, yielding duplicate Snowflake ids.
847
+ // The counter doc is seeded at init below, so it always exists here.
848
+ const allocateMachineId = Effect
849
+ .tryPromise(() =>
850
+ container.item(machineIdCounterDocId, "runner").patch<MachineIdCounterDoc>([
851
+ { op: "incr", path: "/value", value: 1 }
852
+ ])
853
+ )
854
+ .pipe(Effect.tap(annotateItem), Effect.map((resp) => resp.resource!.value))
855
+
856
+ // Seed the counter once (create-if-missing); a lost create race is fine.
857
+ yield* Effect
858
+ .tryPromise(() =>
859
+ container.items.create<MachineIdCounterDoc>({
860
+ id: machineIdCounterDocId,
861
+ _partitionKey: "runner",
862
+ type: "machine-id-counter",
863
+ value: 0
864
+ })
865
+ )
866
+ .pipe(Effect.catchIf(isConflict, () => Effect.void), Effect.orDie)
867
+
868
+ const readLock = (shardId: string) =>
869
+ Effect.tryPromise(() => container.item(lockDocId(shardId), "lock").read<LockDoc>()).pipe(
870
+ Effect.tap(annotateItem),
871
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
872
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
873
+ )
874
+
875
+ const writeLock = (doc: LockDoc) =>
876
+ Effect
877
+ .tryPromise(() =>
878
+ container.item(doc.id, "lock").replace(doc, {
879
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
880
+ })
881
+ )
882
+ .pipe(
883
+ Effect.tap(annotateItem),
884
+ Effect.as(true),
885
+ Effect.catchIf(isPreconditionFailed, () => Effect.succeed(false))
886
+ )
887
+
888
+ const deleteLock = (doc: LockDoc) =>
889
+ Effect
890
+ .tryPromise(() =>
891
+ container.item(doc.id, "lock").delete({
892
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
893
+ })
894
+ )
895
+ .pipe(
896
+ Effect.tap(annotateItem),
897
+ Effect.asVoid,
898
+ Effect.catchIf(isNotFound, () => Effect.void),
899
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
900
+ )
901
+
902
+ const createLock = (address: string, shardId: string, now: number) =>
903
+ Effect
904
+ .tryPromise(() =>
905
+ container.items.create<LockDoc>({
906
+ id: lockDocId(shardId),
907
+ _partitionKey: "lock",
908
+ type: "lock",
909
+ shardId,
910
+ address,
911
+ acquiredAt: now
912
+ })
913
+ )
914
+ .pipe(
915
+ Effect.tap(annotateItem),
916
+ Effect.as(true),
917
+ Effect.catchIf(isConflict, () => Effect.succeed(false))
918
+ )
919
+
920
+ const tryAcquire = (address: string, shardId: string, now: number) =>
921
+ readLock(shardId).pipe(
922
+ Effect.flatMap((lock) =>
923
+ Option.match(lock, {
924
+ onNone: () => createLock(address, shardId, now),
925
+ onSome: (doc) => {
926
+ if (doc.address !== address && now - doc.acquiredAt <= expires) {
927
+ return Effect.succeed(false)
928
+ }
929
+ doc.address = address
930
+ doc.acquiredAt = now
931
+ return writeLock(doc)
932
+ }
933
+ })
934
+ ),
935
+ Effect.map((acquired) => acquired ? Option.some(shardId) : Option.none())
936
+ )
937
+
938
+ return RunnerStorage.makeEncoded({
939
+ getRunners: Effect.sync(() => Date.now()).pipe(
940
+ Effect.flatMap((now) =>
941
+ queryRunners("SELECT * FROM c WHERE c.type = 'runner' AND c.lastHeartbeat > @expiresAt", [
942
+ { name: "@expiresAt", value: now - expires }
943
+ ])
944
+ ),
945
+ Effect.map((docs) => docs.map((doc) => [doc.runner, doc.healthy] as const)),
946
+ annotate("getRunners"),
947
+ refailPersistence,
948
+ withTracerDisabled
949
+ ),
950
+
951
+ register: (address, runner, healthy) =>
952
+ Effect
953
+ .gen(function*() {
954
+ const now = Date.now()
955
+ // Reuse a previously-allocated machine id for this address (stable per
956
+ // address, like SQL's RETURNING on conflict); allocate a fresh unique
957
+ // one on first registration.
958
+ const existing = yield* readRunner(address)
959
+ const machineId = Option.isSome(existing) && existing.value.machineId !== undefined
960
+ ? existing.value.machineId
961
+ : yield* allocateMachineId
962
+ yield* Effect
963
+ .tryPromise(() =>
964
+ container.items.upsert<RunnerDoc>({
965
+ id: runnerDocId(address),
966
+ _partitionKey: "runner",
967
+ type: "runner",
968
+ address,
969
+ runner,
970
+ healthy,
971
+ lastHeartbeat: now,
972
+ machineId
973
+ })
974
+ )
975
+ .pipe(Effect.tap(annotateItem))
976
+ return machineId
977
+ })
978
+ .pipe(annotate("register"), refailPersistence, withTracerDisabled),
979
+
980
+ unregister: (address) =>
981
+ Effect.sync(() => Date.now()).pipe(
982
+ Effect.flatMap((now) =>
983
+ queryRunners(
984
+ "SELECT * FROM c WHERE c.type = 'runner' AND (c.address = @address OR c.lastHeartbeat < @expiresAt)",
985
+ [
986
+ { name: "@address", value: address },
987
+ { name: "@expiresAt", value: now - expires }
988
+ ]
989
+ )
990
+ ),
991
+ Effect.flatMap((docs) => Effect.forEach(docs, deleteRunner, { discard: true })),
992
+ annotate("unregister"),
993
+ refailPersistence,
994
+ withTracerDisabled
995
+ ),
996
+
997
+ setRunnerHealth: (address, healthy) =>
998
+ Effect
999
+ .tryPromise(() =>
1000
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1001
+ { op: "set", path: "/healthy", value: healthy }
1002
+ ])
1003
+ )
1004
+ .pipe(
1005
+ Effect.tap(annotateItem),
1006
+ Effect.asVoid,
1007
+ Effect.catchIf(isNotFound, () => Effect.void),
1008
+ annotate("setRunnerHealth"),
1009
+ refailPersistence,
1010
+ withTracerDisabled
1011
+ ),
1012
+
1013
+ acquire: (address, shardIds) =>
1014
+ Effect.sync(() => Date.now()).pipe(
1015
+ Effect.flatMap((now) => Effect.forEach(shardIds, (shardId) => tryAcquire(address, shardId, now))),
1016
+ Effect.map(Arr.getSomes),
1017
+ annotate("acquire"),
1018
+ refailPersistence,
1019
+ withTracerDisabled
1020
+ ),
1021
+
1022
+ refresh: (address, shardIds) =>
1023
+ Effect
1024
+ .gen(function*() {
1025
+ const now = Date.now()
1026
+ yield* Effect
1027
+ .tryPromise(() =>
1028
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1029
+ { op: "set", path: "/lastHeartbeat", value: now }
1030
+ ])
1031
+ )
1032
+ .pipe(
1033
+ Effect.tap(annotateItem),
1034
+ Effect.asVoid,
1035
+ Effect.catchIf(isNotFound, () => Effect.void)
1036
+ )
1037
+ const refreshed = yield* Effect.forEach(shardIds, (shardId) =>
1038
+ readLock(shardId).pipe(
1039
+ Effect.flatMap((lock) =>
1040
+ Option.match(lock, {
1041
+ onNone: () => Effect.succeed(Option.none<string>()),
1042
+ onSome: (doc) => {
1043
+ if (doc.address !== address) return Effect.succeed(Option.none<string>())
1044
+ doc.acquiredAt = now
1045
+ return writeLock(doc).pipe(Effect.map((ok) => ok ? Option.some(shardId) : Option.none<string>()))
1046
+ }
1047
+ })
1048
+ )
1049
+ ))
1050
+ return Arr.getSomes(refreshed)
1051
+ })
1052
+ .pipe(annotate("refresh"), refailPersistence, withTracerDisabled),
1053
+
1054
+ release: (address, shardId) =>
1055
+ readLock(shardId).pipe(
1056
+ Effect.flatMap((lock) =>
1057
+ Option.match(lock, {
1058
+ onNone: () => Effect.void,
1059
+ onSome: (doc) =>
1060
+ doc.address === address
1061
+ ? deleteLock(doc)
1062
+ : Effect.void
1063
+ })
1064
+ ),
1065
+ annotate("release"),
1066
+ refailPersistence,
1067
+ withTracerDisabled
1068
+ ),
1069
+
1070
+ releaseAll: (address) =>
1071
+ Effect
1072
+ .tryPromise(() =>
1073
+ container
1074
+ .items
1075
+ .query<LockDoc>({
1076
+ query: "SELECT * FROM c WHERE c.type = 'lock' AND c.address = @address",
1077
+ parameters: [{ name: "@address", value: address }]
1078
+ }, { partitionKey: "lock" })
1079
+ .fetchAll()
1080
+ )
1081
+ .pipe(
1082
+ Effect.tap(annotateFeed),
1083
+ Effect.flatMap((resp) => Effect.forEach(resp.resources, deleteLock, { discard: true })),
1084
+ annotate("releaseAll"),
1085
+ refailPersistence,
1086
+ withTracerDisabled
1087
+ )
1088
+ })
1089
+ })
1090
+
1091
+ export const layerMessageStorage = (options?: {
1092
+ readonly prefix?: string | undefined
1093
+ }): Layer.Layer<MessageStorage.MessageStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1094
+ Layer.effect(MessageStorage.MessageStorage, makeMessageStorage(options)).pipe(
1095
+ Layer.provide(Snowflake.layerGenerator)
1096
+ )
1097
+
1098
+ export const layerRunnerStorage = (options?: {
1099
+ readonly prefix?: string | undefined
1100
+ }): Layer.Layer<RunnerStorage.RunnerStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1101
+ Layer.effect(RunnerStorage.RunnerStorage, makeRunnerStorage(options))
1102
+
1103
+ export const layerStorage = (options?: {
1104
+ readonly prefix?: string | undefined
1105
+ }): Layer.Layer<
1106
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1107
+ never,
1108
+ CosmosClient | ShardingConfig.ShardingConfig
1109
+ > => Layer.merge(layerMessageStorage(options), layerRunnerStorage(options))
1110
+
1111
+ export const layerCosmos = (config: ClusterCosmosConfig): Layer.Layer<
1112
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1113
+ never,
1114
+ ShardingConfig.ShardingConfig
1115
+ > =>
1116
+ layerStorage({ prefix: config.prefix }).pipe(
1117
+ Layer.provide(CosmosClientLayer(Redacted.value(config.url), config.dbName))
1118
+ )