@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
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { Effect, Fiber, Layer, Ref } from "effect"
3
+ import { S } from "effect-app"
4
+ import { ConfigureInterruptibilityMiddleware } from "effect-app/middleware"
5
+ import { Rpc, RpcGroup, RpcTest } from "effect/unstable/rpc"
6
+ import { applyRequestTypeInterruptibility } from "../src/api/routing.js"
7
+ import { ConfigureInterruptibilityMiddlewareLive, RequestType } from "../src/api/routing/middleware.js"
8
+
9
+ const InterruptibilityRpcs = RpcGroup.make(
10
+ Rpc
11
+ .make("doCommand", { success: S.Void })
12
+ .annotate(RequestType, "command")
13
+ .middleware(ConfigureInterruptibilityMiddleware),
14
+ Rpc
15
+ .make("doQuery", { success: S.Void })
16
+ .annotate(RequestType, "query")
17
+ .middleware(ConfigureInterruptibilityMiddleware)
18
+ )
19
+
20
+ const makeImplLayer = (commandDone: Ref.Ref<boolean>, queryDone: Ref.Ref<boolean>) =>
21
+ InterruptibilityRpcs.toLayer({
22
+ doCommand: () =>
23
+ applyRequestTypeInterruptibility(
24
+ "command",
25
+ Effect.sleep("120 millis").pipe(Effect.andThen(Ref.set(commandDone, true)))
26
+ ),
27
+ doQuery: () =>
28
+ applyRequestTypeInterruptibility(
29
+ "query",
30
+ Effect.sleep("120 millis").pipe(Effect.andThen(Ref.set(queryDone, true)))
31
+ )
32
+ })
33
+
34
+ describe("routing interruptibility", () => {
35
+ it.live(
36
+ "e2e: command continues after client interrupt, query does not",
37
+ () =>
38
+ Effect.gen(function*() {
39
+ const commandDone = yield* Ref.make(false)
40
+ const queryDone = yield* Ref.make(false)
41
+
42
+ const client = yield* RpcTest
43
+ .makeClient(InterruptibilityRpcs)
44
+ .pipe(
45
+ Effect.provide(
46
+ Layer.mergeAll(makeImplLayer(commandDone, queryDone), ConfigureInterruptibilityMiddlewareLive)
47
+ )
48
+ )
49
+
50
+ const commandFiber = yield* Effect.forkDetach(client.doCommand())
51
+ yield* Effect.sleep("20 millis")
52
+ yield* Fiber.interrupt(commandFiber)
53
+ yield* Effect.sleep("180 millis")
54
+ expect(yield* Ref.get(commandDone)).toBe(true)
55
+
56
+ const queryFiber = yield* Effect.forkDetach(client.doQuery())
57
+ yield* Effect.sleep("20 millis")
58
+ yield* Fiber.interrupt(queryFiber)
59
+ yield* Effect.sleep("180 millis")
60
+ expect(yield* Ref.get(queryDone)).toBe(false)
61
+ })
62
+ )
63
+ })
@@ -0,0 +1,251 @@
1
+ /**
2
+ * E2E tests for the invalidation key flow exercised end-to-end via the
3
+ * production wrap/unwrap path:
4
+ *
5
+ * server: routing.ts wraps command success with `CommandResponseWithMetaData`
6
+ * and handler-thrown failure with `CommandFailureWithMetaData`;
7
+ * routing wraps stream values into `{_tag:"value"|"metadata"|"done"}`
8
+ * chunks. `InvalidationSet.use(...)` inside a handler accumulates keys.
9
+ * client: apiClientFactory unwraps both envelopes and forwards keys to
10
+ * `InvalidationKeysFromServer`.
11
+ *
12
+ * Transport is real HTTP (NodeHttpServer on a loopback port) so the wire
13
+ * encoding is exercised too.
14
+ */
15
+ import { NodeHttpServer } from "@effect/platform-node"
16
+ import { expect, it } from "@effect/vitest"
17
+ import { Effect, Exit, Layer, Option, Ref, Stream } from "effect"
18
+ import { S } from "effect-app"
19
+ import { ApiClientFactory, InvalidationKeysFromServer, makeInvalidationKeysService, makeRpcClient } from "effect-app/client"
20
+ import { HttpRouter, HttpServer } from "effect-app/http"
21
+ import { DefaultGenericMiddlewares } from "effect-app/middleware"
22
+ import { Invalidation, MiddlewareMaker } from "effect-app/rpc"
23
+ import { TaggedErrorClass } from "effect-app/Schema"
24
+ import { FetchHttpClient } from "effect/unstable/http"
25
+ import { RpcSerialization } from "effect/unstable/rpc"
26
+ import { createServer } from "http"
27
+ import { makeRouter } from "../src/api/routing.js"
28
+ import { DefaultGenericMiddlewaresLive } from "../src/api/routing/middleware.js"
29
+ import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, SomeElseMiddleware, SomeElseMiddlewareLive, SomeService, Test, TestLive } from "./fixtures.js"
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Middleware (mirrors AppMiddleware shape — same composite as other e2e tests).
33
+ // ---------------------------------------------------------------------------
34
+
35
+ class AppMiddleware extends MiddlewareMaker
36
+ .Tag<AppMiddleware>()("AppMiddleware", RequestContextMap)
37
+ .middleware(RequireRoles, Test)
38
+ .middleware(AllowAnonymous)
39
+ .middleware(SomeElseMiddleware)
40
+ .middleware(...DefaultGenericMiddlewares)
41
+ {
42
+ static Default = this.layer.pipe(
43
+ Layer.provide(
44
+ [
45
+ RequireRolesLive.pipe(Layer.provide(SomeService.Default)),
46
+ AllowAnonymousLive,
47
+ TestLive,
48
+ SomeElseMiddlewareLive,
49
+ DefaultGenericMiddlewaresLive
50
+ ] as const
51
+ )
52
+ )
53
+ }
54
+
55
+ const { Router, matchAll } = makeRouter(AppMiddleware.Default)
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Resources
59
+ // ---------------------------------------------------------------------------
60
+
61
+ const DynamicKey: Invalidation.InvalidationKey = ["dynamic", "key"]
62
+ const ExtraKey: Invalidation.InvalidationKey = ["extra", "key"]
63
+ const StreamKey: Invalidation.InvalidationKey = ["stream", "key"]
64
+
65
+ const { TaggedRequestFor } = makeRpcClient(AppMiddleware)
66
+ const Req = TaggedRequestFor("Inv")
67
+
68
+ class CmdBoom extends TaggedErrorClass<CmdBoom>()("CmdBoom", { reason: S.String }) {}
69
+
70
+ class DoNothing extends Req.Command<DoNothing>()("DoNothing", {}, {
71
+ allowAnonymous: true,
72
+ success: S.Void
73
+ }) {}
74
+
75
+ class DoWithDynamicKey extends Req.Command<DoWithDynamicKey>()("DoWithDynamicKey", {}, {
76
+ allowAnonymous: true,
77
+ success: S.String
78
+ }) {}
79
+
80
+ class DoWithBothKeys extends Req.Command<DoWithBothKeys>()("DoWithBothKeys", {}, {
81
+ allowAnonymous: true,
82
+ success: S.Number
83
+ }) {}
84
+
85
+ class DoAndFail extends Req.Command<DoAndFail>()("DoAndFail", {}, {
86
+ allowAnonymous: true,
87
+ success: S.Void,
88
+ error: CmdBoom
89
+ }) {}
90
+
91
+ class StreamWithKey extends Req.Command<StreamWithKey>()("StreamWithKey", {}, {
92
+ stream: true,
93
+ allowAnonymous: true,
94
+ success: S.Number
95
+ }) {}
96
+
97
+ const InvRsc = { DoNothing, DoWithDynamicKey, DoWithBothKeys, DoAndFail, StreamWithKey }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Controllers / router
101
+ // ---------------------------------------------------------------------------
102
+
103
+ const router = Router(InvRsc)({
104
+ *effect(match) {
105
+ return match({
106
+ DoNothing: () => Effect.void,
107
+ DoWithDynamicKey: Effect.fnUntraced(function*() {
108
+ yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
109
+ return "done"
110
+ }),
111
+ DoWithBothKeys: Effect.fnUntraced(function*() {
112
+ yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
113
+ yield* Invalidation.InvalidationSet.use((_) => _.add(ExtraKey))
114
+ return 99
115
+ }),
116
+ DoAndFail: Effect.fnUntraced(function*() {
117
+ yield* Invalidation.InvalidationSet.use((_) => _.add(DynamicKey))
118
+ return yield* Effect.fail(new CmdBoom({ reason: "intentional failure" }))
119
+ }),
120
+ StreamWithKey: () =>
121
+ Stream.fromIterable([1, 2, 3]).pipe(
122
+ Stream.tap(() => Invalidation.InvalidationSet.use((_) => _.add(StreamKey)))
123
+ )
124
+ })
125
+ }
126
+ })
127
+
128
+ const RpcRouterLayer = matchAll({ router })
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // HTTP wiring — fresh server on loopback per `it.live`.
132
+ // ---------------------------------------------------------------------------
133
+
134
+ const NodeServerLayer = NodeHttpServer.layer(() => createServer(), { port: 0 })
135
+
136
+ const ServerLayer = HttpRouter
137
+ .serve(RpcRouterLayer)
138
+ .pipe(
139
+ Layer.provide(NodeServerLayer),
140
+ Layer.provide(RpcSerialization.layerNdjson)
141
+ )
142
+
143
+ const ClientLayer = Layer
144
+ .unwrap(
145
+ Effect.gen(function*() {
146
+ const server = yield* HttpServer.HttpServer
147
+ const addr = server.address
148
+ if (addr._tag !== "TcpAddress") return yield* Effect.die(new Error("expected TcpAddress"))
149
+ const host = addr.hostname === "0.0.0.0" ? "127.0.0.1" : addr.hostname
150
+ const url = `http://${host}:${addr.port}`
151
+ return ApiClientFactory
152
+ .layer({ url, headers: Option.none() })
153
+ .pipe(Layer.provide(FetchHttpClient.layer))
154
+ })
155
+ )
156
+ .pipe(Layer.provide(NodeServerLayer))
157
+
158
+ const TestLayer = Layer.mergeAll(ServerLayer, ClientLayer)
159
+
160
+ // Helper: provide a fresh `InvalidationKeysFromServer` and capture forwarded keys.
161
+ const withCapture = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
162
+ Effect.gen(function*() {
163
+ const ref = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
164
+ const svc = makeInvalidationKeysService(ref)
165
+ const result = yield* eff.pipe(Effect.provideService(InvalidationKeysFromServer, svc), Effect.exit)
166
+ return { result, keys: yield* Ref.get(ref) }
167
+ })
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Tests
171
+ // ---------------------------------------------------------------------------
172
+
173
+ it.live(
174
+ "command with no invalidation keys: caller sees raw payload, no keys forwarded",
175
+ Effect.fnUntraced(function*() {
176
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
177
+ const { result, keys } = yield* withCapture(client.DoNothing.handler())
178
+ expect(Exit.isSuccess(result)).toBe(true)
179
+ expect(keys).toStrictEqual([])
180
+ }, Effect.provide(TestLayer)),
181
+ { timeout: 10_000 }
182
+ )
183
+
184
+ it.live(
185
+ "command with dynamic InvalidationSet.use: payload + key forwarded",
186
+ Effect.fnUntraced(function*() {
187
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
188
+ const { result, keys } = yield* withCapture(client.DoWithDynamicKey.handler())
189
+ expect(Exit.isSuccess(result) && result.value).toBe("done")
190
+ expect(keys).toStrictEqual([DynamicKey])
191
+ }, Effect.provide(TestLayer)),
192
+ { timeout: 10_000 }
193
+ )
194
+
195
+ it.live(
196
+ "command accumulating multiple dynamic keys: all keys forwarded in order",
197
+ Effect.fnUntraced(function*() {
198
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
199
+ const { result, keys } = yield* withCapture(client.DoWithBothKeys.handler())
200
+ expect(Exit.isSuccess(result) && result.value).toBe(99)
201
+ expect(keys).toStrictEqual([DynamicKey, ExtraKey])
202
+ }, Effect.provide(TestLayer)),
203
+ { timeout: 10_000 }
204
+ )
205
+
206
+ it.live(
207
+ "per-request isolation: each command call starts with a fresh InvalidationSet",
208
+ Effect.fnUntraced(function*() {
209
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
210
+ const r1 = yield* withCapture(client.DoWithDynamicKey.handler())
211
+ const r2 = yield* withCapture(client.DoWithDynamicKey.handler())
212
+ // Each call must have exactly one key — no accumulation across calls
213
+ expect(r1.keys).toStrictEqual([DynamicKey])
214
+ expect(r2.keys).toStrictEqual([DynamicKey])
215
+ }, Effect.provide(TestLayer)),
216
+ { timeout: 10_000 }
217
+ )
218
+
219
+ it.live(
220
+ "command failure (V2): keys accumulated before fail still reach the client; original error re-thrown",
221
+ Effect.fnUntraced(function*() {
222
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
223
+ const { result, keys } = yield* withCapture(client.DoAndFail.handler())
224
+ expect(Exit.isFailure(result)).toBe(true)
225
+ if (Exit.isFailure(result)) {
226
+ const failures = (result.cause as any).reasons as ReadonlyArray<{ _tag: "Fail"; error: any }>
227
+ expect(failures[0]?.error?._tag).toBe("CmdBoom")
228
+ expect(failures[0]?.error?.reason).toBe("intentional failure")
229
+ }
230
+ expect(keys).toStrictEqual([DynamicKey])
231
+ }, Effect.provide(TestLayer)),
232
+ { timeout: 10_000 }
233
+ )
234
+
235
+ it.live(
236
+ "stream: per-chunk metadata drains keys mid-stream",
237
+ Effect.fnUntraced(function*() {
238
+ const client = yield* ApiClientFactory.makeFor(Layer.empty)(InvRsc)
239
+ const ref = yield* Ref.make<ReadonlyArray<Invalidation.InvalidationKey>>([])
240
+ const svc = makeInvalidationKeysService(ref)
241
+ const values = yield* Stream.runCollect(client.StreamWithKey.handler()).pipe(
242
+ Effect.provideService(InvalidationKeysFromServer, svc)
243
+ )
244
+ const keys = yield* Ref.get(ref)
245
+ expect(values).toStrictEqual([1, 2, 3])
246
+ // Handler taps `InvalidationSet.use` once per emitted value; routing's V3 mid-stream
247
+ // metadata drain forwards each batch as it arrives.
248
+ expect(keys).toStrictEqual([StreamKey, StreamKey, StreamKey])
249
+ }, Effect.provide(TestLayer)),
250
+ { timeout: 10_000 }
251
+ )
@@ -1,19 +1,18 @@
1
1
  import { NodeHttpServer } from "@effect/platform-node"
