@effect-app/infra 4.0.0-beta.22 → 4.0.0-beta.220

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 (310) hide show
  1. package/CHANGELOG.md +1640 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +7 -7
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +10 -12
  6. package/dist/Emailer/Sendgrid.d.ts +14 -14
  7. package/dist/Emailer/Sendgrid.d.ts.map +1 -1
  8. package/dist/Emailer/Sendgrid.js +16 -15
  9. package/dist/Emailer/fake.d.ts +1 -1
  10. package/dist/Emailer/service.d.ts +10 -4
  11. package/dist/Emailer/service.d.ts.map +1 -1
  12. package/dist/Emailer/service.js +3 -3
  13. package/dist/Emailer.d.ts +1 -1
  14. package/dist/MainFiberSet.d.ts +9 -9
  15. package/dist/MainFiberSet.d.ts.map +1 -1
  16. package/dist/MainFiberSet.js +3 -3
  17. package/dist/Model/Repository/Registry.d.ts +20 -0
  18. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  19. package/dist/Model/Repository/Registry.js +17 -0
  20. package/dist/Model/Repository/ext.d.ts +33 -15
  21. package/dist/Model/Repository/ext.d.ts.map +1 -1
  22. package/dist/Model/Repository/ext.js +54 -2
  23. package/dist/Model/Repository/internal/internal.d.ts +6 -6
  24. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  25. package/dist/Model/Repository/internal/internal.js +103 -51
  26. package/dist/Model/Repository/legacy.d.ts +1 -1
  27. package/dist/Model/Repository/makeRepo.d.ts +7 -6
  28. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  29. package/dist/Model/Repository/makeRepo.js +5 -1
  30. package/dist/Model/Repository/service.d.ts +28 -23
  31. package/dist/Model/Repository/service.d.ts.map +1 -1
  32. package/dist/Model/Repository/validation.d.ts +46 -17
  33. package/dist/Model/Repository/validation.d.ts.map +1 -1
  34. package/dist/Model/Repository/validation.js +5 -5
  35. package/dist/Model/Repository.d.ts +2 -1
  36. package/dist/Model/Repository.d.ts.map +1 -1
  37. package/dist/Model/Repository.js +2 -1
  38. package/dist/Model/dsl.d.ts +4 -4
  39. package/dist/Model/dsl.d.ts.map +1 -1
  40. package/dist/Model/filter/filterApi.d.ts +5 -5
  41. package/dist/Model/filter/filterApi.d.ts.map +1 -1
  42. package/dist/Model/filter/types/errors.d.ts +1 -1
  43. package/dist/Model/filter/types/fields.d.ts +1 -1
  44. package/dist/Model/filter/types/path/common.d.ts +1 -1
  45. package/dist/Model/filter/types/path/eager.d.ts +1 -1
  46. package/dist/Model/filter/types/path/eager.d.ts.map +1 -1
  47. package/dist/Model/filter/types/path/eager.js +1 -1
  48. package/dist/Model/filter/types/path/index.d.ts +1 -1
  49. package/dist/Model/filter/types/utils.d.ts +1 -1
  50. package/dist/Model/filter/types/validator.d.ts +1 -1
  51. package/dist/Model/filter/types.d.ts +1 -1
  52. package/dist/Model/query/dsl.d.ts +139 -16
  53. package/dist/Model/query/dsl.d.ts.map +1 -1
  54. package/dist/Model/query/dsl.js +187 -1
  55. package/dist/Model/query/new-kid-interpreter.d.ts +76 -7
  56. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  57. package/dist/Model/query/new-kid-interpreter.js +122 -6
  58. package/dist/Model/query.d.ts +1 -1
  59. package/dist/Model.d.ts +2 -1
  60. package/dist/Model.d.ts.map +1 -1
  61. package/dist/Model.js +2 -1
  62. package/dist/QueueMaker/SQLQueue.d.ts +5 -7
  63. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  64. package/dist/QueueMaker/SQLQueue.js +130 -116
  65. package/dist/QueueMaker/errors.d.ts +2 -2
  66. package/dist/QueueMaker/errors.d.ts.map +1 -1
  67. package/dist/QueueMaker/memQueue.d.ts +7 -4
  68. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  69. package/dist/QueueMaker/memQueue.js +75 -63
  70. package/dist/QueueMaker/sbqueue.d.ts +6 -3
  71. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  72. package/dist/QueueMaker/sbqueue.js +52 -53
  73. package/dist/QueueMaker/service.d.ts +1 -1
  74. package/dist/RequestContext.d.ts +74 -35
  75. package/dist/RequestContext.d.ts.map +1 -1
  76. package/dist/RequestContext.js +13 -14
  77. package/dist/RequestFiberSet.d.ts +7 -7
  78. package/dist/RequestFiberSet.d.ts.map +1 -1
  79. package/dist/RequestFiberSet.js +3 -3
  80. package/dist/Store/ContextMapContainer.d.ts +19 -3
  81. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  82. package/dist/Store/ContextMapContainer.js +13 -3
  83. package/dist/Store/Cosmos/query.d.ts +5 -1
  84. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  85. package/dist/Store/Cosmos/query.js +113 -34
  86. package/dist/Store/Cosmos.d.ts +1 -1
  87. package/dist/Store/Cosmos.d.ts.map +1 -1
  88. package/dist/Store/Cosmos.js +335 -243
  89. package/dist/Store/Disk.d.ts +2 -2
  90. package/dist/Store/Disk.d.ts.map +1 -1
  91. package/dist/Store/Disk.js +72 -35
  92. package/dist/Store/Memory.d.ts +6 -4
  93. package/dist/Store/Memory.d.ts.map +1 -1
  94. package/dist/Store/Memory.js +242 -58
  95. package/dist/Store/SQL/Pg.d.ts +4 -0
  96. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  97. package/dist/Store/SQL/Pg.js +231 -0
  98. package/dist/Store/SQL/query.d.ts +42 -0
  99. package/dist/Store/SQL/query.d.ts.map +1 -0
  100. package/dist/Store/SQL/query.js +479 -0
  101. package/dist/Store/SQL.d.ts +20 -0
  102. package/dist/Store/SQL.d.ts.map +1 -0
  103. package/dist/Store/SQL.js +446 -0
  104. package/dist/Store/codeFilter.d.ts +1 -1
  105. package/dist/Store/codeFilter.d.ts.map +1 -1
  106. package/dist/Store/codeFilter.js +4 -2
  107. package/dist/Store/index.d.ts +5 -2
  108. package/dist/Store/index.d.ts.map +1 -1
  109. package/dist/Store/index.js +15 -3
  110. package/dist/Store/service.d.ts +22 -8
  111. package/dist/Store/service.d.ts.map +1 -1
  112. package/dist/Store/service.js +24 -6
  113. package/dist/Store/utils.d.ts +1 -1
  114. package/dist/Store/utils.d.ts.map +1 -1
  115. package/dist/Store/utils.js +3 -4
  116. package/dist/Store.d.ts +1 -1
  117. package/dist/adapters/SQL/Model.d.ts +31 -42
  118. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  119. package/dist/adapters/SQL/Model.js +29 -38
  120. package/dist/adapters/SQL.d.ts +1 -1
  121. package/dist/adapters/ServiceBus.d.ts +11 -11
  122. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  123. package/dist/adapters/ServiceBus.js +25 -21
  124. package/dist/adapters/cosmos-client.d.ts +3 -3
  125. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  126. package/dist/adapters/cosmos-client.js +3 -3
  127. package/dist/adapters/index.d.ts +8 -2
  128. package/dist/adapters/index.d.ts.map +1 -1
  129. package/dist/adapters/index.js +8 -2
  130. package/dist/adapters/logger.d.ts +1 -1
  131. package/dist/adapters/logger.d.ts.map +1 -1
  132. package/dist/adapters/memQueue.d.ts +3 -3
  133. package/dist/adapters/memQueue.d.ts.map +1 -1
  134. package/dist/adapters/memQueue.js +3 -3
  135. package/dist/adapters/mongo-client.d.ts +3 -3
  136. package/dist/adapters/mongo-client.d.ts.map +1 -1
  137. package/dist/adapters/mongo-client.js +3 -3
  138. package/dist/adapters/redis-client.d.ts +3 -3
  139. package/dist/adapters/redis-client.d.ts.map +1 -1
  140. package/dist/adapters/redis-client.js +3 -3
  141. package/dist/api/ContextProvider.d.ts +8 -8
  142. package/dist/api/ContextProvider.d.ts.map +1 -1
  143. package/dist/api/ContextProvider.js +6 -6
  144. package/dist/api/codec.d.ts +1 -1
  145. package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
  146. package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
  147. package/dist/api/internal/RequestContextMiddleware.js +9 -6
  148. package/dist/api/internal/auth.d.ts +44 -6
  149. package/dist/api/internal/auth.d.ts.map +1 -1
  150. package/dist/api/internal/auth.js +160 -29
  151. package/dist/api/internal/events.d.ts +3 -3
  152. package/dist/api/internal/events.d.ts.map +1 -1
  153. package/dist/api/internal/events.js +10 -8
  154. package/dist/api/internal/health.d.ts +1 -1
  155. package/dist/api/layerUtils.d.ts +6 -6
  156. package/dist/api/layerUtils.d.ts.map +1 -1
  157. package/dist/api/layerUtils.js +5 -5
  158. package/dist/api/middlewares.d.ts +1 -1
  159. package/dist/api/reportError.d.ts +1 -1
  160. package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
  161. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  162. package/dist/api/routing/middleware/middleware.d.ts +39 -3
  163. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  164. package/dist/api/routing/middleware/middleware.js +48 -16
  165. package/dist/api/routing/middleware.d.ts +1 -2
  166. package/dist/api/routing/middleware.d.ts.map +1 -1
  167. package/dist/api/routing/middleware.js +1 -2
  168. package/dist/api/routing/schema/jwt.d.ts +1 -1
  169. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  170. package/dist/api/routing/tsort.d.ts +1 -1
  171. package/dist/api/routing/tsort.d.ts.map +1 -1
  172. package/dist/api/routing/utils.d.ts +3 -3
  173. package/dist/api/routing/utils.d.ts.map +1 -1
  174. package/dist/api/routing.d.ts +80 -37
  175. package/dist/api/routing.d.ts.map +1 -1
  176. package/dist/api/routing.js +109 -41
  177. package/dist/api/setupRequest.d.ts +8 -5
  178. package/dist/api/setupRequest.d.ts.map +1 -1
  179. package/dist/api/setupRequest.js +12 -7
  180. package/dist/api/util.d.ts +1 -1
  181. package/dist/arbs.d.ts +1 -1
  182. package/dist/arbs.d.ts.map +1 -1
  183. package/dist/arbs.js +5 -3
  184. package/dist/errorReporter.d.ts +4 -4
  185. package/dist/errorReporter.d.ts.map +1 -1
  186. package/dist/errorReporter.js +20 -25
  187. package/dist/errors.d.ts +1 -1
  188. package/dist/fileUtil.d.ts +1 -1
  189. package/dist/fileUtil.d.ts.map +1 -1
  190. package/dist/index.d.ts +1 -1
  191. package/dist/logger/jsonLogger.d.ts +1 -1
  192. package/dist/logger/logFmtLogger.d.ts +1 -1
  193. package/dist/logger/shared.d.ts +1 -1
  194. package/dist/logger/shared.js +2 -2
  195. package/dist/logger.d.ts +1 -1
  196. package/dist/logger.d.ts.map +1 -1
  197. package/dist/otel.d.ts +75 -0
  198. package/dist/otel.d.ts.map +1 -0
  199. package/dist/otel.js +65 -0
  200. package/dist/rateLimit.d.ts +9 -3
  201. package/dist/rateLimit.d.ts.map +1 -1
  202. package/dist/rateLimit.js +5 -11
  203. package/dist/test.d.ts +2 -2
  204. package/dist/test.d.ts.map +1 -1
  205. package/dist/test.js +1 -1
  206. package/dist/vitest.d.ts +1 -1
  207. package/examples/query.ts +42 -38
  208. package/package.json +46 -37
  209. package/src/CUPS.ts +9 -11
  210. package/src/Emailer/Sendgrid.ts +17 -14
  211. package/src/Emailer/service.ts +9 -3
  212. package/src/MainFiberSet.ts +5 -6
  213. package/src/Model/Repository/Registry.ts +33 -0
  214. package/src/Model/Repository/ext.ts +96 -10
  215. package/src/Model/Repository/internal/internal.ts +218 -149
  216. package/src/Model/Repository/makeRepo.ts +12 -10
  217. package/src/Model/Repository/service.ts +31 -22
  218. package/src/Model/Repository/validation.ts +4 -4
  219. package/src/Model/Repository.ts +1 -0
  220. package/src/Model/dsl.ts +3 -3
  221. package/src/Model/filter/types/path/eager.ts +1 -2
  222. package/src/Model/query/dsl.ts +348 -18
  223. package/src/Model/query/new-kid-interpreter.ts +206 -6
  224. package/src/Model.ts +1 -0
  225. package/src/QueueMaker/SQLQueue.ts +144 -152
  226. package/src/QueueMaker/memQueue.ts +104 -103
  227. package/src/QueueMaker/sbqueue.ts +70 -86
  228. package/src/RequestContext.ts +14 -16
  229. package/src/RequestFiberSet.ts +2 -2
  230. package/src/Store/ContextMapContainer.ts +41 -2
  231. package/src/Store/Cosmos/query.ts +140 -43
  232. package/src/Store/Cosmos.ts +482 -349
  233. package/src/Store/Disk.ts +102 -65
  234. package/src/Store/Memory.ts +275 -87
  235. package/src/Store/SQL/Pg.ts +361 -0
  236. package/src/Store/SQL/query.ts +539 -0
  237. package/src/Store/SQL.ts +731 -0
  238. package/src/Store/codeFilter.ts +3 -1
  239. package/src/Store/index.ts +17 -2
  240. package/src/Store/service.ts +41 -10
  241. package/src/Store/utils.ts +23 -22
  242. package/src/adapters/SQL/Model.ts +41 -40
  243. package/src/adapters/ServiceBus.ts +125 -121
  244. package/src/adapters/cosmos-client.ts +2 -2
  245. package/src/adapters/index.ts +7 -0
  246. package/src/adapters/memQueue.ts +2 -2
  247. package/src/adapters/mongo-client.ts +2 -2
  248. package/src/adapters/redis-client.ts +2 -2
  249. package/src/api/ContextProvider.ts +12 -13
  250. package/src/api/internal/RequestContextMiddleware.ts +15 -5
  251. package/src/api/internal/auth.ts +246 -44
  252. package/src/api/internal/events.ts +13 -9
  253. package/src/api/layerUtils.ts +8 -8
  254. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  255. package/src/api/routing/middleware/middleware.ts +55 -14
  256. package/src/api/routing/middleware.ts +0 -2
  257. package/src/api/routing.ts +296 -131
  258. package/src/api/setupRequest.ts +28 -8
  259. package/src/arbs.ts +4 -2
  260. package/src/errorReporter.ts +62 -74
  261. package/src/logger/shared.ts +1 -1
  262. package/src/otel.ts +152 -0
  263. package/src/rateLimit.ts +30 -22
  264. package/src/test.ts +1 -1
  265. package/test/auth.test.ts +101 -0
  266. package/test/contextProvider.test.ts +11 -11
  267. package/test/controller.test.ts +21 -30
  268. package/test/dist/auth.test.d.ts.map +1 -0
  269. package/test/dist/contextProvider.test.d.ts.map +1 -1
  270. package/test/dist/controller.test.d.ts.map +1 -1
  271. package/test/dist/date-query.test.d.ts.map +1 -0
  272. package/test/dist/fixtures.d.ts +26 -12
  273. package/test/dist/fixtures.d.ts.map +1 -1
  274. package/test/dist/fixtures.js +12 -10
  275. package/test/dist/query.test.d.ts.map +1 -1
  276. package/test/dist/rawQuery.test.d.ts.map +1 -1
  277. package/test/dist/repository-ext.test.d.ts.map +1 -0
  278. package/test/dist/requires.test.d.ts.map +1 -1
  279. package/test/dist/router-generator.test.d.ts.map +1 -0
  280. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  281. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  282. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  283. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  284. package/test/dist/sql-store.test.d.ts.map +1 -0
  285. package/test/fixtures.ts +11 -9
  286. package/test/query.test.ts +813 -38
  287. package/test/rawQuery.test.ts +301 -20
  288. package/test/repository-ext.test.ts +60 -0
  289. package/test/requires.test.ts +6 -6
  290. package/test/router-generator.test.ts +183 -0
  291. package/test/routing-interruptibility.test.ts +63 -0
  292. package/test/rpc-e2e-invalidation.test.ts +251 -0
  293. package/test/rpc-multi-middleware.test.ts +78 -9
  294. package/test/rpc-stream-fullstack.test.ts +300 -0
  295. package/test/sql-store.test.ts +1592 -0
  296. package/test/validateSample.test.ts +15 -12
  297. package/tsconfig.examples.json +1 -1
  298. package/tsconfig.json +0 -1
  299. package/tsconfig.json.bak +2 -2
  300. package/tsconfig.src.json +35 -35
  301. package/tsconfig.test.json +2 -2
  302. package/dist/Operations.d.ts +0 -55
  303. package/dist/Operations.d.ts.map +0 -1
  304. package/dist/Operations.js +0 -102
  305. package/dist/OperationsRepo.d.ts +0 -41
  306. package/dist/OperationsRepo.d.ts.map +0 -1
  307. package/dist/OperationsRepo.js +0 -14
  308. package/eslint.config.mjs +0 -24
  309. package/src/Operations.ts +0 -235
  310. package/src/OperationsRepo.ts +0 -16
