effect-app 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 (369) hide show
  1. package/CHANGELOG.md +1570 -0
  2. package/dist/Array.d.ts +2 -1
  3. package/dist/Array.d.ts.map +1 -1
  4. package/dist/Array.js +4 -4
  5. package/dist/Chunk.d.ts.map +1 -1
  6. package/dist/Config/SecretURL.d.ts +3 -1
  7. package/dist/Config/SecretURL.d.ts.map +1 -1
  8. package/dist/Config/SecretURL.js +3 -6
  9. package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
  10. package/dist/Config/internal/configSecretURL.js +2 -2
  11. package/dist/Config.d.ts +2 -0
  12. package/dist/Config.d.ts.map +1 -0
  13. package/dist/Config.js +8 -0
  14. package/dist/ConfigProvider.d.ts +2 -0
  15. package/dist/ConfigProvider.d.ts.map +1 -0
  16. package/dist/ConfigProvider.js +6 -0
  17. package/dist/{ServiceMap.d.ts → Context.d.ts} +18 -20
  18. package/dist/Context.d.ts.map +1 -0
  19. package/dist/Context.js +67 -0
  20. package/dist/DataDependencies.d.ts +136 -0
  21. package/dist/DataDependencies.d.ts.map +1 -0
  22. package/dist/DataDependencies.js +112 -0
  23. package/dist/Effect.d.ts +10 -9
  24. package/dist/Effect.d.ts.map +1 -1
  25. package/dist/Effect.js +6 -9
  26. package/dist/Emailer.d.ts +51 -0
  27. package/dist/Emailer.d.ts.map +1 -0
  28. package/dist/Emailer.js +7 -0
  29. package/dist/Function.d.ts.map +1 -1
  30. package/dist/Layer.d.ts +10 -6
  31. package/dist/Layer.d.ts.map +1 -1
  32. package/dist/Layer.js +3 -2
  33. package/dist/Model/Repository/Registry.d.ts +22 -0
  34. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  35. package/dist/Model/Repository/Registry.js +18 -0
  36. package/dist/Model/Repository/dependency.d.ts +6 -0
  37. package/dist/Model/Repository/dependency.d.ts.map +1 -0
  38. package/dist/Model/Repository/dependency.js +29 -0
  39. package/dist/Model/Repository/ext.d.ts +60 -0
  40. package/dist/Model/Repository/ext.d.ts.map +1 -0
  41. package/dist/Model/Repository/ext.js +124 -0
  42. package/dist/Model/Repository/internal/internal.d.ts +68 -0
  43. package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
  44. package/dist/Model/Repository/internal/internal.js +579 -0
  45. package/dist/Model/Repository/legacy.d.ts +21 -0
  46. package/dist/Model/Repository/legacy.d.ts.map +1 -0
  47. package/dist/Model/Repository/legacy.js +2 -0
  48. package/dist/Model/Repository/makeRepo.d.ts +63 -0
  49. package/dist/Model/Repository/makeRepo.d.ts.map +1 -0
  50. package/dist/Model/Repository/makeRepo.js +27 -0
  51. package/dist/Model/Repository/service.d.ts +121 -0
  52. package/dist/Model/Repository/service.d.ts.map +1 -0
  53. package/dist/Model/Repository/service.js +2 -0
  54. package/dist/Model/Repository/validation.d.ts +58 -0
  55. package/dist/Model/Repository/validation.d.ts.map +1 -0
  56. package/dist/Model/Repository/validation.js +32 -0
  57. package/dist/Model/Repository.d.ts +8 -0
  58. package/dist/Model/Repository.d.ts.map +1 -0
  59. package/dist/Model/Repository.js +8 -0
  60. package/dist/Model/dsl.d.ts +33 -0
  61. package/dist/Model/dsl.d.ts.map +1 -0
  62. package/dist/Model/dsl.js +43 -0
  63. package/dist/Model/filter/filterApi.d.ts +30 -0
  64. package/dist/Model/filter/filterApi.d.ts.map +1 -0
  65. package/dist/Model/filter/filterApi.js +2 -0
  66. package/dist/Model/filter/types/errors.d.ts +29 -0
  67. package/dist/Model/filter/types/errors.d.ts.map +1 -0
  68. package/dist/Model/filter/types/errors.js +2 -0
  69. package/dist/Model/filter/types/fields.d.ts +15 -0
  70. package/dist/Model/filter/types/fields.d.ts.map +1 -0
  71. package/dist/Model/filter/types/fields.js +2 -0
  72. package/dist/Model/filter/types/path/common.d.ts +316 -0
  73. package/dist/Model/filter/types/path/common.d.ts.map +1 -0
  74. package/dist/Model/filter/types/path/common.js +2 -0
  75. package/dist/Model/filter/types/path/eager.d.ts +94 -0
  76. package/dist/Model/filter/types/path/eager.d.ts.map +1 -0
  77. package/dist/Model/filter/types/path/eager.js +36 -0
  78. package/dist/Model/filter/types/path/index.d.ts +4 -0
  79. package/dist/Model/filter/types/path/index.d.ts.map +1 -0
  80. package/dist/Model/filter/types/path/index.js +3 -0
  81. package/dist/Model/filter/types/utils.d.ts +79 -0
  82. package/dist/Model/filter/types/utils.d.ts.map +1 -0
  83. package/dist/Model/filter/types/utils.js +2 -0
  84. package/dist/Model/filter/types/validator.d.ts +30 -0
  85. package/dist/Model/filter/types/validator.d.ts.map +1 -0
  86. package/dist/Model/filter/types/validator.js +2 -0
  87. package/dist/Model/filter/types.d.ts +5 -0
  88. package/dist/Model/filter/types.d.ts.map +1 -0
  89. package/dist/Model/filter/types.js +7 -0
  90. package/dist/Model/query/dsl.d.ts +630 -0
  91. package/dist/Model/query/dsl.d.ts.map +1 -0
  92. package/dist/Model/query/dsl.js +376 -0
  93. package/dist/Model/query/new-kid-interpreter.d.ts +132 -0
  94. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -0
  95. package/dist/Model/query/new-kid-interpreter.js +370 -0
  96. package/dist/Model/query.d.ts +15 -0
  97. package/dist/Model/query.d.ts.map +1 -0
  98. package/dist/Model/query.js +3 -0
  99. package/dist/Model.d.ts +6 -0
  100. package/dist/Model.d.ts.map +1 -0
  101. package/dist/Model.js +6 -0
  102. package/dist/NonEmptySet.d.ts +3 -1
  103. package/dist/NonEmptySet.d.ts.map +1 -1
  104. package/dist/NonEmptySet.js +2 -2
  105. package/dist/Option.d.ts +1 -0
  106. package/dist/Option.d.ts.map +1 -1
  107. package/dist/Option.js +3 -1
  108. package/dist/Pure.d.ts +7 -5
  109. package/dist/Pure.d.ts.map +1 -1
  110. package/dist/Pure.js +7 -4
  111. package/dist/QueueMaker.d.ts +11 -0
  112. package/dist/QueueMaker.d.ts.map +1 -0
  113. package/dist/QueueMaker.js +11 -0
  114. package/dist/RequestContext.d.ts +87 -0
  115. package/dist/RequestContext.d.ts.map +1 -0
  116. package/dist/RequestContext.js +45 -0
  117. package/dist/RequestScopedDependencies.d.ts +21 -0
  118. package/dist/RequestScopedDependencies.d.ts.map +1 -0
  119. package/dist/RequestScopedDependencies.js +16 -0
  120. package/dist/Schema/Class.d.ts +200 -20
  121. package/dist/Schema/Class.d.ts.map +1 -1
  122. package/dist/Schema/Class.js +268 -17
  123. package/dist/Schema/FastCheck.d.ts.map +1 -1
  124. package/dist/Schema/SchemaParser.d.ts +5 -0
  125. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  126. package/dist/Schema/SchemaParser.js +6 -0
  127. package/dist/Schema/SpecialJsonSchema.d.ts +34 -0
  128. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  129. package/dist/Schema/SpecialJsonSchema.js +117 -0
  130. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  131. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  132. package/dist/Schema/SpecialOpenApi.js +123 -0
  133. package/dist/Schema/brand.d.ts +5 -5
  134. package/dist/Schema/brand.d.ts.map +1 -1
  135. package/dist/Schema/brand.js +3 -1
  136. package/dist/Schema/email.d.ts.map +1 -1
  137. package/dist/Schema/email.js +7 -6
  138. package/dist/Schema/ext.d.ts +337 -56
  139. package/dist/Schema/ext.d.ts.map +1 -1
  140. package/dist/Schema/ext.js +381 -58
  141. package/dist/Schema/moreStrings.d.ts +82 -36
  142. package/dist/Schema/moreStrings.d.ts.map +1 -1
  143. package/dist/Schema/moreStrings.js +47 -51
  144. package/dist/Schema/numbers.d.ts +34 -21
  145. package/dist/Schema/numbers.d.ts.map +1 -1
  146. package/dist/Schema/numbers.js +55 -12
  147. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  148. package/dist/Schema/phoneNumber.js +6 -6
  149. package/dist/Schema/schema.d.ts +1 -1
  150. package/dist/Schema/schema.d.ts.map +1 -1
  151. package/dist/Schema/schema.js +2 -2
  152. package/dist/Schema/strings.d.ts +18 -4
  153. package/dist/Schema/strings.d.ts.map +1 -1
  154. package/dist/Schema/strings.js +1 -5
  155. package/dist/Schema.d.ts +225 -7
  156. package/dist/Schema.d.ts.map +1 -1
  157. package/dist/Schema.js +193 -26
  158. package/dist/SchemaAST.d.ts +2 -0
  159. package/dist/SchemaAST.d.ts.map +1 -0
  160. package/dist/SchemaAST.js +2 -0
  161. package/dist/Set.d.ts +4 -1
  162. package/dist/Set.d.ts.map +1 -1
  163. package/dist/Set.js +3 -2
  164. package/dist/Store.d.ts +170 -0
  165. package/dist/Store.d.ts.map +1 -0
  166. package/dist/Store.js +113 -0
  167. package/dist/_ext/Array.d.ts +1 -1
  168. package/dist/_ext/Array.d.ts.map +1 -1
  169. package/dist/_ext/Array.js +4 -2
  170. package/dist/_ext/misc.d.ts +4 -1
  171. package/dist/_ext/misc.d.ts.map +1 -1
  172. package/dist/_ext/misc.js +4 -2
  173. package/dist/_ext/ord.ext.d.ts +2 -1
  174. package/dist/_ext/ord.ext.d.ts.map +1 -1
  175. package/dist/_ext/ord.ext.js +2 -2
  176. package/dist/builtin.d.ts +2 -14
  177. package/dist/builtin.d.ts.map +1 -1
  178. package/dist/builtin.js +1 -1
  179. package/dist/client/InvalidationKeys.d.ts +30 -0
  180. package/dist/client/InvalidationKeys.d.ts.map +1 -0
  181. package/dist/client/InvalidationKeys.js +37 -0
  182. package/dist/client/apiClientFactory.d.ts +19 -31
  183. package/dist/client/apiClientFactory.d.ts.map +1 -1
  184. package/dist/client/apiClientFactory.js +157 -39
  185. package/dist/client/clientFor.d.ts +54 -18
  186. package/dist/client/clientFor.d.ts.map +1 -1
  187. package/dist/client/clientFor.js +9 -1
  188. package/dist/client/errors.d.ts +82 -27
  189. package/dist/client/errors.d.ts.map +1 -1
  190. package/dist/client/errors.js +75 -19
  191. package/dist/client/makeClient.d.ts +490 -34
  192. package/dist/client/makeClient.d.ts.map +1 -1
  193. package/dist/client/makeClient.js +62 -30
  194. package/dist/client.d.ts +2 -0
  195. package/dist/client.d.ts.map +1 -1
  196. package/dist/client.js +3 -1
  197. package/dist/faker.d.ts.map +1 -1
  198. package/dist/http/Request.d.ts +1 -1
  199. package/dist/http/Request.d.ts.map +1 -1
  200. package/dist/http/Request.js +2 -2
  201. package/dist/ids.d.ts +42 -14
  202. package/dist/ids.d.ts.map +1 -1
  203. package/dist/ids.js +30 -5
  204. package/dist/index.d.ts +6 -7
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +8 -8
  207. package/dist/middleware.d.ts +13 -7
  208. package/dist/middleware.d.ts.map +1 -1
  209. package/dist/middleware.js +14 -8
  210. package/dist/rpc/Invalidation.d.ts +1124 -0
  211. package/dist/rpc/Invalidation.d.ts.map +1 -0
  212. package/dist/rpc/Invalidation.js +178 -0
  213. package/dist/rpc/MiddlewareMaker.d.ts +11 -7
  214. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  215. package/dist/rpc/MiddlewareMaker.js +59 -38
  216. package/dist/rpc/RpcContextMap.d.ts +3 -3
  217. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  218. package/dist/rpc/RpcContextMap.js +4 -4
  219. package/dist/rpc/RpcMiddleware.d.ts +14 -10
  220. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  221. package/dist/rpc/RpcMiddleware.js +1 -1
  222. package/dist/rpc.d.ts +1 -1
  223. package/dist/rpc.d.ts.map +1 -1
  224. package/dist/rpc.js +2 -2
  225. package/dist/runtime.d.ts +19 -0
  226. package/dist/runtime.d.ts.map +1 -0
  227. package/dist/runtime.js +40 -0
  228. package/dist/setupRequest.d.ts +20 -0
  229. package/dist/setupRequest.d.ts.map +1 -0
  230. package/dist/setupRequest.js +72 -0
  231. package/dist/toast.d.ts +51 -0
  232. package/dist/toast.d.ts.map +1 -0
  233. package/dist/toast.js +34 -0
  234. package/dist/transform.d.ts +1 -1
  235. package/dist/transform.d.ts.map +1 -1
  236. package/dist/transform.js +4 -5
  237. package/dist/utils/effectify.d.ts +1 -1
  238. package/dist/utils/effectify.d.ts.map +1 -1
  239. package/dist/utils/effectify.js +2 -2
  240. package/dist/utils/extend.d.ts.map +1 -1
  241. package/dist/utils/gen.d.ts +4 -4
  242. package/dist/utils/gen.d.ts.map +1 -1
  243. package/dist/utils/logLevel.d.ts +2 -2
  244. package/dist/utils/logLevel.d.ts.map +1 -1
  245. package/dist/utils/logger.d.ts +4 -3
  246. package/dist/utils/logger.d.ts.map +1 -1
  247. package/dist/utils/logger.js +4 -4
  248. package/dist/utils.d.ts +34 -39
  249. package/dist/utils.d.ts.map +1 -1
  250. package/dist/utils.js +33 -37
  251. package/dist/validation/validators.d.ts.map +1 -1
  252. package/dist/validation.d.ts.map +1 -1
  253. package/dist/withToast.d.ts +30 -0
  254. package/dist/withToast.d.ts.map +1 -0
  255. package/dist/withToast.js +64 -0
  256. package/package.json +27 -251
  257. package/src/Array.ts +5 -5
  258. package/src/Chunk.ts +3 -3
  259. package/src/Config/SecretURL.ts +6 -3
  260. package/src/Config/internal/configSecretURL.ts +2 -2
  261. package/src/Config.ts +7 -0
  262. package/src/ConfigProvider.ts +5 -0
  263. package/src/{ServiceMap.ts → Context.ts} +56 -63
  264. package/src/DataDependencies.ts +171 -0
  265. package/src/Effect.ts +15 -17
  266. package/src/Emailer.ts +51 -0
  267. package/src/Inputify.type.ts +1 -1
  268. package/src/Layer.ts +11 -7
  269. package/src/Model/Repository/Registry.ts +35 -0
  270. package/src/Model/Repository/dependency.ts +32 -0
  271. package/src/Model/Repository/ext.ts +374 -0
  272. package/src/Model/Repository/internal/internal.ts +950 -0
  273. package/src/Model/Repository/legacy.ts +29 -0
  274. package/src/Model/Repository/makeRepo.ts +156 -0
  275. package/src/Model/Repository/service.ts +676 -0
  276. package/src/Model/Repository/validation.ts +31 -0
  277. package/src/Model/Repository.ts +7 -0
  278. package/src/Model/dsl.ts +129 -0
  279. package/src/Model/filter/filterApi.ts +78 -0
  280. package/src/Model/filter/types/errors.ts +47 -0
  281. package/src/Model/filter/types/fields.ts +50 -0
  282. package/src/Model/filter/types/path/common.ts +404 -0
  283. package/src/Model/filter/types/path/eager.ts +329 -0
  284. package/src/Model/filter/types/path/index.ts +4 -0
  285. package/src/Model/filter/types/utils.ts +128 -0
  286. package/src/Model/filter/types/validator.ts +46 -0
  287. package/src/Model/filter/types.ts +6 -0
  288. package/src/Model/query/dsl.ts +3092 -0
  289. package/src/Model/query/new-kid-interpreter.ts +532 -0
  290. package/src/Model/query.ts +13 -0
  291. package/src/Model.ts +5 -0
  292. package/src/NonEmptySet.ts +6 -4
  293. package/src/Option.ts +2 -0
  294. package/src/Pure.ts +10 -7
  295. package/src/QueueMaker.ts +11 -0
  296. package/src/RequestContext.ts +48 -0
  297. package/src/RequestScopedDependencies.ts +45 -0
  298. package/src/Schema/Class.ts +648 -60
  299. package/src/Schema/SchemaParser.ts +12 -0
  300. package/src/Schema/SpecialJsonSchema.ts +137 -0
  301. package/src/Schema/SpecialOpenApi.ts +130 -0
  302. package/src/Schema/brand.ts +24 -9
  303. package/src/Schema/email.ts +9 -7
  304. package/src/Schema/ext.ts +502 -98
  305. package/src/Schema/moreStrings.ts +145 -83
  306. package/src/Schema/numbers.ts +97 -28
  307. package/src/Schema/phoneNumber.ts +8 -8
  308. package/src/Schema/schema.ts +1 -1
  309. package/src/Schema/strings.ts +23 -14
  310. package/src/Schema.ts +403 -46
  311. package/src/SchemaAST.ts +1 -0
  312. package/src/Set.ts +6 -2
  313. package/src/Store.ts +280 -0
  314. package/src/_ext/Array.ts +4 -2
  315. package/src/_ext/misc.ts +4 -1
  316. package/src/_ext/ord.ext.ts +2 -1
  317. package/src/builtin-json.d.ts +24 -0
  318. package/src/builtin.ts +16 -21
  319. package/src/client/InvalidationKeys.ts +55 -0
  320. package/src/client/apiClientFactory.ts +301 -141
  321. package/src/client/clientFor.ts +107 -34
  322. package/src/client/errors.ts +100 -29
  323. package/src/client/makeClient.ts +594 -80
  324. package/src/client.ts +6 -4
  325. package/src/http/Request.ts +3 -3
  326. package/src/http.ts +1 -1
  327. package/src/ids.ts +33 -6
  328. package/src/index.ts +20 -23
  329. package/src/middleware.ts +13 -9
  330. package/src/rpc/Invalidation.ts +278 -0
  331. package/src/rpc/MiddlewareMaker.ts +88 -80
  332. package/src/rpc/README.md +2 -2
  333. package/src/rpc/RpcContextMap.ts +7 -6
  334. package/src/rpc/RpcMiddleware.ts +19 -13
  335. package/src/rpc.ts +4 -4
  336. package/src/runtime.ts +57 -0
  337. package/src/setupRequest.ts +138 -0
  338. package/src/toast.ts +54 -0
  339. package/src/transform.ts +4 -4
  340. package/src/utils/effectify.ts +1 -1
  341. package/src/utils/gen.ts +8 -8
  342. package/src/utils/logLevel.ts +1 -1
  343. package/src/utils/logger.ts +4 -3
  344. package/src/utils.ts +85 -158
  345. package/src/validation.ts +2 -2
  346. package/src/withToast.ts +133 -0
  347. package/test/dist/rpc.test.d.ts.map +1 -1
  348. package/test/dist/rpcMiddlewareContext.test.d.ts.map +1 -0
  349. package/test/dist/secretURL.test.d.ts.map +1 -0
  350. package/test/dist/special.test.d.ts.map +1 -0
  351. package/test/moreStrings.test.ts +7 -9
  352. package/test/rpc.test.ts +42 -14
  353. package/test/rpcMiddlewareContext.test.ts +63 -0
  354. package/test/schema.test.ts +514 -35
  355. package/test/secretURL.test.ts +160 -0
  356. package/test/special.test.ts +1261 -0
  357. package/test/utils.test.ts +7 -7
  358. package/tsconfig.base.json +6 -5
  359. package/tsconfig.json +9 -2
  360. package/tsconfig.src.json +29 -29
  361. package/tsconfig.test.json +2 -2
  362. package/dist/Operations.d.ts +0 -123
  363. package/dist/Operations.d.ts.map +0 -1
  364. package/dist/Operations.js +0 -29
  365. package/dist/ServiceMap.d.ts.map +0 -1
  366. package/dist/ServiceMap.js +0 -91
  367. package/eslint.config.mjs +0 -26
  368. package/src/Operations.ts +0 -55
  369. package/tsconfig.json.bak +0 -12