2
2
  import { expect, expectTypeOf, it } from "@effect/vitest"
3
- import { Console, Effect, Layer, Result } from "effect"
4
- import { S } from "effect-app"
3
+ import { Console, Effect, Layer, Ref, Result } from "effect"
4
+ import { Context, RpcX, S } from "effect-app"
5
5
  import { NotLoggedInError } from "effect-app/client"
6
6
  import { HttpRouter } from "effect-app/http"
7
7
  import { DefaultGenericMiddlewares } from "effect-app/middleware"
8
- import { MiddlewareMaker } from "effect-app/rpc"
9
- import { middlewareGroup } from "effect-app/rpc/MiddlewareMaker"
10
8
  import { FetchHttpClient } from "effect/unstable/http"
11
- import { RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
9
+ import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
12
10
  import { createServer } from "http"
13
11
  import { DefaultGenericMiddlewaresLive } from "../src/api/routing.js"
14
12
  import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElseMiddleware, SomeElseMiddlewareLive, SomeMiddleware, SomeMiddlewareLive, SomeService, Test, TestLive, UserProfile } from "./fixtures.js"
15
13
 
16
- const incomplete = MiddlewareMaker
14
+ const incomplete = RpcX
15
+ .MiddlewareMaker
17
16
  .Tag<middleware>()("MiddlewareMaker", RequestContextMap)
