@effect-app/infra 4.0.0-beta.33 → 4.0.0-beta.330

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 (490) hide show
  1. package/CHANGELOG.md +2555 -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 +36 -15
  6. package/dist/ClusterCosmos.d.ts +64 -0
  7. package/dist/ClusterCosmos.d.ts.map +1 -0
  8. package/dist/ClusterCosmos.js +576 -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/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 +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 +395 -256
  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 +93 -42
  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 +349 -79
  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 +261 -0
  47. package/dist/Store/SQL/query.d.ts +53 -0
  48. package/dist/Store/SQL/query.d.ts.map +1 -0
  49. package/dist/Store/SQL/query.js +656 -0
  50. package/dist/Store/SQL.d.ts +28 -0
  51. package/dist/Store/SQL.d.ts.map +1 -0
  52. package/dist/Store/SQL.js +483 -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 +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 +18 -5
  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 +24 -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/internal/health.d.ts.map +1 -0
  98. package/dist/internal/health.js +5 -0
  99. package/dist/{api/layerUtils.d.ts → layerUtils.d.ts} +14 -6
  100. package/dist/layerUtils.d.ts.map +1 -0
  101. package/dist/layerUtils.js +17 -0
  102. package/dist/logger/jsonLogger.d.ts +1 -1
  103. package/dist/logger/jsonLogger.d.ts.map +1 -1
  104. package/dist/logger/jsonLogger.js +5 -3
  105. package/dist/logger/logFmtLogger.d.ts +1 -1
  106. package/dist/logger/logFmtLogger.d.ts.map +1 -1
  107. package/dist/logger/logFmtLogger.js +3 -3
  108. package/dist/logger/shared.d.ts +2 -2
  109. package/dist/logger/shared.d.ts.map +1 -1
  110. package/dist/logger/shared.js +6 -6
  111. package/dist/logger.d.ts +4 -4
  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} +3 -2
  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} +5 -2
  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 +4 -3
  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/routing/middleware/middleware.d.ts +48 -0
  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 +25 -21
  157. package/package.json +38 -352
  158. package/src/CUPS.ts +54 -15
  159. package/src/ClusterCosmos.ts +1150 -0
  160. package/src/ClusterServiceBus.ts +244 -0
  161. package/src/{api/ContextProvider.ts → ContextProvider.ts} +20 -17
  162. package/src/Emailer/Sendgrid.ts +83 -60
  163. package/src/Emailer/fake.ts +4 -4
  164. package/src/MainFiberSet.ts +32 -13
  165. package/src/QueueMaker/errors.ts +3 -1
  166. package/src/{adapters/SQL → SQL}/Model.ts +42 -41
  167. package/src/SQL.ts +1 -0
  168. package/src/Store/Cosmos/query.ts +275 -55
  169. package/src/Store/Cosmos.ts +547 -366
  170. package/src/Store/Disk.ts +146 -73
  171. package/src/Store/Memory.ts +420 -115
  172. package/src/Store/SQL/Pg.ts +412 -0
  173. package/src/Store/SQL/query.ts +748 -0
  174. package/src/Store/SQL.ts +806 -0
  175. package/src/Store/codeFilter.ts +78 -28
  176. package/src/Store/index.ts +25 -10
  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 +56 -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 +22 -13
  189. package/src/{api/layerUtils.ts → layerUtils.ts} +14 -10
  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} +4 -2
  195. package/src/otel.ts +177 -0
  196. package/src/rateLimit.ts +35 -24
  197. package/src/{adapters/redis-client.ts → redis-client.ts} +7 -3
  198. package/src/{api/reportError.ts → reportError.ts} +3 -2
  199. package/src/{api/routing → routing}/middleware/RouterMiddleware.ts +5 -4
  200. package/src/routing/middleware/middleware.ts +196 -0
  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 +612 -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 +15 -12
  214. package/test/controller.test.ts +27 -31
  215. package/test/cosmos-container-throughput.test.ts +61 -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 +29 -11
  228. package/test/dist/fixtures.d.ts.map +1 -1
  229. package/test/dist/fixtures.js +17 -10
  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/sql-store.test.d.ts.map +1 -0
  243. package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
  244. package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
  245. package/test/fixtures.ts +16 -9
  246. package/test/json-lower.test.ts +129 -0
  247. package/test/layerUtils.test.ts +2 -2
  248. package/test/query.test.ts +1382 -44
  249. package/test/rawQuery.test.ts +433 -8
  250. package/test/repository-ext.test.ts +456 -0
  251. package/test/repository-legacy-document.test.ts +394 -0
  252. package/test/requires.test.ts +10 -5
  253. package/test/router-generator.test.ts +187 -0
  254. package/test/routing-interruptibility.test.ts +66 -0
  255. package/test/rpc-context-map-streaming.test.ts +265 -0
  256. package/test/rpc-e2e-invalidation.test.ts +421 -0
  257. package/test/rpc-multi-middleware.test.ts +85 -10
  258. package/test/rpc-stream-fullstack.test.ts +307 -0
  259. package/test/sql-store.test.ts +1931 -0
  260. package/test/validateSample.test.ts +71 -43
  261. package/test/workflow-engine-cosmos.test.ts +345 -0
  262. package/test/workflow-engine-sqlite.test.ts +297 -0
  263. package/tsconfig.examples.json +1 -1
  264. package/tsconfig.json +9 -2
  265. package/tsconfig.src.json +35 -35
  266. package/tsconfig.test.json +2 -2
  267. package/dist/Emailer/service.d.ts +0 -55
  268. package/dist/Emailer/service.d.ts.map +0 -1
  269. package/dist/Emailer/service.js +0 -6
  270. package/dist/Emailer.d.ts +0 -2
  271. package/dist/Emailer.d.ts.map +0 -1
  272. package/dist/Emailer.js +0 -2
  273. package/dist/Model/Repository/ext.d.ts +0 -41
  274. package/dist/Model/Repository/ext.d.ts.map +0 -1
  275. package/dist/Model/Repository/ext.js +0 -65
  276. package/dist/Model/Repository/internal/internal.d.ts +0 -59
  277. package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
  278. package/dist/Model/Repository/internal/internal.js +0 -316
  279. package/dist/Model/Repository/legacy.d.ts +0 -19
  280. package/dist/Model/Repository/legacy.d.ts.map +0 -1
  281. package/dist/Model/Repository/legacy.js +0 -2
  282. package/dist/Model/Repository/makeRepo.d.ts +0 -49
  283. package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
  284. package/dist/Model/Repository/makeRepo.js +0 -24
  285. package/dist/Model/Repository/service.d.ts +0 -89
  286. package/dist/Model/Repository/service.d.ts.map +0 -1
  287. package/dist/Model/Repository/service.js +0 -2
  288. package/dist/Model/Repository/validation.d.ts +0 -42
  289. package/dist/Model/Repository/validation.d.ts.map +0 -1
  290. package/dist/Model/Repository/validation.js +0 -32
  291. package/dist/Model/Repository.d.ts +0 -6
  292. package/dist/Model/Repository.d.ts.map +0 -1
  293. package/dist/Model/Repository.js +0 -6
  294. package/dist/Model/dsl.d.ts +0 -32
  295. package/dist/Model/dsl.d.ts.map +0 -1
  296. package/dist/Model/dsl.js +0 -44
  297. package/dist/Model/filter/filterApi.d.ts +0 -30
  298. package/dist/Model/filter/filterApi.d.ts.map +0 -1
  299. package/dist/Model/filter/filterApi.js +0 -2
  300. package/dist/Model/filter/types/errors.d.ts +0 -29
  301. package/dist/Model/filter/types/errors.d.ts.map +0 -1
  302. package/dist/Model/filter/types/errors.js +0 -2
  303. package/dist/Model/filter/types/fields.d.ts +0 -15
  304. package/dist/Model/filter/types/fields.d.ts.map +0 -1
  305. package/dist/Model/filter/types/fields.js +0 -2
  306. package/dist/Model/filter/types/path/common.d.ts +0 -316
  307. package/dist/Model/filter/types/path/common.d.ts.map +0 -1
  308. package/dist/Model/filter/types/path/common.js +0 -2
  309. package/dist/Model/filter/types/path/eager.d.ts +0 -95
  310. package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
  311. package/dist/Model/filter/types/path/eager.js +0 -31
  312. package/dist/Model/filter/types/path/index.d.ts +0 -4
  313. package/dist/Model/filter/types/path/index.d.ts.map +0 -1
  314. package/dist/Model/filter/types/path/index.js +0 -3
  315. package/dist/Model/filter/types/utils.d.ts +0 -79
  316. package/dist/Model/filter/types/utils.d.ts.map +0 -1
  317. package/dist/Model/filter/types/utils.js +0 -2
  318. package/dist/Model/filter/types/validator.d.ts +0 -30
  319. package/dist/Model/filter/types/validator.d.ts.map +0 -1
  320. package/dist/Model/filter/types/validator.js +0 -2
  321. package/dist/Model/filter/types.d.ts +0 -5
  322. package/dist/Model/filter/types.d.ts.map +0 -1
  323. package/dist/Model/filter/types.js +0 -7
  324. package/dist/Model/query/dsl.d.ts +0 -248
  325. package/dist/Model/query/dsl.d.ts.map +0 -1
  326. package/dist/Model/query/dsl.js +0 -104
  327. package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
  328. package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
  329. package/dist/Model/query/new-kid-interpreter.js +0 -165
  330. package/dist/Model/query.d.ts +0 -15
  331. package/dist/Model/query.d.ts.map +0 -1
  332. package/dist/Model/query.js +0 -3
  333. package/dist/Model.d.ts +0 -4
  334. package/dist/Model.d.ts.map +0 -1
  335. package/dist/Model.js +0 -4
  336. package/dist/Operations.d.ts +0 -55
  337. package/dist/Operations.d.ts.map +0 -1
  338. package/dist/Operations.js +0 -102
  339. package/dist/OperationsRepo.d.ts +0 -41
  340. package/dist/OperationsRepo.d.ts.map +0 -1
  341. package/dist/OperationsRepo.js +0 -14
  342. package/dist/QueueMaker/SQLQueue.d.ts +0 -16
  343. package/dist/QueueMaker/SQLQueue.d.ts.map +0 -1
  344. package/dist/QueueMaker/SQLQueue.js +0 -130
  345. package/dist/QueueMaker/memQueue.d.ts +0 -11
  346. package/dist/QueueMaker/memQueue.d.ts.map +0 -1
  347. package/dist/QueueMaker/memQueue.js +0 -76
  348. package/dist/QueueMaker/sbqueue.d.ts +0 -12
  349. package/dist/QueueMaker/sbqueue.d.ts.map +0 -1
  350. package/dist/QueueMaker/sbqueue.js +0 -91
  351. package/dist/QueueMaker/service.d.ts +0 -11
  352. package/dist/QueueMaker/service.d.ts.map +0 -1
  353. package/dist/QueueMaker/service.js +0 -4
  354. package/dist/RequestContext.d.ts +0 -63
  355. package/dist/RequestContext.d.ts.map +0 -1
  356. package/dist/RequestContext.js +0 -49
  357. package/dist/RequestFiberSet.d.ts +0 -36
  358. package/dist/RequestFiberSet.d.ts.map +0 -1
  359. package/dist/RequestFiberSet.js +0 -78
  360. package/dist/Store/ContextMapContainer.d.ts +0 -14
  361. package/dist/Store/ContextMapContainer.d.ts.map +0 -1
  362. package/dist/Store/ContextMapContainer.js +0 -16
  363. package/dist/Store/service.d.ts +0 -108
  364. package/dist/Store/service.d.ts.map +0 -1
  365. package/dist/Store/service.js +0 -71
  366. package/dist/Store.d.ts +0 -2
  367. package/dist/Store.d.ts.map +0 -1
  368. package/dist/Store.js +0 -2
  369. package/dist/adapters/SQL/Model.d.ts.map +0 -1
  370. package/dist/adapters/SQL/Model.js +0 -478
  371. package/dist/adapters/SQL.d.ts.map +0 -1
  372. package/dist/adapters/ServiceBus.d.ts +0 -58
  373. package/dist/adapters/ServiceBus.d.ts.map +0 -1
  374. package/dist/adapters/ServiceBus.js +0 -99
  375. package/dist/adapters/cosmos-client.d.ts +0 -14
  376. package/dist/adapters/cosmos-client.d.ts.map +0 -1
  377. package/dist/adapters/cosmos-client.js +0 -9
  378. package/dist/adapters/index.d.ts +0 -2
  379. package/dist/adapters/index.d.ts.map +0 -1
  380. package/dist/adapters/index.js +0 -2
  381. package/dist/adapters/logger.d.ts +0 -9
  382. package/dist/adapters/logger.d.ts.map +0 -1
  383. package/dist/adapters/logger.js +0 -3
  384. package/dist/adapters/memQueue.d.ts +0 -13
  385. package/dist/adapters/memQueue.d.ts.map +0 -1
  386. package/dist/adapters/memQueue.js +0 -20
  387. package/dist/adapters/mongo-client.d.ts.map +0 -1
  388. package/dist/adapters/mongo-client.js +0 -13
  389. package/dist/adapters/redis-client.d.ts.map +0 -1
  390. package/dist/adapters/redis-client.js +0 -94
  391. package/dist/api/ContextProvider.d.ts +0 -31
  392. package/dist/api/ContextProvider.d.ts.map +0 -1
  393. package/dist/api/ContextProvider.js +0 -38
  394. package/dist/api/codec.d.ts.map +0 -1
  395. package/dist/api/codec.js +0 -5
  396. package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
  397. package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
  398. package/dist/api/internal/RequestContextMiddleware.js +0 -35
  399. package/dist/api/internal/auth.d.ts +0 -15
  400. package/dist/api/internal/auth.d.ts.map +0 -1
  401. package/dist/api/internal/auth.js +0 -47
  402. package/dist/api/internal/events.d.ts +0 -9
  403. package/dist/api/internal/events.d.ts.map +0 -1
  404. package/dist/api/internal/events.js +0 -42
  405. package/dist/api/internal/health.d.ts.map +0 -1
  406. package/dist/api/internal/health.js +0 -5
  407. package/dist/api/layerUtils.d.ts.map +0 -1
  408. package/dist/api/layerUtils.js +0 -16
  409. package/dist/api/middlewares.d.ts.map +0 -1
  410. package/dist/api/reportError.d.ts.map +0 -1
  411. package/dist/api/reportError.js +0 -27
  412. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
  413. package/dist/api/routing/middleware/middleware.d.ts +0 -9
  414. package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
  415. package/dist/api/routing/middleware/middleware.js +0 -92
  416. package/dist/api/routing/middleware.d.ts.map +0 -1
  417. package/dist/api/routing/schema/jwt.d.ts.map +0 -1
  418. package/dist/api/routing/schema/jwt.js +0 -12
  419. package/dist/api/routing/tsort.d.ts.map +0 -1
  420. package/dist/api/routing/tsort.js +0 -51
  421. package/dist/api/routing/utils.d.ts.map +0 -1
  422. package/dist/api/routing/utils.js +0 -44
  423. package/dist/api/routing.d.ts +0 -138
  424. package/dist/api/routing.d.ts.map +0 -1
  425. package/dist/api/routing.js +0 -166
  426. package/dist/api/setupRequest.d.ts +0 -12
  427. package/dist/api/setupRequest.d.ts.map +0 -1
  428. package/dist/api/setupRequest.js +0 -44
  429. package/dist/api/util.d.ts.map +0 -1
  430. package/dist/api/util.js +0 -14
  431. package/eslint.config.mjs +0 -24
  432. package/src/Emailer/service.ts +0 -52
  433. package/src/Emailer.ts +0 -1
  434. package/src/Model/Repository/ext.ts +0 -283
  435. package/src/Model/Repository/internal/internal.ts +0 -577
  436. package/src/Model/Repository/legacy.ts +0 -27
  437. package/src/Model/Repository/makeRepo.ts +0 -139
  438. package/src/Model/Repository/service.ts +0 -627
  439. package/src/Model/Repository/validation.ts +0 -31
  440. package/src/Model/Repository.ts +0 -5
  441. package/src/Model/dsl.ts +0 -128
  442. package/src/Model/filter/filterApi.ts +0 -60
  443. package/src/Model/filter/types/errors.ts +0 -47
  444. package/src/Model/filter/types/fields.ts +0 -50
  445. package/src/Model/filter/types/path/common.ts +0 -404
  446. package/src/Model/filter/types/path/eager.ts +0 -298
  447. package/src/Model/filter/types/path/index.ts +0 -4
  448. package/src/Model/filter/types/utils.ts +0 -128
  449. package/src/Model/filter/types/validator.ts +0 -46
  450. package/src/Model/filter/types.ts +0 -6
  451. package/src/Model/query/dsl.ts +0 -2110
  452. package/src/Model/query/new-kid-interpreter.ts +0 -210
  453. package/src/Model/query.ts +0 -13
  454. package/src/Model.ts +0 -3
  455. package/src/Operations.ts +0 -235
  456. package/src/OperationsRepo.ts +0 -16
  457. package/src/QueueMaker/SQLQueue.ts +0 -189
  458. package/src/QueueMaker/memQueue.ts +0 -130
  459. package/src/QueueMaker/sbqueue.ts +0 -137
  460. package/src/QueueMaker/service.ts +0 -17
  461. package/src/RequestContext.ts +0 -63
  462. package/src/RequestFiberSet.ts +0 -105
  463. package/src/Store/ContextMapContainer.ts +0 -20
  464. package/src/Store/index.test.ts.bak +0 -112
  465. package/src/Store/service.ts +0 -184
  466. package/src/Store.ts +0 -1
  467. package/src/adapters/SQL.ts +0 -1
  468. package/src/adapters/ServiceBus.ts +0 -209
  469. package/src/adapters/cosmos-client.ts +0 -16
  470. package/src/adapters/index.ts +0 -0
  471. package/src/adapters/logger.ts +0 -3
  472. package/src/adapters/memQueue.ts +0 -21
  473. package/src/api/internal/auth.ts +0 -68
  474. package/src/api/middlewares.ts +0 -10
  475. package/src/api/routing/middleware/middleware.ts +0 -128
  476. package/src/api/routing/middleware.ts +0 -6
  477. package/src/api/routing.ts +0 -598
  478. package/src/api/setupRequest.ts +0 -84
  479. package/tsconfig.json.bak +0 -12
  480. /package/dist/{adapters/SQL.d.ts → SQL.d.ts} +0 -0
  481. /package/dist/{api/codec.d.ts → codec.d.ts} +0 -0
  482. /package/dist/{api/internal → internal}/health.d.ts +0 -0
  483. /package/dist/{api/middlewares.d.ts → middlewares.d.ts} +0 -0
  484. /package/dist/{api/routing → routing}/schema/jwt.d.ts +0 -0
  485. /package/dist/{api/routing → routing}/tsort.d.ts +0 -0
  486. /package/dist/{api/util.d.ts → util.d.ts} +0 -0
  487. /package/src/{api/codec.ts → codec.ts} +0 -0
  488. /package/src/{api/internal → internal}/health.ts +0 -0
  489. /package/src/{api/routing → routing}/tsort.ts +0 -0
  490. /package/src/{api/util.ts → util.ts} +0 -0
