@effect-app/infra 4.0.0-beta.32 → 4.0.0-beta.321

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 (480) hide show
  1. package/CHANGELOG.md +2469 -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 +70 -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 +392 -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 +87 -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 +346 -76
  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 +257 -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 +21 -0
  51. package/dist/Store/SQL.d.ts.map +1 -0
  52. package/dist/Store/SQL.js +478 -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 +13 -0
  60. package/dist/Store/jsonDocument.d.ts.map +1 -0
  61. package/dist/Store/jsonDocument.js +31 -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 +29 -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 +16 -4
  74. package/dist/codec.d.ts.map +1 -0
  75. package/dist/codec.js +5 -0
  76. package/dist/{adapters/cosmos-client.d.ts → cosmos-client.d.ts} +4 -2
  77. package/dist/cosmos-client.d.ts.map +1 -0
  78. package/dist/cosmos-client.js +11 -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 +49 -41
  157. package/package.json +38 -352
  158. package/src/CUPS.ts +54 -15
  159. package/src/ClusterCosmos.ts +1154 -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 +537 -367
  170. package/src/Store/Disk.ts +137 -74
  171. package/src/Store/Memory.ts +402 -113
  172. package/src/Store/SQL/Pg.ts +402 -0
  173. package/src/Store/SQL/query.ts +748 -0
  174. package/src/Store/SQL.ts +786 -0
  175. package/src/Store/codeFilter.ts +78 -28
  176. package/src/Store/index.ts +25 -10
  177. package/src/Store/jsonDocument.ts +43 -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 +20 -5
  182. package/src/{adapters/cosmos-client.ts → cosmos-client.ts} +5 -3
  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/auth.test.ts +101 -0
  208. package/test/cluster-cosmos.test.ts +612 -0
  209. package/test/cluster-servicebus.test.ts +180 -0
  210. package/test/cluster-sqlite.test.ts +207 -0
  211. package/test/contextProvider.test.ts +15 -12
  212. package/test/controller.test.ts +28 -32
  213. package/test/cosmos-query.test.ts +323 -0
  214. package/test/dist/auth.test.d.ts.map +1 -0
  215. package/test/dist/cluster-cosmos.test.d.ts.map +1 -0
  216. package/test/dist/cluster-servicebus.test.d.ts.map +1 -0
  217. package/test/dist/cluster-sqlite.test.d.ts.map +1 -0
  218. package/test/dist/contextProvider.test.d.ts.map +1 -1
  219. package/test/dist/controller.test.d.ts.map +1 -1
  220. package/test/dist/cosmos-query.test.d.ts.map +1 -0
  221. package/test/dist/fixtures.d.ts +29 -11
  222. package/test/dist/fixtures.d.ts.map +1 -1
  223. package/test/dist/fixtures.js +17 -10
  224. package/test/dist/json-lower.test.d.ts.map +1 -0
  225. package/test/dist/query.test.d.ts.map +1 -1
  226. package/test/dist/rawQuery.test.d.ts.map +1 -1
  227. package/test/dist/repository-ext.test.d.ts.map +1 -0
  228. package/test/dist/requires.test.d.ts.map +1 -1
  229. package/test/dist/router-generator.test.d.ts.map +1 -0
  230. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  231. package/test/dist/rpc-context-map-streaming.test.d.ts.map +1 -0
  232. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  233. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  234. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  235. package/test/dist/sql-store.test.d.ts.map +1 -0
  236. package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
  237. package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
  238. package/test/fixtures.ts +16 -9
  239. package/test/json-lower.test.ts +115 -0
  240. package/test/layerUtils.test.ts +2 -2
  241. package/test/query.test.ts +1384 -46
  242. package/test/rawQuery.test.ts +340 -22
  243. package/test/repository-ext.test.ts +456 -0
  244. package/test/requires.test.ts +10 -5
  245. package/test/router-generator.test.ts +187 -0
  246. package/test/routing-interruptibility.test.ts +66 -0
  247. package/test/rpc-context-map-streaming.test.ts +265 -0
  248. package/test/rpc-e2e-invalidation.test.ts +421 -0
  249. package/test/rpc-multi-middleware.test.ts +85 -10
  250. package/test/rpc-stream-fullstack.test.ts +307 -0
  251. package/test/sql-store.test.ts +1905 -0
  252. package/test/validateSample.test.ts +26 -14
  253. package/test/workflow-engine-cosmos.test.ts +345 -0
  254. package/test/workflow-engine-sqlite.test.ts +297 -0
  255. package/tsconfig.examples.json +1 -1
  256. package/tsconfig.json +9 -2
  257. package/tsconfig.src.json +35 -35
  258. package/tsconfig.test.json +2 -2
  259. package/dist/Emailer/service.d.ts +0 -55
  260. package/dist/Emailer/service.d.ts.map +0 -1
  261. package/dist/Emailer/service.js +0 -6
  262. package/dist/Emailer.d.ts +0 -2
  263. package/dist/Emailer.d.ts.map +0 -1
  264. package/dist/Emailer.js +0 -2
  265. package/dist/Model/Repository/ext.d.ts +0 -41
  266. package/dist/Model/Repository/ext.d.ts.map +0 -1
  267. package/dist/Model/Repository/ext.js +0 -65
  268. package/dist/Model/Repository/internal/internal.d.ts +0 -59
  269. package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
  270. package/dist/Model/Repository/internal/internal.js +0 -316
  271. package/dist/Model/Repository/legacy.d.ts +0 -19
  272. package/dist/Model/Repository/legacy.d.ts.map +0 -1
  273. package/dist/Model/Repository/legacy.js +0 -2
  274. package/dist/Model/Repository/makeRepo.d.ts +0 -49
  275. package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
  276. package/dist/Model/Repository/makeRepo.js +0 -24
  277. package/dist/Model/Repository/service.d.ts +0 -89
  278. package/dist/Model/Repository/service.d.ts.map +0 -1
  279. package/dist/Model/Repository/service.js +0 -2
  280. package/dist/Model/Repository/validation.d.ts +0 -42
  281. package/dist/Model/Repository/validation.d.ts.map +0 -1
  282. package/dist/Model/Repository/validation.js +0 -32
  283. package/dist/Model/Repository.d.ts +0 -6
  284. package/dist/Model/Repository.d.ts.map +0 -1
  285. package/dist/Model/Repository.js +0 -6
  286. package/dist/Model/dsl.d.ts +0 -32
  287. package/dist/Model/dsl.d.ts.map +0 -1
  288. package/dist/Model/dsl.js +0 -44
  289. package/dist/Model/filter/filterApi.d.ts +0 -30
  290. package/dist/Model/filter/filterApi.d.ts.map +0 -1
  291. package/dist/Model/filter/filterApi.js +0 -2
  292. package/dist/Model/filter/types/errors.d.ts +0 -29
  293. package/dist/Model/filter/types/errors.d.ts.map +0 -1
  294. package/dist/Model/filter/types/errors.js +0 -2
  295. package/dist/Model/filter/types/fields.d.ts +0 -15
  296. package/dist/Model/filter/types/fields.d.ts.map +0 -1
  297. package/dist/Model/filter/types/fields.js +0 -2
  298. package/dist/Model/filter/types/path/common.d.ts +0 -316
  299. package/dist/Model/filter/types/path/common.d.ts.map +0 -1
  300. package/dist/Model/filter/types/path/common.js +0 -2
  301. package/dist/Model/filter/types/path/eager.d.ts +0 -95
  302. package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
  303. package/dist/Model/filter/types/path/eager.js +0 -31
  304. package/dist/Model/filter/types/path/index.d.ts +0 -4
  305. package/dist/Model/filter/types/path/index.d.ts.map +0 -1
  306. package/dist/Model/filter/types/path/index.js +0 -3
  307. package/dist/Model/filter/types/utils.d.ts +0 -79
  308. package/dist/Model/filter/types/utils.d.ts.map +0 -1
  309. package/dist/Model/filter/types/utils.js +0 -2
  310. package/dist/Model/filter/types/validator.d.ts +0 -30
  311. package/dist/Model/filter/types/validator.d.ts.map +0 -1
  312. package/dist/Model/filter/types/validator.js +0 -2
  313. package/dist/Model/filter/types.d.ts +0 -5
  314. package/dist/Model/filter/types.d.ts.map +0 -1
  315. package/dist/Model/filter/types.js +0 -7
  316. package/dist/Model/query/dsl.d.ts +0 -248
  317. package/dist/Model/query/dsl.d.ts.map +0 -1
  318. package/dist/Model/query/dsl.js +0 -104
  319. package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
  320. package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
  321. package/dist/Model/query/new-kid-interpreter.js +0 -165
  322. package/dist/Model/query.d.ts +0 -15
  323. package/dist/Model/query.d.ts.map +0 -1
  324. package/dist/Model/query.js +0 -3
  325. package/dist/Model.d.ts +0 -4
  326. package/dist/Model.d.ts.map +0 -1
  327. package/dist/Model.js +0 -4
  328. package/dist/Operations.d.ts +0 -55
  329. package/dist/Operations.d.ts.map +0 -1
  330. package/dist/Operations.js +0 -102
  331. package/dist/OperationsRepo.d.ts +0 -41
  332. package/dist/OperationsRepo.d.ts.map +0 -1
  333. package/dist/OperationsRepo.js +0 -14
  334. package/dist/QueueMaker/SQLQueue.d.ts +0 -16
  335. package/dist/QueueMaker/SQLQueue.d.ts.map +0 -1
  336. package/dist/QueueMaker/SQLQueue.js +0 -130
  337. package/dist/QueueMaker/memQueue.d.ts +0 -11
  338. package/dist/QueueMaker/memQueue.d.ts.map +0 -1
  339. package/dist/QueueMaker/memQueue.js +0 -76
  340. package/dist/QueueMaker/sbqueue.d.ts +0 -12
  341. package/dist/QueueMaker/sbqueue.d.ts.map +0 -1
  342. package/dist/QueueMaker/sbqueue.js +0 -91
  343. package/dist/QueueMaker/service.d.ts +0 -11
  344. package/dist/QueueMaker/service.d.ts.map +0 -1
  345. package/dist/QueueMaker/service.js +0 -4
  346. package/dist/RequestContext.d.ts +0 -63
  347. package/dist/RequestContext.d.ts.map +0 -1
  348. package/dist/RequestContext.js +0 -49
  349. package/dist/RequestFiberSet.d.ts +0 -36
  350. package/dist/RequestFiberSet.d.ts.map +0 -1
  351. package/dist/RequestFiberSet.js +0 -78
  352. package/dist/Store/ContextMapContainer.d.ts +0 -14
  353. package/dist/Store/ContextMapContainer.d.ts.map +0 -1
  354. package/dist/Store/ContextMapContainer.js +0 -16
  355. package/dist/Store/service.d.ts +0 -108
  356. package/dist/Store/service.d.ts.map +0 -1
  357. package/dist/Store/service.js +0 -71
  358. package/dist/Store.d.ts +0 -2
  359. package/dist/Store.d.ts.map +0 -1
  360. package/dist/Store.js +0 -2
  361. package/dist/adapters/SQL/Model.d.ts.map +0 -1
  362. package/dist/adapters/SQL/Model.js +0 -478
  363. package/dist/adapters/SQL.d.ts.map +0 -1
  364. package/dist/adapters/ServiceBus.d.ts +0 -58
  365. package/dist/adapters/ServiceBus.d.ts.map +0 -1
  366. package/dist/adapters/ServiceBus.js +0 -99
  367. package/dist/adapters/cosmos-client.d.ts.map +0 -1
  368. package/dist/adapters/cosmos-client.js +0 -9
  369. package/dist/adapters/index.d.ts +0 -2
  370. package/dist/adapters/index.d.ts.map +0 -1
  371. package/dist/adapters/index.js +0 -2
  372. package/dist/adapters/logger.d.ts +0 -9
  373. package/dist/adapters/logger.d.ts.map +0 -1
  374. package/dist/adapters/logger.js +0 -3
  375. package/dist/adapters/memQueue.d.ts +0 -13
  376. package/dist/adapters/memQueue.d.ts.map +0 -1
  377. package/dist/adapters/memQueue.js +0 -20
  378. package/dist/adapters/mongo-client.d.ts.map +0 -1
  379. package/dist/adapters/mongo-client.js +0 -13
  380. package/dist/adapters/redis-client.d.ts.map +0 -1
  381. package/dist/adapters/redis-client.js +0 -94
  382. package/dist/api/ContextProvider.d.ts +0 -31
  383. package/dist/api/ContextProvider.d.ts.map +0 -1
  384. package/dist/api/ContextProvider.js +0 -38
  385. package/dist/api/codec.d.ts.map +0 -1
  386. package/dist/api/codec.js +0 -5
  387. package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
  388. package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
  389. package/dist/api/internal/RequestContextMiddleware.js +0 -35
  390. package/dist/api/internal/auth.d.ts +0 -15
  391. package/dist/api/internal/auth.d.ts.map +0 -1
  392. package/dist/api/internal/auth.js +0 -47
  393. package/dist/api/internal/events.d.ts +0 -9
  394. package/dist/api/internal/events.d.ts.map +0 -1
  395. package/dist/api/internal/events.js +0 -42
  396. package/dist/api/internal/health.d.ts.map +0 -1
  397. package/dist/api/internal/health.js +0 -5
  398. package/dist/api/layerUtils.d.ts.map +0 -1
  399. package/dist/api/layerUtils.js +0 -16
  400. package/dist/api/middlewares.d.ts.map +0 -1
  401. package/dist/api/reportError.d.ts.map +0 -1
  402. package/dist/api/reportError.js +0 -27
  403. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
  404. package/dist/api/routing/middleware/middleware.d.ts +0 -9
  405. package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
  406. package/dist/api/routing/middleware/middleware.js +0 -92
  407. package/dist/api/routing/middleware.d.ts.map +0 -1
  408. package/dist/api/routing/schema/jwt.d.ts.map +0 -1
  409. package/dist/api/routing/schema/jwt.js +0 -12
  410. package/dist/api/routing/tsort.d.ts.map +0 -1
  411. package/dist/api/routing/tsort.js +0 -51
  412. package/dist/api/routing/utils.d.ts.map +0 -1
  413. package/dist/api/routing/utils.js +0 -44
  414. package/dist/api/routing.d.ts +0 -138
  415. package/dist/api/routing.d.ts.map +0 -1
  416. package/dist/api/routing.js +0 -166
  417. package/dist/api/setupRequest.d.ts +0 -12
  418. package/dist/api/setupRequest.d.ts.map +0 -1
  419. package/dist/api/setupRequest.js +0 -44
  420. package/dist/api/util.d.ts.map +0 -1
  421. package/dist/api/util.js +0 -14
  422. package/eslint.config.mjs +0 -24
  423. package/src/Emailer/service.ts +0 -52
  424. package/src/Emailer.ts +0 -1
  425. package/src/Model/Repository/ext.ts +0 -283
  426. package/src/Model/Repository/internal/internal.ts +0 -577
  427. package/src/Model/Repository/legacy.ts +0 -27
  428. package/src/Model/Repository/makeRepo.ts +0 -139
  429. package/src/Model/Repository/service.ts +0 -627
  430. package/src/Model/Repository/validation.ts +0 -31
  431. package/src/Model/Repository.ts +0 -5
  432. package/src/Model/dsl.ts +0 -128
  433. package/src/Model/filter/filterApi.ts +0 -60
  434. package/src/Model/filter/types/errors.ts +0 -47
  435. package/src/Model/filter/types/fields.ts +0 -50
  436. package/src/Model/filter/types/path/common.ts +0 -404
  437. package/src/Model/filter/types/path/eager.ts +0 -298
  438. package/src/Model/filter/types/path/index.ts +0 -4
  439. package/src/Model/filter/types/utils.ts +0 -128
  440. package/src/Model/filter/types/validator.ts +0 -46
  441. package/src/Model/filter/types.ts +0 -6
  442. package/src/Model/query/dsl.ts +0 -2110
  443. package/src/Model/query/new-kid-interpreter.ts +0 -210
  444. package/src/Model/query.ts +0 -13
  445. package/src/Model.ts +0 -3
  446. package/src/Operations.ts +0 -235
  447. package/src/OperationsRepo.ts +0 -16
  448. package/src/QueueMaker/SQLQueue.ts +0 -189
  449. package/src/QueueMaker/memQueue.ts +0 -130
  450. package/src/QueueMaker/sbqueue.ts +0 -137
  451. package/src/QueueMaker/service.ts +0 -17
  452. package/src/RequestContext.ts +0 -63
  453. package/src/RequestFiberSet.ts +0 -105
  454. package/src/Store/ContextMapContainer.ts +0 -20
  455. package/src/Store/index.test.ts.bak +0 -112
  456. package/src/Store/service.ts +0 -184
  457. package/src/Store.ts +0 -1
  458. package/src/adapters/SQL.ts +0 -1
  459. package/src/adapters/ServiceBus.ts +0 -209
  460. package/src/adapters/index.ts +0 -0
  461. package/src/adapters/logger.ts +0 -3
  462. package/src/adapters/memQueue.ts +0 -21
  463. package/src/api/internal/auth.ts +0 -68
  464. package/src/api/middlewares.ts +0 -10
  465. package/src/api/routing/middleware/middleware.ts +0 -128
  466. package/src/api/routing/middleware.ts +0 -6
  467. package/src/api/routing.ts +0 -598
  468. package/src/api/setupRequest.ts +0 -84
  469. package/tsconfig.json.bak +0 -12
  470. /package/dist/{adapters/SQL.d.ts → SQL.d.ts} +0 -0
  471. /package/dist/{api/codec.d.ts → codec.d.ts} +0 -0
  472. /package/dist/{api/internal → internal}/health.d.ts +0 -0
  473. /package/dist/{api/middlewares.d.ts → middlewares.d.ts} +0 -0
  474. /package/dist/{api/routing → routing}/schema/jwt.d.ts +0 -0
  475. /package/dist/{api/routing → routing}/tsort.d.ts +0 -0
  476. /package/dist/{api/util.d.ts → util.d.ts} +0 -0
  477. /package/src/{api/codec.ts → codec.ts} +0 -0
  478. /package/src/{api/internal → internal}/health.ts +0 -0
  479. /package/src/{api/routing → routing}/tsort.ts +0 -0
  480. /package/src/{api/util.ts → util.ts} +0 -0