@@ -0,0 +1,950 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import * as Equivalence from "effect/Equivalence"
4
+ import { flow, pipe } from "effect/Function"
5
+ import * as HashMap from "effect/HashMap"
6
+ import * as HashSet from "effect/HashSet"
7
+ import * as Metric from "effect/Metric"
8
+ import * as Pipeable from "effect/Pipeable"
9
+ import * as Ref from "effect/Ref"
10
+ import * as Result from "effect/Result"
11
+ import type * as Scope from "effect/Scope"
12
+ import * as Unify from "effect/Unify"
13
+ import * as Array from "../../../Array.ts"
14
+ import type { NonEmptyReadonlyArray } from "../../../Array.ts"
15
+ import { toNonEmptyArray } from "../../../Array.ts"
16
+ import * as Chunk from "../../../Chunk.ts"
17
+ import { NotFoundError } from "../../../client/errors.ts"
18
+ import * as Context from "../../../Context.ts"
19
+ import * as DataDependencies from "../../../DataDependencies.ts"
20
+ import * as Effect from "../../../Effect.ts"
21
+ import { flatMapOption } from "../../../Effect.ts"
22
+ import * as Option from "../../../Option.ts"
23
+ import * as S from "../../../Schema.ts"
24
+ import { type Codec, NonNegativeInt } from "../../../Schema.ts"
25
+ import * as SchemaAST from "../../../SchemaAST.ts"
26
+ import { setupRequestContextFromCurrent } from "../../../setupRequest.ts"
27
+ import { type FilterArgs, getContextMap, type PersistenceModelType, type StoreConfig, storeId, StoreMaker } from "../../../Store.ts"
28
+ import type { FilterResult } from "../../filter/filterApi.ts"
29
+ import type { FieldValues } from "../../filter/types.ts"
30
+ import * as Q from "../../query.ts"
31
+ import { repositoryDependencyPaths } from "../dependency.ts"
32
+ import type { ChangeFeed, ChangeFeedEvent, Repository } from "../service.ts"
33
+ import { ValidationError, ValidationResult } from "../validation.ts"
34
+
35
+ const dedupe = Array.dedupeWith(Equivalence.String)
36
+
37
+ // ms buckets: dense under 100ms (common path), then mid-tail and multi-second stalls.
38
+ // Rare 0.5–1s+ encodes (fat aggregates) must not collapse into a single overflow bin.
39
+ const schemaDurationBoundaries = [
40
+ 0.1,
41
+ 0.5,
42
+ 1,
43
+ 2,
44
+ 5,
45
+ 10,
46
+ 25,
47
+ 50,
48
+ 100,
49
+ 150,
50
+ 200,
51
+ 300,
52
+ 500,
53
+ 750,
54
+ 1_000,
55
+ 1_500,
56
+ 2_000,
57
+ 5_000
58
+ ]
59
+ // Event-loop-relevant stall floor: encodes/decodes above this are rare but block Node.
60
+ const SCHEMA_SLOW_MS = 100
61
+ const schemaDecodeDuration = Metric.histogram("app.schema.decode.duration", { boundaries: schemaDurationBoundaries })
62
+ const schemaEncodeDuration = Metric.histogram("app.schema.encode.duration", { boundaries: schemaDurationBoundaries })
63
+ const schemaItemCount = Metric.histogram("app.schema.item_count", {
64
+ boundaries: [0, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1_000, 5_000]
65
+ })
66
+ const schemaSlow = Metric.counter("app.schema.slow", {
67
+ description: `Repository schema encode/decode slower than ${SCHEMA_SLOW_MS}ms`,
68
+ incremental: true
69
+ })
70
+
71
+ const entityStateFromItems = (items: readonly unknown[]): string | undefined => {
72
+ const first = items[0]
73
+ if (first === null || typeof first !== "object" || !("_tag" in first)) return undefined
74
+ const tag = (first as { readonly _tag: unknown })._tag
75
+ return typeof tag === "string" ? tag : undefined
76
+ }
77
+
78
+ const timeSchema = (
79
+ operation: "decode" | "encode",
80
+ entity: string,
81
+ queryMode: "aggregate" | "collect" | "project" | "transform" | undefined,
82
+ itemCount: number,
83
+ entityState?: string
84
+ ) =>
85
+ <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
86
+ Effect.clockWith((clock) => {
87
+ const startedAt = clock.currentTimeNanosUnsafe()
88
+ const attributes = {
89
+ "app.entity": entity,
90
+ "app.schema.operation": operation,
91
+ ...(queryMode !== undefined && { "app.query.mode": queryMode }),
92
+ ...(entityState !== undefined && { "app.entity.state": entityState })
93
+ }
94
+ return Effect.onExit(self, () => {
95
+ const durationMs = Number(clock.currentTimeNanosUnsafe() - startedAt) / 1_000_000
96
+ const slow = durationMs >= SCHEMA_SLOW_MS
97
+ return Effect.all([
98
+ Effect.annotateCurrentSpan({
99
+ [`app.schema.${operation}.duration_ms`]: durationMs,
100
+ "app.schema.item_count": itemCount,
101
+ "app.schema.slow": slow,
102
+ ...(queryMode !== undefined && { "app.query.mode": queryMode }),
103
+ ...(entityState !== undefined && { "app.entity.state": entityState })
104
+ }),
105
+ Metric.update(
106
+ Metric.withAttributes(
107
+ operation === "decode" ? schemaDecodeDuration : schemaEncodeDuration,
108
+ attributes
109
+ ),
110
+ durationMs
111
+ ),
112
+ Metric.update(Metric.withAttributes(schemaItemCount, attributes), itemCount),
113
+ ...(slow
114
+ ? [Metric.update(Metric.withAttributes(schemaSlow, attributes), 1)] as const
115
+ : [])
116
+ ], { discard: true })
117
+ })
118
+ })
119
+
120
+ /**
121
+ * A base implementation to create a repository.
122
+ */
123
+ export function makeRepoInternal<
124
+ Evt = never
125
+ >() {
126
+ return <
127
+ ItemType extends string,
128
+ R,
129
+ Encoded extends FieldValues,
130
+ T,
131
+ IdKey extends keyof T & keyof Encoded
132
+ >(
133
+ name: ItemType,
134
+ schema: S.Codec<T, Encoded, R>,
135
+ mapFrom: (pm: Encoded) => Encoded,
136
+ mapTo: (e: Encoded, etag: string | undefined) => PersistenceModelType<Encoded>,
137
+ idKey: IdKey
138
+ ) => {
139
+ type PM = PersistenceModelType<Encoded>
140
+ function mapToPersistenceModel(
141
+ e: Encoded,
142
+ getEtag: (id: string) => string | undefined
143
+ ): PM {
144
+ return mapTo(e, getEtag(e[idKey]))
145
+ }
146
+
147
+ function mapReverse(
148
+ { _etag, ...e }: PM,
149
+ setEtag: (id: string, eTag: string | undefined) => void
150
+ ): Encoded {
151
+ setEtag((e as any)[idKey], _etag)
152
+ return mapFrom(e as unknown as Encoded)
153
+ }
154
+
155
+ const mkStore = makeStore<Encoded>()(name, schema, mapTo, idKey)
156
+
157
+ function make<RInitial = never, E = never, RPublish = never, RCtx = never>(
158
+ args: [Evt] extends [never] ? {
159
+ schemaContext?: Context.Context<RCtx>
160
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
161
+ dependencyIds?: (item: T) => NonEmptyReadonlyArray<string>
162
+ additionalWriteDependencies?: (item: T) => ReadonlyArray<DataDependencies.DataDependency>
163
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
164
+ partitionValue?: (e?: Encoded) => string
165
+ }
166
+ }
167
+ : {
168
+ schemaContext?: Context.Context<RCtx>
169
+ publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, RPublish>
170
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
171
+ dependencyIds?: (item: T) => NonEmptyReadonlyArray<string>
172
+ additionalWriteDependencies?: (item: T) => ReadonlyArray<DataDependencies.DataDependency>
173
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
174
+ partitionValue?: (e?: Encoded) => string
175
+ }
176
+ }
177
+ ) {
178
+ return Effect
179
+ .gen(function*() {
180
+ const rctx: Context.Context<RCtx> = args.schemaContext ?? Context.empty() as any
181
+ const provideRctx = Effect.provide(rctx)
182
+ const encodeMany = (items: readonly T[]) =>
183
+ S.encodeEffect(S.Array(schema))(items).pipe(
184
+ provideRctx,
185
+ timeSchema("encode", name, undefined, items.length, entityStateFromItems(items))
186
+ )
187
+ const decode = flow(S.decodeEffectConcurrently(schema), provideRctx)
188
+ const decodeMany = flow(
189
+ S.decodeEffectConcurrently(S.Array(schema)),
190
+ provideRctx
191
+ )
192
+
193
+ const store = yield* mkStore(args.makeInitial, args.config)
194
+ const dependencyPaths = repositoryDependencyPaths(schema)
195
+ const recordRead = DataDependencies.readRepo(name)
196
+ const entityDependency = (ids: NonEmptyReadonlyArray<T[IdKey]>) =>
197
+ DataDependencies.repo(name, [String(ids[0]), ...ids.slice(1).map(String)])
198
+ const valuesAtPath = (value: unknown, path: string): readonly unknown[] => {
199
+ if (path.length === 0) return [value]
200
+ const [head, ...tail] = path.split(".")
201
+ if (head === undefined) return [value]
202
+ if (head === "-1") {
203
+ return globalThis.Array.isArray(value)
204
+ ? value.flatMap((item) => valuesAtPath(item, tail.join(".")))
205
+ : []
206
+ }
207
+ return typeof value === "object" && value !== null
208
+ ? valuesAtPath((value as Record<string, unknown>)[head], tail.join("."))
209
+ : []
210
+ }
211
+ const dependencyIds = (item: T): NonEmptyReadonlyArray<string> => {
212
+ const configured = dependencyPaths
213
+ .flatMap((path) => valuesAtPath(item, String(path)))
214
+ .filter((value): value is string => typeof value === "string") ?? []
215
+ const ids = args.dependencyIds?.(item) ?? [String(item[idKey]), ...configured]
216
+ return [ids[0], ...ids.slice(1)]
217
+ }
218
+ const itemDependencies = (item: T) => {
219
+ const ids = dependencyIds(item)
220
+ return [
221
+ DataDependencies.repo(name, ids),
222
+ ...(args.additionalWriteDependencies?.(item) ?? [])
223
+ ]
224
+ }
225
+ const queryDependencyIds = (filter: readonly FilterResult[] | undefined): readonly string[] => {
226
+ if (!filter) return []
227
+ const paths = new Set([String(idKey), ...dependencyPaths])
228
+ const visit = (items: readonly FilterResult[]): readonly string[] => {
229
+ if (items.some((item) => item.t === "or" || item.t === "or-scope")) return []
230
+ return items.flatMap((item) => {
231
+ if ("result" in item) return visit(item.result)
232
+ if (!paths.has(item.path)) return []
233
+ const value: unknown = item.value
234
+ if (item.op === "eq" && typeof value === "string") return [value]
235
+ if (item.op === "in" && globalThis.Array.isArray(value)) {
236
+ return value.filter((entry): entry is string => typeof entry === "string")
237
+ }
238
+ return []
239
+ })
240
+ }
241
+ return [...new Set(visit(filter))]
242
+ }
243
+ const recordEntityRead = (id: T[IdKey]) => DataDependencies.read(entityDependency([id]))
244
+ const recordEntityWrite = (ids: NonEmptyReadonlyArray<T[IdKey]>) =>
245
+ DataDependencies.write(entityDependency(ids))
246
+ const recordItemWrite = (items: NonEmptyReadonlyArray<T>) =>
247
+ Effect.forEach(
248
+ DataDependencies.merge(new Set(items.flatMap(itemDependencies))),
249
+ DataDependencies.write,
250
+ { discard: true }
251
+ )
252
+ const cms = Effect.map(getContextMap.pipe(Effect.orDie), (_) => ({
253
+ get: (id: string) => _.get(`${name}.${id}`),
254
+ set: (id: string, etag: string | undefined) => _.set(`${name}.${id}`, etag)
255
+ }))
256
+
257
+ const pub = "publishEvents" in args
258
+ ? args.publishEvents
259
+ : () => Effect.void
260
+
261
+ type ChangeFeedHandler = (evt: ChangeFeedEvent<T>) => Effect.Effect<void>
262
+ const changeFeedByNamespace = yield* Ref.make(HashMap.empty<string, HashSet.HashSet<ChangeFeedHandler>>())
263
+ const changeFeedWildcard = yield* Ref.make(HashSet.empty<ChangeFeedHandler>())
264
+ // clear all handlers when the repository's scope closes
265
+ yield* Effect.addFinalizer(() =>
266
+ Effect.all([
267
+ Ref.set(changeFeedByNamespace, HashMap.empty<string, HashSet.HashSet<ChangeFeedHandler>>()),
268
+ Ref.set(changeFeedWildcard, HashSet.empty<ChangeFeedHandler>())
269
+ ], { discard: true })
270
+ )
271
+ const changeFeed: ChangeFeed<T> = {
272
+ publish: ([items, op]) =>
273
+ Effect.gen(function*() {
274
+ const ns = yield* storeId
275
+ const evt: ChangeFeedEvent<T> = [items, op, ns]
276
+ const map = yield* Ref.get(changeFeedByNamespace)
277
+ const wild = yield* Ref.get(changeFeedWildcard)
278
+ const targeted = Option.getOrElse(HashMap.get(map, ns), () => HashSet.empty<ChangeFeedHandler>())
279
+ const all = HashSet.union(targeted, wild)
280
+ yield* Effect.forEach(all, (h) => h(evt), { concurrency: "unbounded", discard: true })
281
+ }),
282
+ subscribe: (handler, options) => {
283
+ const ns = options?.namespace
284
+ if (ns === undefined) {
285
+ return Effect.acquireRelease(
286
+ Ref.update(changeFeedWildcard, HashSet.add(handler)),
287
+ () => Ref.update(changeFeedWildcard, HashSet.remove(handler))
288
+ )
289
+ }
290
+ return Effect.acquireRelease(
291
+ Ref.update(changeFeedByNamespace, (m) => {
292
+ const cur = Option.getOrElse(HashMap.get(m, ns), () => HashSet.empty<ChangeFeedHandler>())
293
+ return HashMap.set(m, ns, HashSet.add(cur, handler))
294
+ }),
295
+ () =>
296
+ Ref.update(changeFeedByNamespace, (m) => {
297
+ const cur = Option.getOrElse(HashMap.get(m, ns), () => HashSet.empty<ChangeFeedHandler>())
298
+ const next = HashSet.remove(cur, handler)
299
+ return HashSet.size(next) === 0 ? HashMap.remove(m, ns) : HashMap.set(m, ns, next)
300
+ })
301
+ )
302
+ }
303
+ }
304
+
305
+ const allE = cms
306
+ .pipe(Effect.flatMap((cm) => Effect.map(store.all, (_) => _.map((_) => mapReverse(_, cm.set)))))
307
+
308
+ const all = Effect
309
+ .flatMap(
310
+ allE,
311
+ (items) =>
312
+ decodeMany(items).pipe(
313
+ Effect.orDie,
314
+ timeSchema("decode", name, undefined, items.length)
315
+ )
316
+ )
317
+ .pipe(
318
+ Effect.tap(() => recordRead),
319
+ Effect.map((_) => _ as T[]),
320
+ Effect.withSpan("Repository.all", {
321
+ kind: "client",
322
+ attributes: { "app.entity": name }
323
+ }, { captureStackTrace: false })
324
+ )
325
+
326
+ const fieldsSchema = schema as unknown as { fields: any }
327
+ // assumes the id field never needs a service...
328
+ const i = ("fields" in fieldsSchema ? S.Struct(fieldsSchema["fields"]) as unknown as typeof schema : schema)
329
+ .pipe((_) => {
330
+ let ast = _.ast
331
+ if (ast._tag === "Declaration") ast = ast.typeParameters[0]!
332
+
333
+ const pickIdFromAst = (a: SchemaAST.AST) => {
334
+ // Unwrap Declaration (e.g. TaggedClass) to get the underlying Objects AST
335
+ let inner = a
336
+ if (inner._tag === "Declaration") inner = inner.typeParameters[0]!
337
+ // Pick from the original AST to preserve the full encoding chain (e.g. decodeTo transformations).
338
+ // Using toEncoded would lose transformation info needed to encode Type -> Encoded.
339
+ if (SchemaAST.isObjects(inner)) {
340
+ const field = inner.propertySignatures.find((_) => _.name === idKey)
341
+ if (field) {
342
+ return S.Struct({ [idKey]: S.make(field.type) }) as unknown as Codec<T, Encoded>
343
+ }
344
+ }
345
+ return S.make(a) as unknown as Codec<T, Encoded>
346
+ }
347
+
348
+ return ast._tag === "Union"
349
+ // we need to get the Objects (TypeLiteral), in case of class it has encoding chain...
350
+ ? S.Union(
351
+ ast.types.map((_) => pickIdFromAst(_))
352
+ )
353
+ : pickIdFromAst(ast)
354
+ })
355
+ const encodeId = flow(S.encodeEffect(i), provideRctx)
356
+ const encodeIdOnly = (id: string) =>
357
+ encodeId({ [idKey]: id } as any).pipe(
358
+ Effect.map((_: Record<string, unknown>) => _[idKey as string] as Encoded[IdKey])
359
+ )
360
+ const findEId = Effect.fnUntraced(function*(id: Encoded[IdKey]) {
361
+ yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
362
+
363
+ return yield* Effect.flatMap(
364
+ store.find(id),
365
+ (item) =>
366
+ Effect.gen(function*() {
367
+ const { set } = yield* cms
368
+ return item.pipe(Option.map((_) => mapReverse(_, set)))
369
+ })
370
+ )
371
+ })
372
+ // TODO: select the particular field, instead of as struct
373
+ const findE = Effect.fnUntraced(function*(id: T[IdKey]) {
374
+ yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
375
+
376
+ return yield* pipe(
377
+ encodeId({ [idKey]: id } as any),
378
+ Effect.orDie,
379
+ Effect.map((_) => (_ as any)[idKey]),
380
+ Effect.flatMap(findEId)
381
+ )
382
+ })
383
+
384
+ const loadExistingItems = (ids: ReadonlyArray<T[IdKey]>) =>
385
+ Effect
386
+ .forEach(
387
+ ids,
388
+ (id) =>
389
+ findE(id).pipe(
390
+ Effect.flatMap(Option.match({
391
+ onNone: () => Effect.succeed(Option.none<T>()),
392
+ onSome: (encoded) => decode(encoded).pipe(Effect.orDie, Effect.map(Option.some))
393
+ }))
394
+ )
395
+ )
396
+ .pipe(Effect.map(Array.getSomes))
397
+
398
+ const find = Effect.fn("Repository.find", {
399
+ kind: "client",
400
+ attributes: { "app.entity": name }
401
+ })(function*(id: T[IdKey]) {
402
+ yield* recordEntityRead(id)
403
+ yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
404
+ return yield* flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
405
+ })
406
+
407
+ const saveAllE = (a: Iterable<Encoded>) =>
408
+ flatMapOption(
409
+ Effect
410
+ .sync(() => toNonEmptyArray([...a])),
411
+ (a) =>
412
+ Effect.gen(function*() {
413
+ const { get, set } = yield* cms
414
+ const items = a.map((_) => mapToPersistenceModel(_, get))
415
+ const ret = yield* store.batchSet(items)
416
+ ret.forEach((_) => set(_[idKey], _._etag))
417
+ })
418
+ )
419
+ .pipe(Effect.asVoid)
420
+
421
+ const saveAll = (a: Iterable<T>) =>
422
+ encodeMany(Array.fromIterable(a))
423
+ .pipe(
424
+ Effect.orDie,
425
+ Effect.andThen(saveAllE)
426
+ )
427
+
428
+ const saveAndPublish = Effect.fn("Repository.saveAndPublish", { attributes: { "app.entity": name } })(
429
+ function*(items: Iterable<T>, events: Iterable<Evt> = []) {
430
+ const it = Chunk.fromIterable(items)
431
+ if (Chunk.isNonEmpty(it)) {
432
+ const values = Chunk.toReadonlyArray(it)
433
+ const previous = args.additionalWriteDependencies || args.dependencyIds || dependencyPaths.length > 0
434
+ ? yield* loadExistingItems(values.map((item) => item[idKey]))
435
+ : []
436
+ yield* recordItemWrite([values[0], ...values.slice(1), ...previous])
437
+ }
438
+ const evts = [...events]
439
+ yield* Effect.annotateCurrentSpan({
440
+ "app.entity.ids": Chunk.map(it, (_) => _[idKey]),
441
+ "app.event.count": evts.length
442
+ })
443
+ return yield* saveAll(it)
444
+ .pipe(
445
+ Effect.andThen(Effect.sync(() => toNonEmptyArray(evts))),
446
+ // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
447
+ (_) => flatMapOption(_, pub),
448
+ Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"] as [T[], "save" | "remove"])),
449
+ Effect.asVoid
450
+ )
451
+ }
452
+ )
453
+
454
+ const removeAndPublish = Effect.fn("Repository.removeAndPublish", { attributes: { "app.entity": name } })(
455
+ function*(a: Iterable<T>, events: Iterable<Evt> = []) {
456
+ const { set } = yield* cms
457
+ const it = [...a]
458
+ if (Array.isReadonlyArrayNonEmpty(it)) {
459
+ yield* recordItemWrite(it)
460
+ }
461
+ const evts = [...events]
462
+ yield* Effect.annotateCurrentSpan({
463
+ "app.entity.ids": it.map((_) => _[idKey]),
464
+ "app.event.count": evts.length
465
+ })
466
+ const items = yield* encodeMany(it).pipe(Effect.orDie)
467
+ if (Array.isReadonlyArrayNonEmpty(items)) {
468
+ yield* store.batchRemove(
469
+ items.map((_) => (_[idKey])),
470
+ args.config?.partitionValue?.(items[0])
471
+ )
472
+ for (const e of items) {
473
+ set(e[idKey], undefined)
474
+ }
475
+ yield* Effect
476
+ .sync(() => toNonEmptyArray(evts))
477
+ // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
478
+ .pipe((_) => flatMapOption(_, pub))
479
+
480
+ yield* changeFeed.publish([it, "remove"] as [T[], "save" | "remove"])
481
+ }
482
+ }
483
+ )
484
+
485
+ const removeById = Effect.fn("Repository.removeById", { attributes: { "app.entity": name } })(
486
+ function*(idOrIds: T[IdKey] | ReadonlyArray<T[IdKey]>) {
487
+ const ids = globalThis.Array.isArray(idOrIds)
488
+ ? idOrIds as readonly T[IdKey][]
489
+ : [idOrIds as T[IdKey]]
490
+ if (!Array.isReadonlyArrayNonEmpty(ids)) {
491
+ return
492
+ }
493
+ yield* recordEntityWrite(ids)
494
+ if (args.additionalWriteDependencies || args.dependencyIds || dependencyPaths.length > 0) {
495
+ const previous = yield* loadExistingItems(ids)
496
+ if (Array.isReadonlyArrayNonEmpty(previous)) yield* recordItemWrite(previous)
497
+ }
498
+ const { set } = yield* cms
499
+ const eids = yield* Effect.forEach(ids, (_) => encodeIdOnly(_ as any)).pipe(Effect.orDie)
500
+ yield* Effect.annotateCurrentSpan({ "app.entity.ids": eids })
501
+ yield* store.batchRemove(eids)
502
+ for (const id of eids) {
503
+ set(id, undefined)
504
+ }
505
+ yield* changeFeed.publish([[], "remove"] as [T[], "save" | "remove"])
506
+ }
507
+ )
508
+
509
+ const parseMany = Effect.fnUntraced(
510
+ function*(items: readonly PM[]) {
511
+ const cm = yield* cms
512
+ return yield* decodeMany(items.map((_) => mapReverse(_, cm.set))).pipe(
513
+ Effect.orDie,
514
+ timeSchema("decode", name, "transform", items.length)
515
+ )
516
+ }
517
+ )
518
+ const decodeManyCache = new WeakMap<
519
+ S.Codec<any, any, any>,
520
+ (i: readonly any[]) => Effect.Effect<any, any, any>
521
+ >()
522
+ const getDecodeMany = (s: S.Codec<any, Encoded, any>) => {
523
+ let dec = decodeManyCache.get(s)
524
+ if (!dec) {
525
+ dec = S.decodeEffectConcurrently(S.Array(s))
526
+ decodeManyCache.set(s, dec)
527
+ }
528
+ return dec
529
+ }
530
+ const parseMany2 = Effect.fnUntraced(
531
+ function*<A, R>(items: readonly PM[], schema: S.Codec<A, Encoded, R>) {
532
+ const cm = yield* cms
533
+ return yield* getDecodeMany(schema)(items.map((_) => mapReverse(_, cm.set))).pipe(
534
+ Effect.orDie,
535
+ timeSchema("decode", name, "transform", items.length)
536
+ )
537
+ }
538
+ )
539
+ const filter = <U extends keyof Encoded = keyof Encoded>(args: FilterArgs<Encoded, U>) =>
540
+ store
541
+ .filter(
542
+ // always enforce id and _etag because they are system fields, required for etag tracking etc
543
+ {
544
+ ...args,
545
+ select: args.select
546
+ ? dedupe([...args.select, idKey, "_etag" as any])
547
+ : undefined
548
+ } as typeof args
549
+ )
550
+ .pipe(
551
+ Effect.tap((items) =>
552
+ Effect.map(cms, ({ set }) => items.forEach((_) => set((_ as Encoded)[idKey], (_ as PM)._etag)))
553
+ )
554
+ )
555
+
556
+ // TODO: For raw we should use S.from, and drop the R...
557
+ const query: {
558
+ <A, R, From extends FieldValues>(
559
+ q: Q.QueryProjection<Encoded extends From ? From : never, A, R>
560
+ ): Effect.Effect<readonly A[], S.SchemaError, Exclude<R, RCtx>>
561
+ <A, R, EncodedRefined extends Encoded = Encoded>(
562
+ q: Q.QAll<NoInfer<Encoded>, NoInfer<EncodedRefined>, A, R>
563
+ ): Effect.Effect<readonly A[], never, Exclude<R, RCtx>>
564
+ } = (<A, R, EncodedRefined extends Encoded = Encoded>(q: Q.QAll<Encoded, EncodedRefined, A, R>) => {
565
+ const a = Q.toFilter(q, schema)
566
+ const scopedIds = queryDependencyIds(a.filter)
567
+ const recordQueryRead = Array.isReadonlyArrayNonEmpty(scopedIds)
568
+ ? DataDependencies.read(DataDependencies.repo(name, scopedIds))
569
+ : recordRead
570
+ // Mode dispatch — see `Q.project` JSDoc for the contract:
571
+ // aggregate: GROUP BY + aggregate functions at DB level; decode raw rows with schema; SchemaError surfaces.
572
+ // project : decode raw encoded rows with schema; no PM reverse-mapping; SchemaError surfaces.
573
+ // collect : same as project, but schema yields Option and None rows are dropped.
574
+ // transform: PM reverse-map (re-inject _etag/PM state from cms cache) then decode; orDie.
575
+ const eff = a.mode === "aggregate"
576
+ ? store
577
+ // `a.select` contains `{ key, aggregate }` items not expressible in FilterFunc<Encoded, U>'s
578
+ // `U extends keyof Encoded` generic. Cast is unavoidable until FilterFunc supports aggregate mode.
579
+ .filter(a as any)
580
+ // Decode raw aggregate rows directly — no PM reverse-mapping, no id/_etag needed.
581
+ .pipe(
582
+ Effect.andThen(
583
+ (items) =>
584
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema ?? schema)))(items).pipe(
585
+ provideRctx,
586
+ timeSchema("decode", name, "aggregate", items.length)
587
+ )
588
+ )
589
+ )
590
+ : a.mode === "project"
591
+ ? filter(a)
592
+ // TODO: mapFrom but need to support per field and dependencies
593
+ .pipe(
594
+ Effect.andThen(
595
+ (items) =>
596
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema ?? schema)))(items).pipe(
597
+ provideRctx,
598
+ timeSchema("decode", name, "project", items.length)
599
+ )
600
+ )
601
+ )
602
+ : a.mode === "collect"
603
+ ? filter(a)
604
+ // TODO: mapFrom but need to support per field and dependencies
605
+ .pipe(
606
+ Effect.flatMap((items) =>
607
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema)))(items).pipe(
608
+ Effect.map(Array.getSomes),
609
+ provideRctx,
610
+ timeSchema("decode", name, "collect", items.length)
611
+ )
612
+ )
613
+ )
614
+ : Effect.flatMap(
615
+ filter(a),
616
+ (_) =>
617
+ Unify.unify(
618
+ a.schema
619
+ // TODO: partial may not match?
620
+ ? parseMany2(_ as any, a.schema as any)
621
+ : parseMany(_ as any)
622
+ )
623
+ )
624
+ return pipe(
625
+ a.ttype === "one"
626
+ ? Effect.flatMap(
627
+ eff,
628
+ flow(
629
+ Array.head,
630
+ Option.match({
631
+ onNone: () => Effect.fail(new NotFoundError({ id: "query", /* TODO */ type: name })),
632
+ onSome: Effect.succeed
633
+ })
634
+ )
635
+ )
636
+ : a.ttype === "count"
637
+ ? Effect
638
+ .map(eff, (_) => NonNegativeInt(_.length))
639
+ .pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e)))
640
+ : eff,
641
+ Effect.tap((r) =>
642
+ Effect.annotateCurrentSpan({
643
+ "app.query.ttype": a.ttype,
644
+ "app.query.mode": a.mode,
645
+ "db.response.returned_rows": Array.isArray(r) ? r.length : 1
646
+ })
647
+ ),
648
+ Effect.tap(() => recordQueryRead),
649
+ Effect.withSpan("Repository.query", {
650
+ kind: "client",
651
+ attributes: { "app.entity": name }
652
+ }, { captureStackTrace: false })
653
+ )
654
+ }) as any
655
+
656
+ const validateSample = Effect.fn("Repository.validateSample", { attributes: { "app.entity": name } })(
657
+ function*(options?: {
658
+ percentage?: number
659
+ maxItems?: number
660
+ }) {
661
+ const percentage = options?.percentage ?? 0.1 // default 10%
662
+ const maxItems = options?.maxItems
663
+
664
+ // 1. get all IDs with projection (bypasses main schema decode)
665
+ const allIds = yield* store
666
+ .filter({
667
+ t: null as unknown as Encoded,
668
+ select: [idKey as keyof Encoded]
669
+ })
670
+ .pipe(Effect.withSpan("Repository.filter", {
671
+ kind: "client",
672
+ attributes: { "app.entity": name }
673
+ }, { captureStackTrace: false }))
674
+
675
+ // 2. random subset
676
+ const shuffled = [...allIds].sort(() => Math.random() - 0.5)
677
+ const sampleSize = Math.min(
678
+ maxItems ?? Infinity,
679
+ Math.ceil(allIds.length * percentage)
680
+ )
681
+ const sample = shuffled.slice(0, sampleSize)
682
+
683
+ // 3. validate each item
684
+ const errors: ValidationError[] = []
685
+
686
+ for (const item of sample) {
687
+ const id = item[idKey]
688
+ const rawResult = yield* store.find(id).pipe(
689
+ Effect.withSpan("Repository.find", {
690
+ kind: "client",
691
+ attributes: { "app.entity": name, "app.entity.id": id }
692
+ }, { captureStackTrace: false })
693
+ )
694
+
695
+ if (Option.isNone(rawResult)) continue
696
+
697
+ const rawData = rawResult.value as Encoded
698
+ const jitMResult = mapFrom(rawData) // apply jitM
699
+
700
+ const decodeResult = yield* S.decodeEffectConcurrently(schema)(jitMResult).pipe(
701
+ Effect.result,
702
+ provideRctx
703
+ )
704
+
705
+ if (Result.isFailure(decodeResult)) {
706
+ errors.push(
707
+ ValidationError.make({
708
+ id,
709
+ rawData,
710
+ jitMResult,
711
+ error: decodeResult.failure
712
+ })
713
+ )
714
+ }
715
+ }
716
+
717
+ return ValidationResult.make({
718
+ total: NonNegativeInt(allIds.length),
719
+ sampled: NonNegativeInt(sample.length),
720
+ valid: NonNegativeInt(sample.length - errors.length),
721
+ errors
722
+ })
723
+ }
724
+ )
725
+
726
+ const r = {
727
+ changeFeed,
728
+ itemType: name,
729
+ idKey,
730
+ find,
731
+ all,
732
+ saveAndPublish,
733
+ removeAndPublish,
734
+ removeById,
735
+ seedNamespace: (namespace: string) => store.seedNamespace(namespace),
736
+ validateSample,
737
+ queryRaw<A, Out, QR>(schema: S.Codec<A, Out, QR>, q: Q.RawQuery<Encoded, Out>) {
738
+ return store.queryRaw(q).pipe(
739
+ Effect.flatMap((items) =>
740
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(schema)))(items as readonly S.Json[]).pipe(
741
+ timeSchema("decode", name, undefined, items.length)
742
+ )
743
+ ),
744
+ Effect.tap(() => recordRead),
745
+ Effect.withSpan("Repository.queryRaw", {
746
+ kind: "client",
747
+ attributes: { "app.entity": name }
748
+ }, { captureStackTrace: false })
749
+ )
750
+ },
751
+ query(q: any) {
752
+ // eslint-disable-next-line prefer-rest-params
753
+ return query(typeof q === "function" ? Pipeable.pipeArguments(Q.make(), arguments) : q) as any
754
+ },
755
+ /**
756
+ * @internal
757
+ */
758
+ mapped: <A, R>(schema: S.Codec<A, any, R>) => {
759
+ const dec = S.decodeEffectConcurrently(schema)
760
+ const encMany = S.encodeEffect(S.Array(schema))
761
+ const decMany = S.decodeEffectConcurrently(S.Array(schema))
762
+ const spanAttrs = { kind: "client" as const, attributes: { "app.entity": name } }
763
+ return {
764
+ all: allE.pipe(
765
+ Effect.flatMap((items) =>
766
+ decMany(items).pipe(
767
+ timeSchema("decode", name, undefined, items.length)
768
+ )
769
+ ),
770
+ Effect.tap(() => recordRead),
771
+ Effect.map((_) => _ as any[]),
772
+ Effect.withSpan("Repository.mapped.all", spanAttrs, { captureStackTrace: false })
773
+ ),
774
+ find: (id: T[IdKey]) =>
775
+ flatMapOption(findE(id), dec).pipe(
776
+ Effect.tap(() => recordEntityRead(id)),
777
+ Effect.withSpan("Repository.mapped.find", {
778
+ ...spanAttrs,
779
+ attributes: { ...spanAttrs.attributes, "app.entity.id": id }
780
+ }, { captureStackTrace: false })
781
+ ),
782
+ // query: (q: any) => {
783
+ // const a = Q.toFilter(q)
784
+
785
+ // return filter(a)
786
+ // .pipe(
787
+ // Effect.flatMap(decMany),
788
+ // Effect.map((_) => _ as any[]),
789
+ // Effect.withSpan("Repository.mapped.query [effect-app/infra]", {
790
+ // captureStackTrace: false,
791
+ // attributes: {
792
+ // "repository.model_name": name,
793
+ // query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter.build() }
794
+ // }
795
+ // })
796
+ // )
797
+ // },
798
+ save: (...xes: any[]) =>
799
+ Effect
800
+ .flatMap(
801
+ encMany(xes).pipe(
802
+ timeSchema("encode", name, undefined, xes.length, entityStateFromItems(xes))
803
+ ),
804
+ (_) => saveAllE(_)
805
+ )
806
+ .pipe(
807
+ Effect.tap(() =>
808
+ Array.isReadonlyArrayNonEmpty(xes)
809
+ ? recordEntityWrite([xes[0][idKey], ...xes.slice(1).map((_) => _[idKey])])
810
+ : Effect.void
811
+ ),
812
+ Effect.withSpan("Repository.mapped.save", spanAttrs, { captureStackTrace: false })
813
+ )
814
+ }
815
+ }
816
+ }
817
+ return r as Repository<T, Encoded, Evt, ItemType, IdKey, Exclude<R, RCtx>, RPublish, RCtx>
818
+ })
819
+ .pipe(Effect
820
+ // .withSpan("Repository.make [effect-app/infra]", { attributes: { "repository.model_name": name } })
821
+ .withLogSpan("Repository.make: " + name))
822
+ }
823
+
824
+ return {
825
+ make,
826
+ Q: Q.make<Encoded>()
827
+ }
828
+ }
829
+ }
830
+
831
+ const pluralize = (s: string) =>
832
+ s.endsWith("s")
833
+ ? s + "es"
834
+ : s.endsWith("y")
835
+ ? s.substring(0, s.length - 1) + "ies"
836
+ : s + "s"
837
+
838
+ export function makeStore<Encoded extends FieldValues>() {
839
+ return <
840
+ ItemType extends string,
841
+ R,
842
+ E,
843
+ T,
844
+ IdKey extends keyof Encoded
845
+ >(
846
+ name: ItemType,
847
+ schema: S.Codec<T, E, R>,
848
+ mapTo: (e: E, etag: string | undefined) => Encoded,
849
+ idKey: IdKey
850
+ ) => {
851
+ function makeStore<RInitial = never, EInitial = never>(
852
+ makeInitial?: Effect.Effect<readonly T[], EInitial, RInitial>,
853
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
854
+ partitionValue?: (e?: Encoded) => string
855
+ }
856
+ ) {
857
+ function encodeToEncoded() {
858
+ const getEtag = () => undefined
859
+ return (t: T) =>
860
+ S.encodeEffect(schema)(t).pipe(
861
+ Effect.orDie,
862
+ Effect.map((_) => mapToPersistenceModel(_, getEtag))
863
+ )
864
+ }
865
+
866
+ function mapToPersistenceModel(
867
+ e: E,
868
+ getEtag: (id: string) => string | undefined
869
+ ): Encoded {
870
+ return mapTo(e, getEtag((e as any)[idKey] as string))
871
+ }
872
+
873
+ return Effect.gen(function*() {
874
+ const { make } = yield* StoreMaker
875
+
876
+ const store = yield* make<IdKey, Encoded, RInitial | R, EInitial>(
877
+ pluralize(name),
878
+ idKey,
879
+ makeInitial
880
+ ? makeInitial
881
+ .pipe(
882
+ Effect.flatMap(Effect.forEach(encodeToEncoded())),
883
+ setupRequestContextFromCurrent("Repository.makeInitial [effect-app/infra]", {
884
+ attributes: { "app.entity": name }
885
+ })
886
+ )
887
+ : undefined,
888
+ {
889
+ ...config,
890
+ schema,
891
+ partitionValue: config?.partitionValue
892
+ ?? ((_) => "primary") /*(isIntegrationEvent(r) ? r.companyId : r.id*/
893
+ }
894
+ )
895
+
896
+ return store
897
+ })
898
+ }
899
+
900
+ return makeStore
901
+ }
902
+ }
903
+
904
+ export interface Repos<
905
+ T,
906
+ Encoded extends { id: string },
907
+ RSchema,
908
+ Evt,
909
+ ItemType extends string,
910
+ IdKey extends keyof T,
911
+ RPublish
912
+ > {
913
+ make<RInitial = never, E = never, R2 = never>(
914
+ args: [Evt] extends [never] ? {
915
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
916
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
917
+ partitionValue?: (e?: Encoded) => string
918
+ }
919
+ }
920
+ : {
921
+ publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, R2>
922
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
923
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
924
+ partitionValue?: (e?: Encoded) => string
925
+ }
926
+ }
927
+ ): Effect.Effect<
928
+ Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>,
929
+ E,
930
+ StoreMaker | RInitial | R2 | Scope.Scope
931
+ >
932
+ makeWith<Out, RInitial = never, E = never, R2 = never>(
933
+ args: [Evt] extends [never] ? {
934
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
935
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
936
+ partitionValue?: (e?: Encoded) => string
937
+ }
938
+ }
939
+ : {
940
+ publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, R2>
941
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
942
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
943
+ partitionValue?: (e?: Encoded) => string
944
+ }
945
+ },
946
+ f: (r: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>) => Out
947
+ ): Effect.Effect<Out, E, StoreMaker | RInitial | R2 | Scope.Scope>
948
+ readonly Q: ReturnType<typeof Q.make<Encoded>>
949
+ readonly type: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>
950
+ }