18
17
  .middleware(RequireRoles)
19
18
  .middleware(AllowAnonymous, Test)
@@ -21,7 +20,8 @@ const incomplete = MiddlewareMaker
21
20
  // this extension is allowed otherwise the error is quite obscure
22
21
  export class incompleteMiddleware extends incomplete {}
23
22
 
24
- class middleware extends MiddlewareMaker
23
+ class middleware extends RpcX
24
+ .MiddlewareMaker
25
25
  .Tag<middleware>()("MiddlewareMaker", RequestContextMap)
26
26
  .middleware(RequireRoles)
27
27
  .middleware(AllowAnonymous, Test)
@@ -29,7 +29,7 @@ class middleware extends MiddlewareMaker
29
29
  .middleware(...DefaultGenericMiddlewares)
30
30
  {}
31
31
 
32
- const UserRpcs = middlewareGroup(middleware)(
32
+ const UserRpcs = RpcX.MiddlewareMaker.middlewareGroup(middleware)(
33
33
  RpcGroup.make(
34
34
  middleware.rpc("getUser", {
35
35
  success: S.Literal("awesome")
@@ -56,7 +56,7 @@ const impl = UserRpcs
56
56
 
57
57
  expectTypeOf<Layer.Services<typeof impl>>().toEqualTypeOf<never>()
58
58
 
59
- const UserRpcsBad = middlewareGroup(middleware)(
59
+ const UserRpcsBad = RpcX.MiddlewareMaker.middlewareGroup(middleware)(
60
60
  RpcGroup.make(
61
61
  middleware.rpc("doSomethingElse", {
62
62
  success: S.Literal("also-awesome2"),
@@ -136,3 +136,72 @@ it.live(
136
136
  Effect.provide(RpcTestLayer)
137
137
  )
138
138
  )
139
+
140
+ // Per-request service isolation test
141
+
142
+ class PerRequestCounter extends Context.Service<PerRequestCounter>()(
143
+ "PerRequestCounter",
144
+ { make: Effect.sync(() => ({ a: 0 })) }
145
+ ) {
146
+ static Default = Layer.effect(this, this.make)
147
+ }
148
+
149
+ class GlobalCounter extends Context.Service<GlobalCounter, {
150
+ readonly ref: Ref.Ref<number>
151
+ }>()("GlobalCounter") {}
152
+
153
+ const CounterRpcs = RpcGroup.make(
154
+ Rpc.make("incrementA", {
155
+ success: S.Number
156
+ }),
157
+ Rpc.make("incrementB", {
158
+ success: S.Number
159
+ })
160
+ )
161
+
162
+ const counterImpl = CounterRpcs
163
+ .toLayer({
164
+ incrementA: Effect.fn(function*() {
165
+ const counter = yield* PerRequestCounter
166
+ counter.a++
167
+ const global = yield* GlobalCounter
168
+ yield* Ref.update(global.ref, (n) => n + 1)
169
+ return counter.a
170
+ }, Effect.provide(PerRequestCounter.Default)),
171
+ incrementB: Effect.fn(function*() {
172
+ const counter = yield* PerRequestCounter
173
+ counter.a++
174
+ const global = yield* GlobalCounter
175
+ yield* Ref.update(global.ref, (n) => n + 1)
176
+ return counter.a
177
+ }, Effect.provide(PerRequestCounter.Default))
178
+ })
179
+
180
+ const GlobalCounterLive = Layer.effect(
181
+ GlobalCounter,
182
+ Ref.make(0).pipe(Effect.map((ref) => ({ ref })))
183
+ )
184
+
185
+ const CounterTestLayer = counterImpl.pipe(Layer.provideMerge(GlobalCounterLive))
186
+
187
+ it.live(
188
+ "per-request service isolation with shared global counter",
189
+ Effect.fnUntraced(
190
+ function*() {
191
+ const client = yield* RpcTest.makeClient(CounterRpcs)
192
+ const global = yield* GlobalCounter
193
+
194
+ const r1 = yield* client.incrementA()
195
+ const r2 = yield* client.incrementB()
196
+
197
+ // per-request counter is fresh each time → both return 1
198
+ expect(r1).toBe(1)
199
+ expect(r2).toBe(1)
200
+
201
+ // global counter is shared across requests → accumulates to 2
202
+ const globalCount = yield* Ref.get(global.ref)
203
+ expect(globalCount).toBe(2)
204
+ },
205
+ Effect.provide(CounterTestLayer)
206
+ )
207
+ )