@@ -0,0 +1,1154 @@
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, 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?: {
838
+ readonly prefix?: string | undefined
839
+ }) {
840
+ const prefix = options?.prefix ?? "cluster-"
841
+ const container = yield* createContainer(prefix)().pipe(Effect.orDie)
842
+ const config = yield* ShardingConfig.ShardingConfig
843
+ const expires = Duration.toMillis(Duration.fromInputUnsafe(config.shardLockExpiration))
844
+ const containerId = `${prefix}cluster`
845
+ const annotate = (operation: string) =>
846
+ annotateDb({ operation, system: "cosmosdb", collection: containerId, entity: "cluster-runner-storage" })
847
+
848
+ const queryRunners = (query: string, parameters: ReadonlyArray<CosmosParameter>) =>
849
+ Effect
850
+ .tryPromise(() =>
851
+ container
852
+ .items
853
+ .query<RunnerDoc>({ query, parameters: Array.from(parameters) }, { partitionKey: "runner" })
854
+ .fetchAll()
855
+ )
856
+ .pipe(Effect.tap(annotateFeed), Effect.map((resp) => resp.resources))
857
+
858
+ const deleteRunner = (doc: RunnerDoc) =>
859
+ Effect
860
+ .tryPromise(() =>
861
+ container.item(doc.id, "runner").delete({
862
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
863
+ })
864
+ )
865
+ .pipe(
866
+ Effect.tap(annotateItem),
867
+ Effect.asVoid,
868
+ Effect.catchIf(isNotFound, () => Effect.void),
869
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
870
+ )
871
+
872
+ const readRunner = (address: string) =>
873
+ Effect.tryPromise(() => container.item(runnerDocId(address), "runner").read<RunnerDoc>()).pipe(
874
+ Effect.tap(annotateItem),
875
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
876
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
877
+ )
878
+
879
+ // Allocate a unique, monotonically increasing machine id via an atomic
880
+ // server-side `incr` on a single counter doc — mirrors SQL's auto-increment
881
+ // `machine_id` PK. A per-address string hash (the previous approach) could
882
+ // collide mod 1024 across distinct runners, yielding duplicate Snowflake ids.
883
+ // The counter doc is seeded at init below, so it always exists here.
884
+ const allocateMachineId = Effect
885
+ .tryPromise(() =>
886
+ container.item(machineIdCounterDocId, "runner").patch<MachineIdCounterDoc>([
887
+ { op: "incr", path: "/value", value: 1 }
888
+ ])
889
+ )
890
+ .pipe(Effect.tap(annotateItem), Effect.map((resp) => resp.resource!.value))
891
+
892
+ // Seed the counter once (create-if-missing); a lost create race is fine.
893
+ yield* Effect
894
+ .tryPromise(() =>
895
+ container.items.create<MachineIdCounterDoc>({
896
+ id: machineIdCounterDocId,
897
+ _partitionKey: "runner",
898
+ type: "machine-id-counter",
899
+ value: 0
900
+ })
901
+ )
902
+ .pipe(Effect.catchIf(isConflict, () => Effect.void), Effect.orDie)
903
+
904
+ const readLock = (shardId: string) =>
905
+ Effect.tryPromise(() => container.item(lockDocId(shardId), "lock").read<LockDoc>()).pipe(
906
+ Effect.tap(annotateItem),
907
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
908
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
909
+ )
910
+
911
+ const writeLock = (doc: LockDoc) =>
912
+ Effect
913
+ .tryPromise(() =>
914
+ container.item(doc.id, "lock").replace(doc, {
915
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
916
+ })
917
+ )
918
+ .pipe(
919
+ Effect.tap(annotateItem),
920
+ Effect.as(true),
921
+ Effect.catchIf(isPreconditionFailed, () => Effect.succeed(false))
922
+ )
923
+
924
+ const deleteLock = (doc: LockDoc) =>
925
+ Effect
926
+ .tryPromise(() =>
927
+ container.item(doc.id, "lock").delete({
928
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
929
+ })
930
+ )
931
+ .pipe(
932
+ Effect.tap(annotateItem),
933
+ Effect.asVoid,
934
+ Effect.catchIf(isNotFound, () => Effect.void),
935
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
936
+ )
937
+
938
+ const createLock = (address: string, shardId: string, now: number) =>
939
+ Effect
940
+ .tryPromise(() =>
941
+ container.items.create<LockDoc>({
942
+ id: lockDocId(shardId),
943
+ _partitionKey: "lock",
944
+ type: "lock",
945
+ shardId,
946
+ address,
947
+ acquiredAt: now
948
+ })
949
+ )
950
+ .pipe(
951
+ Effect.tap(annotateItem),
952
+ Effect.as(true),
953
+ Effect.catchIf(isConflict, () => Effect.succeed(false))
954
+ )
955
+
956
+ const tryAcquire = (address: string, shardId: string, now: number) =>
957
+ readLock(shardId).pipe(
958
+ Effect.flatMap((lock) =>
959
+ Option.match(lock, {
960
+ onNone: () => createLock(address, shardId, now),
961
+ onSome: (doc) => {
962
+ if (doc.address !== address && now - doc.acquiredAt <= expires) {
963
+ return Effect.succeed(false)
964
+ }
965
+ doc.address = address
966
+ doc.acquiredAt = now
967
+ return writeLock(doc)
968
+ }
969
+ })
970
+ ),
971
+ Effect.map((acquired) => acquired ? Option.some(shardId) : Option.none())
972
+ )
973
+
974
+ return RunnerStorage.makeEncoded({
975
+ getRunners: Effect.sync(() => Date.now()).pipe(
976
+ Effect.flatMap((now) =>
977
+ queryRunners("SELECT * FROM c WHERE c.type = 'runner' AND c.lastHeartbeat > @expiresAt", [
978
+ { name: "@expiresAt", value: now - expires }
979
+ ])
980
+ ),
981
+ Effect.map((docs) => docs.map((doc) => [doc.runner, doc.healthy] as const)),
982
+ annotate("getRunners"),
983
+ refailPersistence,
984
+ withTracerDisabled
985
+ ),
986
+
987
+ register: (address, runner, healthy) =>
988
+ Effect
989
+ .gen(function*() {
990
+ const now = Date.now()
991
+ // Reuse a previously-allocated machine id for this address (stable per
992
+ // address, like SQL's RETURNING on conflict); allocate a fresh unique
993
+ // one on first registration.
994
+ const existing = yield* readRunner(address)
995
+ const machineId = Option.isSome(existing) && existing.value.machineId !== undefined
996
+ ? existing.value.machineId
997
+ : yield* allocateMachineId
998
+ yield* Effect
999
+ .tryPromise(() =>
1000
+ container.items.upsert<RunnerDoc>({
1001
+ id: runnerDocId(address),
1002
+ _partitionKey: "runner",
1003
+ type: "runner",
1004
+ address,
1005
+ runner,
1006
+ healthy,
1007
+ lastHeartbeat: now,
1008
+ machineId
1009
+ })
1010
+ )
1011
+ .pipe(Effect.tap(annotateItem))
1012
+ return machineId
1013
+ })
1014
+ .pipe(annotate("register"), refailPersistence, withTracerDisabled),
1015
+
1016
+ unregister: (address) =>
1017
+ Effect.sync(() => Date.now()).pipe(
1018
+ Effect.flatMap((now) =>
1019
+ queryRunners(
1020
+ "SELECT * FROM c WHERE c.type = 'runner' AND (c.address = @address OR c.lastHeartbeat < @expiresAt)",
1021
+ [
1022
+ { name: "@address", value: address },
1023
+ { name: "@expiresAt", value: now - expires }
1024
+ ]
1025
+ )
1026
+ ),
1027
+ Effect.flatMap((docs) => Effect.forEach(docs, deleteRunner, { discard: true })),
1028
+ annotate("unregister"),
1029
+ refailPersistence,
1030
+ withTracerDisabled
1031
+ ),
1032
+
1033
+ setRunnerHealth: (address, healthy) =>
1034
+ Effect
1035
+ .tryPromise(() =>
1036
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1037
+ { op: "set", path: "/healthy", value: healthy }
1038
+ ])
1039
+ )
1040
+ .pipe(
1041
+ Effect.tap(annotateItem),
1042
+ Effect.asVoid,
1043
+ Effect.catchIf(isNotFound, () => Effect.void),
1044
+ annotate("setRunnerHealth"),
1045
+ refailPersistence,
1046
+ withTracerDisabled
1047
+ ),
1048
+
1049
+ acquire: (address, shardIds) =>
1050
+ Effect.sync(() => Date.now()).pipe(
1051
+ Effect.flatMap((now) => Effect.forEach(shardIds, (shardId) => tryAcquire(address, shardId, now))),
1052
+ Effect.map(Arr.getSomes),
1053
+ annotate("acquire"),
1054
+ refailPersistence,
1055
+ withTracerDisabled
1056
+ ),
1057
+
1058
+ refresh: (address, shardIds) =>
1059
+ Effect
1060
+ .gen(function*() {
1061
+ const now = Date.now()
1062
+ yield* Effect
1063
+ .tryPromise(() =>
1064
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1065
+ { op: "set", path: "/lastHeartbeat", value: now }
1066
+ ])
1067
+ )
1068
+ .pipe(
1069
+ Effect.tap(annotateItem),
1070
+ Effect.asVoid,
1071
+ Effect.catchIf(isNotFound, () => Effect.void)
1072
+ )
1073
+ const refreshed = yield* Effect.forEach(shardIds, (shardId) =>
1074
+ readLock(shardId).pipe(
1075
+ Effect.flatMap((lock) =>
1076
+ Option.match(lock, {
1077
+ onNone: () => Effect.succeed(Option.none<string>()),
1078
+ onSome: (doc) => {
1079
+ if (doc.address !== address) return Effect.succeed(Option.none<string>())
1080
+ doc.acquiredAt = now
1081
+ return writeLock(doc).pipe(Effect.map((ok) => ok ? Option.some(shardId) : Option.none<string>()))
1082
+ }
1083
+ })
1084
+ )
1085
+ ))
1086
+ return Arr.getSomes(refreshed)
1087
+ })
1088
+ .pipe(annotate("refresh"), refailPersistence, withTracerDisabled),
1089
+
1090
+ release: (address, shardId) =>
1091
+ readLock(shardId).pipe(
1092
+ Effect.flatMap((lock) =>
1093
+ Option.match(lock, {
1094
+ onNone: () => Effect.void,
1095
+ onSome: (doc) =>
1096
+ doc.address === address
1097
+ ? deleteLock(doc)
1098
+ : Effect.void
1099
+ })
1100
+ ),
1101
+ annotate("release"),
1102
+ refailPersistence,
1103
+ withTracerDisabled
1104
+ ),
1105
+
1106
+ releaseAll: (address) =>
1107
+ Effect
1108
+ .tryPromise(() =>
1109
+ container
1110
+ .items
1111
+ .query<LockDoc>({
1112
+ query: "SELECT * FROM c WHERE c.type = 'lock' AND c.address = @address",
1113
+ parameters: [{ name: "@address", value: address }]
1114
+ }, { partitionKey: "lock" })
1115
+ .fetchAll()
1116
+ )
1117
+ .pipe(
1118
+ Effect.tap(annotateFeed),
1119
+ Effect.flatMap((resp) => Effect.forEach(resp.resources, deleteLock, { discard: true })),
1120
+ annotate("releaseAll"),
1121
+ refailPersistence,
1122
+ withTracerDisabled
1123
+ )
1124
+ })
1125
+ })
1126
+
1127
+ export const layerMessageStorage = (options?: {
1128
+ readonly prefix?: string | undefined
1129
+ }): Layer.Layer<MessageStorage.MessageStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1130
+ Layer.effect(MessageStorage.MessageStorage, makeMessageStorage(options)).pipe(
1131
+ Layer.provide(Snowflake.layerGenerator)
1132
+ )
1133
+
1134
+ export const layerRunnerStorage = (options?: {
1135
+ readonly prefix?: string | undefined
1136
+ }): Layer.Layer<RunnerStorage.RunnerStorage, never, CosmosClient | ShardingConfig.ShardingConfig> =>
1137
+ Layer.effect(RunnerStorage.RunnerStorage, makeRunnerStorage(options))
1138
+
1139
+ export const layerStorage = (options?: {
1140
+ readonly prefix?: string | undefined
1141
+ }): Layer.Layer<
1142
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1143
+ never,
1144
+ CosmosClient | ShardingConfig.ShardingConfig
1145
+ > => Layer.merge(layerMessageStorage(options), layerRunnerStorage(options))
1146
+
1147
+ export const layerCosmos = (config: ClusterCosmosConfig): Layer.Layer<
1148
+ MessageStorage.MessageStorage | RunnerStorage.RunnerStorage,
1149
+ never,
1150
+ ShardingConfig.ShardingConfig
1151
+ > =>
1152
+ layerStorage({ prefix: config.prefix }).pipe(
1153
+ Layer.provide(CosmosClientLayer(Redacted.value(config.url), config.dbName))
1154
+ )