@@ -0,0 +1,1150 @@
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, 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) =>
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, options) => {
628
+ if (options?.addresses !== undefined && options.addresses.length === 0) {
629
+ return Effect.succeed([])
630
+ }
631
+ const parameters: Array<CosmosParameter> = [
632
+ { name: "@shardIds", value: Array.from(shardIds) },
633
+ { name: "@lastReadBefore", value: now - tenMinutes },
634
+ { name: "@now", value: now }
635
+ ]
636
+ let query =
637
+ "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)"
638
+ if (options?.addresses !== undefined) {
639
+ const match = addressClauses(options.addresses, "a")
640
+ query += ` AND (${match.sql})`
641
+ parameters.push(...match.parameters)
642
+ }
643
+ query += " ORDER BY c.rowid"
644
+ if (options?.limit !== undefined) {
645
+ query += ` OFFSET 0 LIMIT ${Math.floor(options.limit)}`
646
+ }
647
+ return queryMessages(query, parameters)
648
+ .pipe(
649
+ Effect.flatMap((docs) => collectUnprocessed(docs, now, claimMessageRead, queryReplies)),
650
+ annotate("unprocessedMessages"),
651
+ refailPersistence,
652
+ withTracerDisabled
653
+ )
654
+ },
655
+
656
+ unprocessedMessagesById: (messageIds, now) =>
657
+ queryMessages(
658
+ "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",
659
+ [
660
+ { name: "@messageIds", value: Array.from(messageIds, String) },
661
+ { name: "@now", value: now }
662
+ ]
663
+ )
664
+ .pipe(
665
+ Effect.flatMap((docs) => collectUnprocessedById(docs, queryReplies)),
666
+ annotate("unprocessedMessagesById"),
667
+ refailPersistence,
668
+ withTracerDisabled
669
+ ),
670
+
671
+ resetAddresses: (addresses) => {
672
+ if (addresses.length === 0) return Effect.void
673
+ const match = addressClauses(addresses, "r")
674
+ return queryMessages(
675
+ `SELECT * FROM c WHERE c.type = 'message' AND c.processed = false AND (${match.sql})`,
676
+ match.parameters
677
+ )
678
+ .pipe(
679
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
680
+ annotate("resetAddresses"),
681
+ refailPersistence,
682
+ withTracerDisabled
683
+ )
684
+ },
685
+
686
+ clearAddress: (address) =>
687
+ queryMessages(
688
+ "SELECT * FROM c WHERE c.type = 'message' AND c.entityType = @entityType AND c.entityId = @entityId",
689
+ [
690
+ { name: "@entityType", value: address.entityType },
691
+ { name: "@entityId", value: address.entityId }
692
+ ]
693
+ )
694
+ .pipe(
695
+ Effect.flatMap((messages) => {
696
+ if (!Arr.isArrayNonEmpty(messages)) return Effect.void
697
+ const requestIds = Array.from(new Set(messages.map((message) => message.requestId)))
698
+ return queryReplies(
699
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId)",
700
+ [{ name: "@requestIds", value: requestIds }]
701
+ )
702
+ .pipe(
703
+ Effect.flatMap((replies) => deleteDocs(replies)),
704
+ Effect.andThen(deleteDocs(messages))
705
+ )
706
+ }),
707
+ annotate("clearAddress"),
708
+ refailPersistence,
709
+ withTracerDisabled
710
+ ),
711
+
712
+ resetShards: (shardIds) =>
713
+ queryMessages(
714
+ "SELECT * FROM c WHERE c.type = 'message' AND c.processed = false AND ARRAY_CONTAINS(@shardIds, c.shardId)",
715
+ [{ name: "@shardIds", value: Array.from(shardIds) }]
716
+ )
717
+ .pipe(
718
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
719
+ annotate("resetShards"),
720
+ refailPersistence,
721
+ withTracerDisabled
722
+ ),
723
+
724
+ withTransaction: (effect) => effect
725
+ })
726
+ })
727
+
728
+ const addressClauses = (
729
+ addresses: ReadonlyArray<{
730
+ readonly shardId: Parameters<typeof ShardId.toString>[0]
731
+ readonly entityType: string
732
+ readonly entityId: string
733
+ }>,
734
+ prefix: string
735
+ ): { readonly sql: string; readonly parameters: Array<CosmosParameter> } => {
736
+ const parameters: Array<CosmosParameter> = []
737
+ const clauses: Array<string> = []
738
+ for (const [i, address] of addresses.entries()) {
739
+ const shard = `@${prefix}s${i}`
740
+ const entityType = `@${prefix}t${i}`
741
+ const entityId = `@${prefix}e${i}`
742
+ clauses.push(`(c.shardId = ${shard} AND c.entityType = ${entityType} AND c.entityId = ${entityId})`)
743
+ parameters.push(
744
+ { name: shard, value: ShardId.toString(address.shardId) },
745
+ { name: entityType, value: address.entityType },
746
+ { name: entityId, value: address.entityId }
747
+ )
748
+ }
749
+ return { sql: clauses.join(" OR "), parameters }
750
+ }
751
+
752
+ const collectUnprocessed = <E>(
753
+ docs: ReadonlyArray<MessageDoc>,
754
+ now: number,
755
+ claimMessageRead: (doc: MessageDoc, now: number) => Effect.Effect<boolean, E>,
756
+ queryReplies: (
757
+ query: string,
758
+ parameters: ReadonlyArray<CosmosParameter>
759
+ ) => Effect.Effect<Array<ReplyDoc>, E>
760
+ ) =>
761
+ Effect.gen(function*() {
762
+ const messages: Array<{
763
+ readonly envelope: Envelope.Encoded
764
+ readonly lastSentReply: Option.Option<Reply.Encoded>
765
+ }> = []
766
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
767
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
768
+ for (const doc of docs) {
769
+ if (activeRequestIds.has(doc.requestId)) continue
770
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
771
+ const claimed = yield* claimMessageRead(doc, now)
772
+ if (claimed) {
773
+ messages.push(envelopeFromDoc({ ...doc, lastRead: now }, sentReply))
774
+ }
775
+ }
776
+ return messages
777
+ })
778
+
779
+ const collectUnprocessedById = <E>(
780
+ docs: ReadonlyArray<MessageDoc>,
781
+ queryReplies: (
782
+ query: string,
783
+ parameters: ReadonlyArray<CosmosParameter>
784
+ ) => Effect.Effect<Array<ReplyDoc>, E>
785
+ ) =>
786
+ Effect.gen(function*() {
787
+ const messages: Array<{
788
+ readonly envelope: Envelope.Encoded
789
+ readonly lastSentReply: Option.Option<Reply.Encoded>
790
+ }> = []
791
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
792
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
793
+ for (const doc of docs) {
794
+ if (activeRequestIds.has(doc.requestId)) continue
795
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
796
+ messages.push(envelopeFromDoc(doc, sentReply))
797
+ }
798
+ return messages
799
+ })
800
+
801
+ const activeReplyRequestIds = <E>(
802
+ docs: ReadonlyArray<MessageDoc>,
803
+ queryReplies: (
804
+ query: string,
805
+ parameters: ReadonlyArray<CosmosParameter>
806
+ ) => Effect.Effect<Array<ReplyDoc>, E>
807
+ ) => {
808
+ const requestIds = Array.from(new Set(docs.map((doc) => doc.requestId)))
809
+ if (!Arr.isArrayNonEmpty(requestIds)) return Effect.succeed(new Set<string>())
810
+ return queryReplies(
811
+ "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))",
812
+ [{ name: "@requestIds", value: requestIds }]
813
+ )
814
+ .pipe(Effect.map((replies) => new Set(replies.map((reply) => reply.requestId))))
815
+ }
816
+
817
+ const lastRepliesById = <E>(
818
+ docs: ReadonlyArray<MessageDoc>,
819
+ activeRequestIds: ReadonlySet<string>,
820
+ queryReplies: (
821
+ query: string,
822
+ parameters: ReadonlyArray<CosmosParameter>
823
+ ) => Effect.Effect<Array<ReplyDoc>, E>
824
+ ) => {
825
+ const replyIds = Array.from(
826
+ new Set(docs
827
+ .flatMap((doc) => activeRequestIds.has(doc.requestId) || doc.lastReplyId === null ? [] : [doc.lastReplyId]))
828
+ )
829
+ if (!Arr.isArrayNonEmpty(replyIds)) return Effect.succeed(new Map<string, Reply.Encoded>())
830
+ return queryReplies(
831
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@replyIds, c.rowid)",
832
+ [{ name: "@replyIds", value: replyIds }]
833
+ )
834
+ .pipe(Effect.map((replies) => new Map(replies.map((reply) => [reply.rowid, replyFromDoc(reply)]))))
835
+ }
836
+
837
+ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: StorageOptions) {
838
+ const prefix = options?.prefix ?? "cluster-"
839
+ const container = yield* createContainer(prefix, options)().pipe(Effect.orDie)
840
+ const config = yield* ShardingConfig.ShardingConfig
841
+ const expires = Duration.toMillis(Duration.fromInputUnsafe(config.shardLockExpiration))
842
+ const containerId = `${prefix}cluster`
843
+ const annotate = (operation: string) =>
844
+ annotateDb({ operation, system: "cosmosdb", collection: containerId, entity: "cluster-runner-storage" })
845
+
846
+ const queryRunners = (query: string, parameters: ReadonlyArray<CosmosParameter>) =>
847
+ Effect
848
+ .tryPromise(() =>
849
+ container
850
+ .items
851
+ .query<RunnerDoc>({ query, parameters: Array.from(parameters) }, { partitionKey: "runner" })
852
+ .fetchAll()
853
+ )
854
+ .pipe(Effect.tap(annotateFeed), Effect.map((resp) => resp.resources))
855
+
856
+ const deleteRunner = (doc: RunnerDoc) =>
857
+ Effect
858
+ .tryPromise(() =>
859
+ container.item(doc.id, "runner").delete({
860
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
861
+ })
862
+ )
863
+ .pipe(
864
+ Effect.tap(annotateItem),
865
+ Effect.asVoid,
866
+ Effect.catchIf(isNotFound, () => Effect.void),
867
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
868
+ )
869
+
870
+ const readRunner = (address: string) =>
871
+ Effect.tryPromise(() => container.item(runnerDocId(address), "runner").read<RunnerDoc>()).pipe(
872
+ Effect.tap(annotateItem),
873
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
874
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
875
+ )
876
+
877
+ // Allocate a unique, monotonically increasing machine id via an atomic
878
+ // server-side `incr` on a single counter doc — mirrors SQL's auto-increment
879
+ // `machine_id` PK. A per-address string hash (the previous approach) could
880
+ // collide mod 1024 across distinct runners, yielding duplicate Snowflake ids.
881
+ // The counter doc is seeded at init below, so it always exists here.
882
+ const allocateMachineId = Effect
883
+ .tryPromise(() =>
884
+ container.item(machineIdCounterDocId, "runner").patch<MachineIdCounterDoc>([
885
+ { op: "incr", path: "/value", value: 1 }
886
+ ])
887
+ )
888
+ .pipe(Effect.tap(annotateItem), Effect.map((resp) => resp.resource!.value))
889
+
890
+ // Seed the counter once (create-if-missing); a lost create race is fine.
891
+ yield* Effect
892
+ .tryPromise(() =>
893
+ container.items.create<MachineIdCounterDoc>({
894
+ id: machineIdCounterDocId,
895
+ _partitionKey: "runner",
896
+ type: "machine-id-counter",
897
+ value: 0
898
+ })
899
+ )
900
+ .pipe(Effect.catchIf(isConflict, () => Effect.void), Effect.orDie)
901
+
902
+ const readLock = (shardId: string) =>
903
+ Effect.tryPromise(() => container.item(lockDocId(shardId), "lock").read<LockDoc>()).pipe(
904
+ Effect.tap(annotateItem),
905
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
906
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
907
+ )
908
+
909
+ const writeLock = (doc: LockDoc) =>
910
+ Effect
911
+ .tryPromise(() =>
912
+ container.item(doc.id, "lock").replace(doc, {
913
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
914
+ })
915
+ )
916
+ .pipe(
917
+ Effect.tap(annotateItem),
918
+ Effect.as(true),
919
+ Effect.catchIf(isPreconditionFailed, () => Effect.succeed(false))
920
+ )
921
+
922
+ const deleteLock = (doc: LockDoc) =>
923
+ Effect
924
+ .tryPromise(() =>
925
+ container.item(doc.id, "lock").delete({
926
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
927
+ })
928
+ )
929
+ .pipe(
930
+ Effect.tap(annotateItem),
931
+ Effect.asVoid,
932
+ Effect.catchIf(isNotFound, () => Effect.void),
933
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
934
+ )
935
+
936
+ const createLock = (address: string, shardId: string, now: number) =>
937
+ Effect
938
+ .tryPromise(() =>
939
+ container.items.create<LockDoc>({
940
+ id: lockDocId(shardId),
941
+ _partitionKey: "lock",
942
+ type: "lock",
943
+ shardId,
944
+ address,
945
+ acquiredAt: now
946
+ })
947
+ )
948
+ .pipe(
949
+ Effect.tap(annotateItem),
950
+ Effect.as(true),
951
+ Effect.catchIf(isConflict, () => Effect.succeed(false))
952
+ )
953
+
954
+ const tryAcquire = (address: string, shardId: string, now: number) =>
955
+ readLock(shardId).pipe(
956
+ Effect.flatMap((lock) =>
957
+ Option.match(lock, {
958
+ onNone: () => createLock(address, shardId, now),
959
+ onSome: (doc) => {
960
+ if (doc.address !== address && now - doc.acquiredAt <= expires) {
961
+ return Effect.succeed(false)
962
+ }
963
+ doc.address = address
964
+ doc.acquiredAt = now
965
+ return writeLock(doc)
966
+ }
967
+ })
968
+ ),
969
+ Effect.map((acquired) => acquired ? Option.some(shardId) : Option.none())
970
+ )
971
+
972
+ return RunnerStorage.makeEncoded({
973
+ getRunners: Effect.sync(() => Date.now()).pipe(
974
+ Effect.flatMap((now) =>
975
+ queryRunners("SELECT * FROM c WHERE c.type = 'runner' AND c.lastHeartbeat > @expiresAt", [
976
+ { name: "@expiresAt", value: now - expires }
977
+ ])
978
+ ),
979
+ Effect.map((docs) => docs.map((doc) => [doc.runner, doc.healthy] as const)),
980
+ annotate("getRunners"),
981
+ refailPersistence,
982
+ withTracerDisabled
983
+ ),
984
+
985
+ register: (address, runner, healthy) =>
986
+ Effect
987
+ .gen(function*() {
988
+ const now = Date.now()
989
+ // Reuse a previously-allocated machine id for this address (stable per
990
+ // address, like SQL's RETURNING on conflict); allocate a fresh unique
991
+ // one on first registration.
992
+ const existing = yield* readRunner(address)
993
+ const machineId = Option.isSome(existing) && existing.value.machineId !== undefined
994
+ ? existing.value.machineId
995
+ : yield* allocateMachineId
996
+ yield* Effect
997
+ .tryPromise(() =>
998
+ container.items.upsert<RunnerDoc>({
999
+ id: runnerDocId(address),
1000
+ _partitionKey: "runner",
1001
+ type: "runner",
1002
+ address,
1003
+ runner,
1004
+ healthy,
1005
+ lastHeartbeat: now,
1006
+ machineId
1007
+ })
1008
+ )
1009
+ .pipe(Effect.tap(annotateItem))
1010
+ return machineId
1011
+ })
1012
+ .pipe(annotate("register"), refailPersistence, withTracerDisabled),
1013
+
1014
+ unregister: (address) =>
1015
+ Effect.sync(() => Date.now()).pipe(
1016
+ Effect.flatMap((now) =>
1017
+ queryRunners(
1018
+ "SELECT * FROM c WHERE c.type = 'runner' AND (c.address = @address OR c.lastHeartbeat < @expiresAt)",
1019
+ [
1020
+ { name: "@address", value: address },
1021
+ { name: "@expiresAt", value: now - expires }
1022
+ ]
1023
+ )
1024
+ ),
1025
+ Effect.flatMap((docs) => Effect.forEach(docs, deleteRunner, { discard: true })),
1026
+ annotate("unregister"),
1027
+ refailPersistence,
1028
+ withTracerDisabled
1029
+ ),
1030
+
1031
+ setRunnerHealth: (address, healthy) =>
1032
+ Effect
1033
+ .tryPromise(() =>
1034
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1035
+ { op: "set", path: "/healthy", value: healthy }
1036
+ ])
1037
+ )
1038
+ .pipe(
1039
+ Effect.tap(annotateItem),
1040
+ Effect.asVoid,
1041
+ Effect.catchIf(isNotFound, () => Effect.void),
1042
+ annotate("setRunnerHealth"),
1043
+ refailPersistence,
1044
+ withTracerDisabled
1045
+ ),
1046
+
1047
+ acquire: (address, shardIds) =>
1048
+ Effect.sync(() => Date.now()).pipe(
1049
+ Effect.flatMap((now) => Effect.forEach(shardIds, (shardId) => tryAcquire(address, shardId, now))),
1050
+ Effect.map(Arr.getSomes),
1051
+ annotate("acquire"),
1052
+ refailPersistence,
1053
+ withTracerDisabled
1054
+ ),
1055
+
1056
+ refresh: (address, shardIds) =>
1057
+ Effect
1058
+ .gen(function*() {
1059
+ const now = Date.now()
1060
+ yield* Effect
1061
+ .tryPromise(() =>
1062
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1063
+ { op: "set", path: "/lastHeartbeat", value: now }
1064
+ ])
1065
+ )
1066
+ .pipe(
1067
+ Effect.tap(annotateItem),
1068
+ Effect.asVoid,
1069
+ Effect.catchIf(isNotFound, () => Effect.void)
1070
+ )
1071
+ const refreshed = yield* Effect.forEach(shardIds, (shardId) =>
1072
+ readLock(shardId).pipe(
1073
+ Effect.flatMap((lock) =>
1074
+ Option.match(lock, {
1075
+ onNone: () => Effect.succeed(Option.none<string>()),
1076
+ onSome: (doc) => {
1077
+ if (doc.address !== address) return Effect.succeed(Option.none<string>())
1078
+ doc.acquiredAt = now
1079
+ return writeLock(doc).pipe(Effect.map((ok) => ok ? Option.some(shardId) : Option.none<string>()))
1080
+ }
1081
+ })
1082
+ )
1083
+ ))
1084
+ return Arr.getSomes(refreshed)
1085
+ })
1086
+ .pipe(annotate("refresh"), refailPersistence, withTracerDisabled),
1087
+
1088
+ release: (address, shardId) =>
1089
+ readLock(shardId).pipe(
1090
+ Effect.flatMap((lock) =>
1091
+ Option.match(lock, {
1092
+ onNone: () => Effect.void,
1093
+ onSome: (doc) =>
1094
+ doc.address === address
1095
+ ? deleteLock(doc)
1096
+ : Effect.void
1097
+ })
1098
+ ),
1099
+ annotate("release"),
1100
+ refailPersistence,
1101
+ withTracerDisabled
1102
+ ),
1103
+
1104
+ releaseAll: (address) =>
1105
+ Effect
1106
+ .tryPromise(() =>
1107
+ container
1108
+ .items
1109
+ .query<LockDoc>({
1110
+ query: "SELECT * FROM c WHERE c.type = 'lock' AND c.address = @address",
1111
+ parameters: [{ name: "@address", value: address }]
1112
+ }, { partitionKey: "lock" })
1113
+ .fetchAll()
1114
+ )
1115
+ .pipe(
1116
+ Effect.tap(annotateFeed),
1117
+ Effect.flatMap((resp) => Effect.forEach(resp.resources, deleteLock, { discard: true })),
1118
+ annotate("releaseAll"),
1119
+ refailPersistence,
1120
+ withTracerDisabled
1121
+ )
1122
+ })
1123
+ })
1124
+
1125
+ export const layerMessageStorage = (
1126
+ options?: StorageOptions
1127
+ ): Layer.Layer<MessageStorage.MessageStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1128
+ Layer.effect(MessageStorage.MessageStorage, makeMessageStorage(options)).pipe(
1129
+ Layer.provide(Snowflake.layerGenerator)
1130
+ )
1131
+
1132
+ export const layerRunnerStorage = (
1133
+ options?: StorageOptions
1134
+ ): Layer.Layer<RunnerStorage.RunnerStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1135
+ Layer.effect(RunnerStorage.RunnerStorage, makeRunnerStorage(options))
1136
+
1137
+ export const layerStorage = (options?: StorageOptions): Layer.Layer<
1138
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1139
+ never,
1140
+ CosmosClient | ShardingConfig.ShardingConfig
1141
+ > => Layer.merge(layerMessageStorage(options), layerRunnerStorage(options))
1142
+
1143
+ export const layerCosmos = (config: ClusterCosmosConfig): Layer.Layer<
1144
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1145
+ never,
1146
+ ShardingConfig.ShardingConfig
1147
+ > =>
1148
+ layerStorage({ prefix: config.prefix, autoscaleMaxThroughput: config.autoscaleMaxThroughput }).pipe(
1149
+ Layer.provide(CosmosClientLayer(Redacted.value(config.url), config.dbName))
1150
+ )