@@ -1,9 +1,18 @@
1
- import { Effect, Layer, Tracer } from "effect-app"
1
+ import { Effect, Layer, Option, Tracer } from "effect-app"
2
2
  import { NonEmptyString255 } from "effect-app/Schema"
3
+ import { SqlClient } from "effect/unstable/sql"
3
4
  import { LocaleRef, RequestContext, spanAttributes } from "../RequestContext.js"
4
5
  import { ContextMapContainer } from "../Store/ContextMapContainer.js"
5
6
  import { storeId } from "../Store/Memory.js"
6
7
 
8
+ const withSqlTransaction = <R, E, A>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
9
+ Effect.serviceOption(SqlClient.SqlClient).pipe(
10
+ Effect.flatMap(Option.match({
11
+ onNone: () => self,
12
+ onSome: (sql) => sql.withTransaction(self).pipe(Effect.orDie)
13
+ }))
14
+ )
15
+
7
16
  export const getRequestContext = Effect
8
17
  .all({
9
18
  span: Effect.currentSpan.pipe(Effect.orDie),
@@ -12,7 +21,7 @@ export const getRequestContext = Effect
12
21
  })
13
22
  .pipe(
14
23
  Effect.map(({ locale, namespace, span }) =>
15
- new RequestContext({
24
+ RequestContext.make({
16
25
  span: Tracer.externalSpan(span),
17
26
  locale,
18
27
  namespace,
@@ -43,16 +52,25 @@ const withRequestSpan = (name = "request", options?: Tracer.SpanOptions) => <R,
43
52
  )
44
53
  )
45
54
 
55
+ export interface SetupRequestOptions {
56
+ readonly withTransaction?: boolean
57
+ }
58
+
46
59
  export const setupRequestContextFromCurrent =
47
- (name = "request", options?: Tracer.SpanOptions) => <R, E, A>(self: Effect.Effect<A, E, R>) =>
60
+ (name = "request", options?: Tracer.SpanOptions & SetupRequestOptions) => <R, E, A>(self: Effect.Effect<A, E, R>) =>
48
61
  self
49
62
  .pipe(
63
+ options?.withTransaction === true ? withSqlTransaction : (_) => _,
50
64
  withRequestSpan(name, options),
51
- Effect.provide(ContextMapContainer.layer)
65
+ Effect.provide(ContextMapContainer.layer, { local: true })
52
66
  )
53
67
 
54
68
  // TODO: consider integrating Effect.withParentSpan
55
- export function setupRequestContext<R, E, A>(self: Effect.Effect<A, E, R>, requestContext: RequestContext) {
69
+ export function setupRequestContext<R, E, A>(
70
+ self: Effect.Effect<A, E, R>,
71
+ requestContext: RequestContext,
72
+ options?: SetupRequestOptions
73
+ ) {
56
74
  const layer = Layer.mergeAll(
57
75
  ContextMapContainer.layer,
58
76
  Layer.succeed(LocaleRef, requestContext.locale),
@@ -60,8 +78,9 @@ export function setupRequestContext<R, E, A>(self: Effect.Effect<A, E, R>, reque
60
78
  )
61
79
  return self
62
80
  .pipe(
81
+ options?.withTransaction === true ? withSqlTransaction : (_) => _,
63
82
  withRequestSpan(requestContext.name),
64
- Effect.provide(layer)
83
+ Effect.provide(layer, { local: true })
65
84
  )
66
85
  }
67
86
 
@@ -69,7 +88,7 @@ export function setupRequestContextWithCustomSpan<R, E, A>(
69
88
  self: Effect.Effect<A, E, R>,
70
89
  requestContext: RequestContext,
71
90
  name: string,
72
- options?: Tracer.SpanOptions
91
+ options?: Tracer.SpanOptions & SetupRequestOptions
73
92
  ) {
74
93
  const layer = Layer.mergeAll(
75
94
  ContextMapContainer.layer,
@@ -78,7 +97,8 @@ export function setupRequestContextWithCustomSpan<R, E, A>(
78
97
  )
79
98
  return self
80
99
  .pipe(
100
+ options?.withTransaction === true ? withSqlTransaction : (_) => _,
81
101
  withRequestSpan(name, options),
82
- Effect.provide(layer)
102
+ Effect.provide(layer, { local: true })
83
103
  )
84
104
  }
package/src/arbs.ts CHANGED
@@ -5,9 +5,11 @@ import { type S } from "effect-app"
5
5
  import { setFaker } from "effect-app/faker"
6
6
  import * as FastCheck from "effect/testing/FastCheck"
7
7
  import { Random } from "fast-check"
8
- import * as rand from "pure-rand"
8
+ import { congruential32 } from "pure-rand/generator/congruential32"
9
9
 
10
- const rnd = new Random(rand.congruential32(5))
10
+ const seed = 5
11
+ const rng = congruential32(seed)
12
+ const rnd = new Random(rng)
11
13
 
12
14
  setFaker(faker)
13
15
 
@@ -13,47 +13,41 @@ const tryCauseException = <E>(cause: Cause.Cause<E>, name: string): CauseExcepti
13
13
  }
14
14
  }
15
15
 
16
- export function reportError(
17
- name: string
18
- ) {
19
- return (
20
- cause: Cause.Cause<unknown>,
21
- extras?: Record<string, unknown>,
22
- level: LogLevel.Severity = "Error"
23
- ) =>
24
- Effect
25
- .gen(function*() {
26
- if (Cause.hasInterruptsOnly(cause)) {
27
- yield* InfraLogger.logDebug("Interrupted").pipe(Effect.annotateLogs("extras", JSON.stringify(extras ?? {})))
28
- return
29
- }
30
- const error = tryCauseException(cause, name)
31
-
32
- yield* reportSentry(error, extras, LogLevelToSentry(level))
33
- yield* InfraLogger
34
- .logWithLevel(level, "Reporting error", cause)
35
- .pipe(
36
- Effect.annotateLogs(dropUndefined({
37
- extras,
38
- error: tryToReport(error),
39
- cause: tryToJson(cause),
40
- __error_name__: name
41
- }))
42
- )
43
- .pipe(
44
- Effect.catchCause((cause) => InfraLogger.logWarning("Failed to log error", cause)),
45
- Effect.catchCause(() => InfraLogger.logFatal("Failed to log error cause"))
46
- )
16
+ export function reportError(name: string) {
17
+ return Effect.fnUntraced(
18
+ function*(
19
+ cause: Cause.Cause<unknown>,
20
+ extras?: Record<string, unknown>,
21
+ level: LogLevel.Severity = "Error"
22
+ ) {
23
+ if (Cause.hasInterruptsOnly(cause)) {
24
+ yield* InfraLogger.logDebug("Interrupted").pipe(Effect.annotateLogs("extras", JSON.stringify(extras ?? {})))
25
+ return
26
+ }
27
+ const error = tryCauseException(cause, name)
47
28
 
48
- return error
49
- })
50
- .pipe(
51
- Effect.tapCause((cause) =>
52
- InfraLogger.logError("Failed to report error", cause).pipe(
53
- Effect.tapCause(() => InfraLogger.logFatal("Failed to log error cause"))
54
- )
29
+ yield* reportSentry(error, extras, LogLevelToSentry(level))
30
+ yield* InfraLogger
31
+ .logWithLevel(level, "Reporting error", cause)
32
+ .pipe(
33
+ Effect.annotateLogs(dropUndefined({
34
+ extras,
35
+ error: tryToReport(error),
36
+ cause: tryToJson(cause),
37
+ __error_name__: name
38
+ })),
39
+ Effect.catchCause((cause) => InfraLogger.logWarning("Failed to log error", cause)),
40
+ Effect.catchCause(() => InfraLogger.logFatal("Failed to log error cause"))
55
41
  )
56
- )
42
+
43
+ return error
44
+ },
45
+ (effect) =>
46
+ Effect.tapCause(effect, (cause) =>
47
+ InfraLogger.logError("Failed to report error", cause).pipe(
48
+ Effect.tapCause(() => InfraLogger.logFatal("Failed to log error cause"))
49
+ ))
50
+ )
57
51
  }
58
52
 
59
53
  function reportSentry(
@@ -66,45 +60,39 @@ function reportSentry(
66
60
  scope.setLevel(level)
67
61
  if (context) scope.setContext("context", { ...context })
68
62
  if (extras) scope.setContext("extras", extras)
69
- scope.setContext("error", { data: tryToReport(error) })
70
- scope.setContext("cause", { data: tryToJson(error.originalCause) })
63
+ const squashed = Cause.squash(error.originalCause)
64
+ scope.setContext("mainError", tryToJson(squashed))
65
+ scope.setContext("error", tryToReport(error))
66
+ scope.setContext("cause", tryToJson(error.originalCause))
71
67
  Sentry.captureException(error, scope)
72
68
  }))
73
69
  }
74
70
 
75
- export function logError<E>(
76
- name: string
77
- ) {
78
- return (cause: Cause.Cause<E>, extras?: Record<string, unknown>) =>
79
- Effect
80
- .gen(function*() {
81
- if (Cause.hasInterruptsOnly(cause)) {
82
- yield* InfraLogger.logDebug("Interrupted").pipe(Effect.annotateLogs(dropUndefined({ extras })))
83
- return
84
- }
85
- yield* InfraLogger
86
- .logWarning("Logging error", cause)
87
- .pipe(
88
- Effect.annotateLogs(dropUndefined({
89
- extras,
90
- cause: tryToJson(cause),
91
- __error_name__: name
92
- }))
93
- )
94
- })
95
- .pipe(
96
- Effect.tapCause(() => InfraLogger.logFatal("Failed to log error cause"))
97
- )
71
+ export function logError<E>(name: string) {
72
+ return Effect.fnUntraced(
73
+ function*(cause: Cause.Cause<E>, extras?: Record<string, unknown>) {
74
+ if (Cause.hasInterruptsOnly(cause)) {
75
+ yield* InfraLogger.logDebug("Interrupted").pipe(Effect.annotateLogs(dropUndefined({ extras })))
76
+ return
77
+ }
78
+ yield* InfraLogger
79
+ .logWarning("Logging error", cause)
80
+ .pipe(Effect.annotateLogs(dropUndefined({
81
+ extras,
82
+ cause: tryToJson(cause),
83
+ __error_name__: name
84
+ })))
85
+ },
86
+ (effect) => Effect.tapCause(effect, () => InfraLogger.logFatal("Failed to log error cause"))
87
+ )
98
88
  }
99
89
 
100
- export function reportMessage(message: string, extras?: Record<string, unknown>) {
101
- return Effect.gen(function*() {
102
- const context = yield* getRC
103
- const scope = new Sentry.Scope()
104
- if (context) scope.setContext("context", { ...context })
105
- if (extras) scope.setContext("extras", extras)
106
- Sentry.captureMessage(message, scope)
90
+ export const reportMessage = Effect.fnUntraced(function*(message: string, extras?: Record<string, unknown>) {
91
+ const context = yield* getRC
92
+ const scope = new Sentry.Scope()
93
+ if (context) scope.setContext("context", { ...context })
94
+ if (extras) scope.setContext("extras", extras)
95
+ Sentry.captureMessage(message, scope)
107
96
 
108
- console.warn(message, extras)
109
- })
110
- }
97
+ console.warn(message, extras)
98
+ })
@@ -7,7 +7,7 @@ export function getRequestContextFromFiber(fiber: Fiber.Fiber<unknown, unknown>)
7
7
  const span = Option.fromNullishOr(fiber.currentSpan)
8
8
  const locale = fiber.getRef(LocaleRef)
9
9
  const namespace = fiber.getRef(storeId)
10
- return new RequestContext({
10
+ return RequestContext.make({
11
11
  span: Option.map(span, (s) => ({ spanId: s.spanId, traceId: s.traceId, sampled: s.sampled })).pipe(
12
12
  Option.getOrElse(() => ({ spanId: "bogus", sampled: true, traceId: "bogus" }))
13
13
  ),
package/src/otel.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * OpenTelemetry semantic-convention helpers for span attributes.
3
+ *
4
+ * Aligns repository / queue / cache adapters with stable OTel semconv keys so
5
+ * downstream collectors and dashboards work without per-adapter mappings.
6
+ *
7
+ * - Database: https://opentelemetry.io/docs/specs/semconv/database/
8
+ * - Messaging: https://opentelemetry.io/docs/specs/semconv/messaging/
9
+ * - Cosmos DB: https://opentelemetry.io/docs/specs/semconv/database/cosmosdb/
10
+ */
11
+
12
+ import { Effect } from "effect-app"
13
+
14
+ export type DbSystem =
15
+ | "postgresql"
16
+ | "sqlite"
17
+ | "cosmosdb"
18
+ | "mongodb"
19
+ | "redis"
20
+ | "other_sql"
21
+ | "memory"
22
+ | "disk"
23
+
24
+ export interface DbSpanOptions {
25
+ /** OTel `db.operation.name` (e.g. `find`, `all`, `filter`, `set`). */
26
+ readonly operation: string
27
+ readonly system: DbSystem
28
+ /** Logical collection / table / container name. */
29
+ readonly collection: string
30
+ /** Tenant / namespace / database name. */
31
+ readonly namespace?: string | undefined
32
+ /** Application-level entity / model name (custom: `app.entity`). */
33
+ readonly entity?: string | undefined
34
+ /** Sanitized / parameterized query text. Never include bound values. */
35
+ readonly query?: string | undefined
36
+ /** Optional fragments merged into final attributes (e.g. id, partition). */
37
+ readonly extra?: Record<string, unknown> | undefined
38
+ }
39
+
40
+ const dbAttributes = (a: DbSpanOptions): Record<string, unknown> => ({
41
+ "db.system.name": a.system,
42
+ "db.operation.name": a.operation,
43
+ "db.collection.name": a.collection,
44
+ ...(a.namespace !== undefined && { "db.namespace": a.namespace }),
45
+ ...(a.query !== undefined && { "db.query.text": a.query }),
46
+ ...(a.entity !== undefined && { "app.entity": a.entity }),
47
+ ...a.extra
48
+ })
49
+
50
+ /**
51
+ * Wrap an effect with an OTel-semconv database span.
52
+ *
53
+ * Span name follows the low-cardinality convention: `<operation> <collection>`.
54
+ */
55
+ export const withDbSpan = (a: DbSpanOptions) =>
56
+ Effect.withSpan(
57
+ `${a.operation} ${a.collection}`,
58
+ { attributes: dbAttributes(a), kind: "client" as const },
59
+ { captureStackTrace: false }
60
+ )
61
+
62
+ /**
63
+ * Annotate the current span with OTel-semconv database attributes.
64
+ *
65
+ * Use when the caller already owns the span (e.g. a repository) and the
66
+ * adapter should only contribute db.* semconv attrs without opening a child.
67
+ * Annotates before running so attrs persist even on failure.
68
+ * No-op if there is no current span.
69
+ */
70
+ export const annotateDb = (a: DbSpanOptions) => <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
71
+ Effect.flatMap(Effect.annotateCurrentSpan(dbAttributes(a)), () => self)
72
+
73
+ /** Annotate the current span with response metrics from a DB call. */
74
+ export const annotateDbResponse = (m: {
75
+ readonly returnedRows?: number | undefined
76
+ readonly responseBytes?: number | undefined
77
+ }) =>
78
+ Effect.annotateCurrentSpan({
79
+ ...(m.returnedRows !== undefined && { "db.response.returned_rows": m.returnedRows }),
80
+ ...(m.responseBytes !== undefined && { "db.response.body.size": m.responseBytes })
81
+ })
82
+
83
+ /** Cosmos-specific response annotations. */
84
+ export const annotateCosmosResponse = (m: {
85
+ readonly requestCharge?: number | undefined
86
+ readonly returnedRows?: number | undefined
87
+ readonly responseBytes?: number | undefined
88
+ readonly statusCode?: number | undefined
89
+ }) =>
90
+ Effect.annotateCurrentSpan({
91
+ ...(m.requestCharge !== undefined && { "azure.cosmosdb.operation.request_charge": m.requestCharge }),
92
+ ...(m.statusCode !== undefined && { "db.response.status_code": String(m.statusCode) }),
93
+ ...(m.returnedRows !== undefined && { "db.response.returned_rows": m.returnedRows }),
94
+ ...(m.responseBytes !== undefined && { "db.response.body.size": m.responseBytes })
95
+ })
96
+
97
+ export type MessagingSystem =
98
+ | "servicebus"
99
+ | "rabbitmq"
100
+ | "kafka"
101
+ | "memory"
102
+ | "sql"
103
+
104
+ export type MessagingOperation =
105
+ | "publish"
106
+ | "create"
107
+ | "receive"
108
+ | "process"
109
+ | "settle"
110
+
111
+ export interface MessagingSpanOptions {
112
+ readonly operation: MessagingOperation
113
+ readonly system: MessagingSystem
114
+ /** Queue / topic name. */
115
+ readonly destination: string
116
+ readonly messageId?: string | undefined
117
+ readonly conversationId?: string | undefined
118
+ readonly bodySize?: number | undefined
119
+ readonly extra?: Record<string, unknown> | undefined
120
+ }
121
+
122
+ const messagingAttributes = (a: MessagingSpanOptions): Record<string, unknown> => ({
123
+ "messaging.system": a.system,
124
+ "messaging.operation.name": a.operation,
125
+ "messaging.destination.name": a.destination,
126
+ ...(a.messageId !== undefined && { "messaging.message.id": a.messageId }),
127
+ ...(a.conversationId !== undefined && { "messaging.message.conversation_id": a.conversationId }),
128
+ ...(a.bodySize !== undefined && { "messaging.message.body.size": a.bodySize }),
129
+ ...a.extra
130
+ })
131
+
132
+ /** Wrap an effect with an OTel-semconv messaging span. */
133
+ export const withMessagingSpan = (
134
+ a: MessagingSpanOptions,
135
+ kind: "producer" | "consumer"
136
+ ) =>
137
+ Effect.withSpan(
138
+ `${a.operation} ${a.destination}`,
139
+ { kind, attributes: messagingAttributes(a) },
140
+ { captureStackTrace: false }
141
+ )
142
+
143
+ /** Build messaging span options without wrapping (for Effect.fn / setupRequestContextWithCustomSpan). */
144
+ export const messagingSpanArgs = (
145
+ a: MessagingSpanOptions,
146
+ kind: "producer" | "consumer"
147
+ ) =>
148
+ ({
149
+ name: `${a.operation} ${a.destination}`,
150
+ kind,
151
+ attributes: messagingAttributes(a)
152
+ }) as const
package/src/rateLimit.ts CHANGED
@@ -21,7 +21,9 @@
21
21
  // }
22
22
 
23
23
  import { Array, type Duration, Effect, type NonEmptyArray } from "effect-app"
24
+ import { dual } from "effect-app/Function"
24
25
  import type { Semaphore } from "effect/Semaphore"
26
+ import type { Concurrency } from "effect/Types"
25
27
 
26
28
  /**
27
29
  * Executes the specified effect, acquiring the specified number of permits
@@ -45,36 +47,42 @@ export function SEM_withPermitsDuration(permits: number, duration: Duration.Dura
45
47
  }
46
48
  }
47
49
 
48
- export function batchPar<R, E, A, R2, E2, A2, T>(
49
- n: number,
50
- forEachItem: (item: T, iWithinBatch: number, batchI: number) => Effect.Effect<A, E, R>,
51
- forEachBatch: (a: NonEmptyArray<A>, i: number) => Effect.Effect<A2, E2, R2>
52
- ) {
53
- return (items: Iterable<T>) =>
54
- Effect.forEach(
55
- Array.chunksOf(items, n),
56
- (_, i) =>
57
- Effect
58
- .forEach(_, (_, j) => forEachItem(_, j, i), { concurrency: "inherit" })
59
- .pipe(Effect.flatMap((_) => forEachBatch(_, i))),
60
- { concurrency: "inherit" }
61
- )
50
+ export interface BatchOptions {
51
+ readonly concurrency?: Concurrency | undefined
62
52
  }
63
53
 
64
- export function batch<R, E, A, R2, E2, A2, T>(
65
- n: number,
66
- forEachItem: (item: T, iWithinBatch: number, batchI: number) => Effect.Effect<A, E, R>,
67
- forEachBatch: (a: NonEmptyArray<A>, i: number) => Effect.Effect<A2, E2, R2>
68
- ) {
69
- return (items: Iterable<T>) =>
54
+ export const batch: {
55
+ <T, A, E, R, A2, E2, R2>(
56
+ n: number,
57
+ forEachItem: (item: T, iWithinBatch: number, batchI: number) => Effect.Effect<A, E, R>,
58
+ forEachBatch: (a: NonEmptyArray<A>, i: number) => Effect.Effect<A2, E2, R2>,
59
+ options?: BatchOptions
60
+ ): (items: Iterable<T>) => Effect.Effect<Array<A2>, E | E2, R | R2>
61
+ <T, A, E, R, A2, E2, R2>(
62
+ items: Iterable<T>,
63
+ n: number,
64
+ forEachItem: (item: T, iWithinBatch: number, batchI: number) => Effect.Effect<A, E, R>,
65
+ forEachBatch: (a: NonEmptyArray<A>, i: number) => Effect.Effect<A2, E2, R2>,
66
+ options?: BatchOptions
67
+ ): Effect.Effect<Array<A2>, E | E2, R | R2>
68
+ } = dual(
69
+ (args) => typeof args[0] !== "number",
70
+ <T, A, E, R, A2, E2, R2>(
71
+ items: Iterable<T>,
72
+ n: number,
73
+ forEachItem: (item: T, iWithinBatch: number, batchI: number) => Effect.Effect<A, E, R>,
74
+ forEachBatch: (a: NonEmptyArray<A>, i: number) => Effect.Effect<A2, E2, R2>,
75
+ options?: BatchOptions
76
+ ) =>
70
77
  Effect.forEach(
71
78
  Array.chunksOf(items, n),
72
79
  (_, i) =>
73
80
  Effect
74
81
  .forEach(_, (_, j) => forEachItem(_, j, i), { concurrency: "inherit" })
75
- .pipe(Effect.flatMap((_) => forEachBatch(_, i)))
82
+ .pipe(Effect.flatMap((_) => forEachBatch(_, i))),
83
+ { concurrency: options?.concurrency }
76
84
  )
77
- }
85
+ )
78
86
 
79
87
  // export function rateLimit(
80
88
  // n: number,
package/src/test.ts CHANGED
@@ -16,7 +16,7 @@ export const createRandomInstance = <A extends object, I, R>(s: S.Codec<A, I, R>
16
16
  /**
17
17
  * Like `createRandomInstance`, but takes encoded values rather than decoded ones.
18
18
  */
19
- export const createRandomInstanceI = <A extends object, I>(s: S.Codec<A, I, never> & { fields: S.Struct.Fields }) => {
19
+ export const createRandomInstanceI = <A extends object, I>(s: S.Codec<A, I> & { fields: S.Struct.Fields }) => {
20
20
  const gen = generate(S.toArbitrary(s))
21
21
  const encode = S.encodeSync(s)
22
22
  const decode = S.decodeSync(s)
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { Effect } from "effect-app"
3
+ import { HttpHeaders } from "effect-app/http"
4
+ import { SignJWT } from "jose"
5
+ import { checkJWTI, InvalidRequestError, InvalidTokenError, UnauthorizedError } from "../src/api/internal/auth.js"
6
+
7
+ const issuer = "https://issuer.example.com/"
8
+ const audience = "effect-app"
9
+ const secret = "test-secret-test-secret-test-secret"
10
+
11
+ const makeToken = () =>
12
+ new SignJWT({ scope: "read:all" })
13
+ .setProtectedHeader({ alg: "HS256", typ: "at+jwt" })
14
+ .setIssuer(issuer)
15
+ .setAudience(audience)
16
+ .setIssuedAt()
17
+ .setExpirationTime("10m")
18
+ .sign(new TextEncoder().encode(secret))
19
+
20
+ describe("checkJWTI", () => {
21
+ it.effect(
22
+ "validates a bearer token from headers",
23
+ Effect.fnUntraced(function*() {
24
+ const token = yield* Effect.promise(() => makeToken())
25
+
26
+ yield* checkJWTI({
27
+ audience,
28
+ issuer,
29
+ secret,
30
+ strict: true,
31
+ tokenSigningAlg: "HS256"
32
+ })(HttpHeaders.fromRecordUnsafe({ authorization: `Bearer ${token}` }))
33
+ })
34
+ )
35
+
36
+ it.effect(
37
+ "fails on malformed authorization headers",
38
+ Effect.fnUntraced(function*() {
39
+ const error = yield* Effect.flip(
40
+ checkJWTI({
41
+ audience,
42
+ issuer,
43
+ secret,
44
+ tokenSigningAlg: "HS256"
45
+ })(HttpHeaders.fromRecordUnsafe({ authorization: "Basic abc" }))
46
+ )
47
+
48
+ expect(error).toBeInstanceOf(InvalidRequestError)
49
+ expect(error.status).toBe(400)
50
+ })
51
+ )
52
+
53
+ it.effect(
54
+ "fails when the token is missing",
55
+ Effect.fnUntraced(function*() {
56
+ const error = yield* Effect.flip(
57
+ checkJWTI({
58
+ audience,
59
+ issuer,
60
+ secret,
61
+ tokenSigningAlg: "HS256"
62
+ })(HttpHeaders.empty)
63
+ )
64
+
65
+ expect(error).toBeInstanceOf(UnauthorizedError)
66
+ expect(error.status).toBe(401)
67
+ })
68
+ )
69
+
70
+ it.effect(
71
+ "allows missing tokens when auth is optional",
72
+ Effect.fnUntraced(function*() {
73
+ yield* checkJWTI({
74
+ audience,
75
+ authRequired: false,
76
+ issuer,
77
+ secret,
78
+ tokenSigningAlg: "HS256"
79
+ })(HttpHeaders.empty)
80
+ })
81
+ )
82
+
83
+ it.effect(
84
+ "fails when the token signature is invalid",
85
+ Effect.fnUntraced(function*() {
86
+ const token = yield* Effect.promise(() => makeToken())
87
+
88
+ const error = yield* Effect.flip(
89
+ checkJWTI({
90
+ audience,
91
+ issuer,
92
+ secret: "wrong-secret-wrong-secret-wrong-secret",
93
+ tokenSigningAlg: "HS256"
94
+ })(HttpHeaders.fromRecordUnsafe({ authorization: `Bearer ${token}` }))
95
+ )
96
+
97
+ expect(error).toBeInstanceOf(InvalidTokenError)
98
+ expect(error.status).toBe(401)
99
+ })
100
+ )
101
+ })