effect-app 4.0.0-beta.23 → 4.0.0-beta.230

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 (241) hide show
  1. package/CHANGELOG.md +1004 -0
  2. package/dist/Array.d.ts +3 -2
  3. package/dist/Array.d.ts.map +1 -1
  4. package/dist/Array.js +4 -4
  5. package/dist/Chunk.d.ts +1 -1
  6. package/dist/Chunk.d.ts.map +1 -1
  7. package/dist/Config/SecretURL.d.ts +4 -2
  8. package/dist/Config/SecretURL.d.ts.map +1 -1
  9. package/dist/Config/SecretURL.js +3 -6
  10. package/dist/Config/internal/configSecretURL.d.ts +1 -1
  11. package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
  12. package/dist/Config/internal/configSecretURL.js +2 -2
  13. package/dist/Config.d.ts +7 -0
  14. package/dist/Config.d.ts.map +1 -0
  15. package/dist/Config.js +6 -0
  16. package/dist/ConfigProvider.d.ts +39 -0
  17. package/dist/ConfigProvider.d.ts.map +1 -0
  18. package/dist/ConfigProvider.js +42 -0
  19. package/dist/Context.d.ts +43 -0
  20. package/dist/Context.d.ts.map +1 -0
  21. package/dist/Context.js +67 -0
  22. package/dist/Effect.d.ts +11 -10
  23. package/dist/Effect.d.ts.map +1 -1
  24. package/dist/Effect.js +6 -7
  25. package/dist/Function.d.ts +1 -1
  26. package/dist/Function.d.ts.map +1 -1
  27. package/dist/Inputify.type.d.ts +1 -1
  28. package/dist/Layer.d.ts +11 -6
  29. package/dist/Layer.d.ts.map +1 -1
  30. package/dist/Layer.js +3 -2
  31. package/dist/NonEmptySet.d.ts +4 -2
  32. package/dist/NonEmptySet.d.ts.map +1 -1
  33. package/dist/NonEmptySet.js +2 -2
  34. package/dist/Option.d.ts +2 -1
  35. package/dist/Option.d.ts.map +1 -1
  36. package/dist/Option.js +3 -1
  37. package/dist/Pure.d.ts +8 -6
  38. package/dist/Pure.d.ts.map +1 -1
  39. package/dist/Pure.js +17 -14
  40. package/dist/Schema/Class.d.ts +66 -20
  41. package/dist/Schema/Class.d.ts.map +1 -1
  42. package/dist/Schema/Class.js +192 -23
  43. package/dist/Schema/FastCheck.d.ts +1 -1
  44. package/dist/Schema/FastCheck.d.ts.map +1 -1
  45. package/dist/Schema/Methods.d.ts +1 -1
  46. package/dist/Schema/SchemaParser.d.ts +5 -0
  47. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  48. package/dist/Schema/SchemaParser.js +6 -0
  49. package/dist/Schema/SpecialJsonSchema.d.ts +34 -0
  50. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  51. package/dist/Schema/SpecialJsonSchema.js +118 -0
  52. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  53. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  54. package/dist/Schema/SpecialOpenApi.js +123 -0
  55. package/dist/Schema/brand.d.ts +5 -3
  56. package/dist/Schema/brand.d.ts.map +1 -1
  57. package/dist/Schema/brand.js +3 -1
  58. package/dist/Schema/email.d.ts +1 -1
  59. package/dist/Schema/email.d.ts.map +1 -1
  60. package/dist/Schema/email.js +7 -4
  61. package/dist/Schema/ext.d.ts +339 -56
  62. package/dist/Schema/ext.d.ts.map +1 -1
  63. package/dist/Schema/ext.js +351 -53
  64. package/dist/Schema/moreStrings.d.ts +108 -26
  65. package/dist/Schema/moreStrings.d.ts.map +1 -1
  66. package/dist/Schema/moreStrings.js +45 -16
  67. package/dist/Schema/numbers.d.ts +55 -15
  68. package/dist/Schema/numbers.d.ts.map +1 -1
  69. package/dist/Schema/numbers.js +60 -12
  70. package/dist/Schema/phoneNumber.d.ts +1 -1
  71. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  72. package/dist/Schema/phoneNumber.js +6 -3
  73. package/dist/Schema/schema.d.ts +1 -1
  74. package/dist/Schema/strings.d.ts +5 -5
  75. package/dist/Schema/strings.d.ts.map +1 -1
  76. package/dist/Schema/strings.js +1 -5
  77. package/dist/Schema.d.ts +214 -8
  78. package/dist/Schema.d.ts.map +1 -1
  79. package/dist/Schema.js +219 -21
  80. package/dist/Set.d.ts +5 -2
  81. package/dist/Set.d.ts.map +1 -1
  82. package/dist/Set.js +3 -2
  83. package/dist/TypeTest.d.ts +1 -1
  84. package/dist/Types.d.ts +1 -1
  85. package/dist/Widen.type.d.ts +1 -1
  86. package/dist/_ext/Array.d.ts +2 -2
  87. package/dist/_ext/Array.d.ts.map +1 -1
  88. package/dist/_ext/Array.js +4 -2
  89. package/dist/_ext/date.d.ts +1 -1
  90. package/dist/_ext/misc.d.ts +5 -2
  91. package/dist/_ext/misc.d.ts.map +1 -1
  92. package/dist/_ext/misc.js +4 -2
  93. package/dist/_ext/ord.ext.d.ts +3 -2
  94. package/dist/_ext/ord.ext.d.ts.map +1 -1
  95. package/dist/_ext/ord.ext.js +2 -2
  96. package/dist/builtin.d.ts +1 -1
  97. package/dist/builtin.d.ts.map +1 -1
  98. package/dist/client/InvalidationKeys.d.ts +29 -0
  99. package/dist/client/InvalidationKeys.d.ts.map +1 -0
  100. package/dist/client/InvalidationKeys.js +33 -0
  101. package/dist/client/apiClientFactory.d.ts +20 -32
  102. package/dist/client/apiClientFactory.d.ts.map +1 -1
  103. package/dist/client/apiClientFactory.js +96 -33
  104. package/dist/client/clientFor.d.ts +51 -17
  105. package/dist/client/clientFor.d.ts.map +1 -1
  106. package/dist/client/clientFor.js +9 -1
  107. package/dist/client/errors.d.ts +49 -25
  108. package/dist/client/errors.d.ts.map +1 -1
  109. package/dist/client/errors.js +43 -17
  110. package/dist/client/makeClient.d.ts +481 -33
  111. package/dist/client/makeClient.d.ts.map +1 -1
  112. package/dist/client/makeClient.js +66 -24
  113. package/dist/client.d.ts +2 -1
  114. package/dist/client.d.ts.map +1 -1
  115. package/dist/client.js +2 -1
  116. package/dist/faker.d.ts +1 -1
  117. package/dist/faker.d.ts.map +1 -1
  118. package/dist/http/Request.d.ts +2 -2
  119. package/dist/http/Request.d.ts.map +1 -1
  120. package/dist/http/Request.js +2 -2
  121. package/dist/http/internal/lib.d.ts +1 -1
  122. package/dist/http.d.ts +1 -1
  123. package/dist/ids.d.ts +40 -12
  124. package/dist/ids.d.ts.map +1 -1
  125. package/dist/ids.js +25 -3
  126. package/dist/index.d.ts +5 -8
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +6 -8
  129. package/dist/logger.d.ts +1 -1
  130. package/dist/middleware.d.ts +14 -8
  131. package/dist/middleware.d.ts.map +1 -1
  132. package/dist/middleware.js +14 -8
  133. package/dist/rpc/Invalidation.d.ts +402 -0
  134. package/dist/rpc/Invalidation.d.ts.map +1 -0
  135. package/dist/rpc/Invalidation.js +150 -0
  136. package/dist/rpc/MiddlewareMaker.d.ts +11 -7
  137. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  138. package/dist/rpc/MiddlewareMaker.js +59 -38
  139. package/dist/rpc/RpcContextMap.d.ts +4 -4
  140. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  141. package/dist/rpc/RpcContextMap.js +4 -4
  142. package/dist/rpc/RpcMiddleware.d.ts +14 -10
  143. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  144. package/dist/rpc/RpcMiddleware.js +1 -1
  145. package/dist/rpc.d.ts +2 -2
  146. package/dist/rpc.d.ts.map +1 -1
  147. package/dist/rpc.js +2 -2
  148. package/dist/transform.d.ts +2 -2
  149. package/dist/transform.d.ts.map +1 -1
  150. package/dist/transform.js +4 -5
  151. package/dist/utils/effectify.d.ts +2 -2
  152. package/dist/utils/effectify.d.ts.map +1 -1
  153. package/dist/utils/effectify.js +2 -2
  154. package/dist/utils/extend.d.ts +1 -1
  155. package/dist/utils/extend.d.ts.map +1 -1
  156. package/dist/utils/gen.d.ts +2 -2
  157. package/dist/utils/gen.d.ts.map +1 -1
  158. package/dist/utils/logLevel.d.ts +3 -3
  159. package/dist/utils/logLevel.d.ts.map +1 -1
  160. package/dist/utils/logger.d.ts +5 -4
  161. package/dist/utils/logger.d.ts.map +1 -1
  162. package/dist/utils/logger.js +4 -4
  163. package/dist/utils.d.ts +35 -40
  164. package/dist/utils.d.ts.map +1 -1
  165. package/dist/utils.js +19 -27
  166. package/dist/validation/validators.d.ts +1 -1
  167. package/dist/validation/validators.d.ts.map +1 -1
  168. package/dist/validation.d.ts +1 -1
  169. package/dist/validation.d.ts.map +1 -1
  170. package/package.json +46 -24
  171. package/src/Array.ts +3 -3
  172. package/src/Config/SecretURL.ts +5 -2
  173. package/src/Config/internal/configSecretURL.ts +1 -1
  174. package/src/Config.ts +14 -0
  175. package/src/ConfigProvider.ts +48 -0
  176. package/src/{ServiceMap.ts → Context.ts} +56 -60
  177. package/src/Effect.ts +14 -14
  178. package/src/Layer.ts +10 -5
  179. package/src/NonEmptySet.ts +3 -1
  180. package/src/Option.ts +2 -0
  181. package/src/Pure.ts +21 -19
  182. package/src/Schema/Class.ts +274 -64
  183. package/src/Schema/SchemaParser.ts +12 -0
  184. package/src/Schema/SpecialJsonSchema.ts +139 -0
  185. package/src/Schema/SpecialOpenApi.ts +130 -0
  186. package/src/Schema/brand.ts +22 -2
  187. package/src/Schema/email.ts +7 -2
  188. package/src/Schema/ext.ts +432 -88
  189. package/src/Schema/moreStrings.ts +93 -37
  190. package/src/Schema/numbers.ts +64 -16
  191. package/src/Schema/phoneNumber.ts +5 -1
  192. package/src/Schema/strings.ts +4 -8
  193. package/src/Schema.ts +404 -18
  194. package/src/Set.ts +5 -1
  195. package/src/_ext/Array.ts +3 -1
  196. package/src/_ext/misc.ts +4 -1
  197. package/src/_ext/ord.ext.ts +2 -1
  198. package/src/client/InvalidationKeys.ts +50 -0
  199. package/src/client/apiClientFactory.ts +224 -130
  200. package/src/client/clientFor.ts +95 -29
  201. package/src/client/errors.ts +52 -26
  202. package/src/client/makeClient.ts +572 -71
  203. package/src/client.ts +1 -0
  204. package/src/http/Request.ts +1 -1
  205. package/src/ids.ts +25 -3
  206. package/src/index.ts +5 -10
  207. package/src/middleware.ts +13 -9
  208. package/src/rpc/Invalidation.ts +226 -0
  209. package/src/rpc/MiddlewareMaker.ts +82 -74
  210. package/src/rpc/README.md +2 -2
  211. package/src/rpc/RpcContextMap.ts +6 -5
  212. package/src/rpc/RpcMiddleware.ts +14 -11
  213. package/src/rpc.ts +1 -1
  214. package/src/transform.ts +3 -3
  215. package/src/utils/effectify.ts +1 -1
  216. package/src/utils/gen.ts +1 -1
  217. package/src/utils/logLevel.ts +1 -1
  218. package/src/utils/logger.ts +4 -3
  219. package/src/utils.ts +57 -134
  220. package/test/dist/rpc.test.d.ts.map +1 -1
  221. package/test/dist/secretURL.test.d.ts.map +1 -0
  222. package/test/dist/special.test.d.ts.map +1 -0
  223. package/test/moreStrings.test.ts +1 -1
  224. package/test/rpc.test.ts +46 -6
  225. package/test/schema.test.ts +585 -10
  226. package/test/secretURL.test.ts +160 -0
  227. package/test/special.test.ts +1026 -0
  228. package/test/utils.test.ts +7 -7
  229. package/tsconfig.base.json +3 -4
  230. package/tsconfig.json +0 -1
  231. package/tsconfig.json.bak +2 -2
  232. package/tsconfig.src.json +29 -29
  233. package/tsconfig.test.json +2 -2
  234. package/dist/Operations.d.ts +0 -123
  235. package/dist/Operations.d.ts.map +0 -1
  236. package/dist/Operations.js +0 -29
  237. package/dist/ServiceMap.d.ts +0 -44
  238. package/dist/ServiceMap.d.ts.map +0 -1
  239. package/dist/ServiceMap.js +0 -91
  240. package/eslint.config.mjs +0 -26
  241. package/src/Operations.ts +0 -55
@@ -1,4 +1,4 @@
1
- import { Option } from "effect"
1
+ import * as Option from "effect/Option"
2
2
  import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"
3
3
  import * as Effect from "../Effect.js"
4
4
  import { HttpClient, HttpClientError, HttpClientRequest, HttpHeaders } from "./internal/lib.js"
package/src/ids.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { brandedStringId, type Codec, NonEmptyString255, StringId, type StringIdBrand, withDefaultMake } from "effect-app/Schema"
2
2
  import type { B } from "effect-app/Schema/schema"
3
+ import * as Effect from "effect/Effect"
3
4
  import type { Simplify } from "effect/Types"
4
5
  import { S } from "./index.js"
5
6
  import { extendM } from "./utils.js"
@@ -9,7 +10,16 @@ export interface RequestIdBrand extends StringIdBrand {
9
10
  }
10
11
 
11
12
  export type RequestId = NonEmptyString255
12
- // a request id may be made from a span id, which does not comply with StringId schema.
13
+ /**
14
+ * Schema for a request id.
15
+ *
16
+ * A request id may be made from a span id, which does not comply with the
17
+ * `StringId` schema (hence the looser `NonEmptyString255` base).
18
+ *
19
+ * `.withConstructorDefault` => fresh `StringId` (construction-only; not
20
+ * applied during decode — cannot be used to JIT-migrate database fields).
21
+ * See `./Schema/ext.ts` for the full policy note.
22
+ */
13
23
  export const RequestId = extendM(
14
24
  Object
15
25
  .assign(Object.create(NonEmptyString255) as {}, NonEmptyString255 as unknown as Codec<NonEmptyString255, string>),
@@ -17,12 +27,24 @@ export const RequestId = extendM(
17
27
  const make = StringId.make as () => NonEmptyString255
18
28
  return ({
19
29
  make,
20
- withDefault: s.pipe(S.withDefaultConstructor(make))
30
+ /**
31
+ * Construction-only default: fresh `StringId`. Applied only when the
32
+ * field is omitted from `.make(...)` input. NOT applied during decode —
33
+ * cannot be used to JIT-migrate database fields. See `./Schema/ext.ts`
34
+ * file-level note.
35
+ */
36
+ withConstructorDefault: S.withConstructorDefault(Effect.sync(make))(s as typeof s & S.WithoutConstructorDefault)
21
37
  })
22
38
  }
23
39
  )
24
40
  .pipe(withDefaultMake)
25
41
 
26
42
  export interface UserProfileIdBrand extends Simplify<B.Brand<"UserProfileId"> & StringIdBrand> {}
27
- export type UserProfileId = StringId & UserProfileIdBrand
43
+ export type UserProfileId = string & UserProfileIdBrand
44
+ /**
45
+ * Branded `StringId` for user profiles.
46
+ *
47
+ * Exposes `.withConstructorDefault` (fresh id) — construction-only; not
48
+ * applied during decode. See `./Schema/ext.ts` for the full policy note.
49
+ */
28
50
  export const UserProfileId = brandedStringId<UserProfileId>()
package/src/index.ts CHANGED
@@ -1,24 +1,18 @@
1
+ // eslint-disable-next-line import/no-unassigned-import
1
2
  import "./builtin.js"
2
3
 
3
- import * as ServiceMap from "./ServiceMap.js"
4
-
5
4
  export * as Fnc from "./Function.js"
6
5
  export * as Utils from "./utils.js"
7
6
 
8
7
  export * as Array from "./Array.js"
8
+ export * as Config from "./Config.js"
9
+ export * as ConfigProvider from "./ConfigProvider.js"
10
+ export * as Context from "./Context.js"
9
11
  export * as Effect from "./Effect.js"
10
12
  export * as Layer from "./Layer.js"
11
13
  export * as NonEmptySet from "./NonEmptySet.js"
12
- export * as ServiceMap from "./ServiceMap.js"
13
14
  export * as Set from "./Set.js"
14
15
 
15
- export {
16
- /**
17
- * @deprecated use ServiceMap directly instead
18
- */
19
- ServiceMap as Context
20
- }
21
-
22
16
  export { type NonEmptyArray, type NonEmptyReadonlyArray } from "./Array.js"
23
17
 
24
18
  export * from "effect"
@@ -26,5 +20,6 @@ export * from "effect"
26
20
  export type * as Types from "./Types.js"
27
21
 
28
22
  export * as SecretURL from "./Config/SecretURL.js"
23
+ export * as RpcX from "./rpc.js"
29
24
  export * as S from "./Schema.js"
30
25
  export { copy } from "./utils.js"
package/src/middleware.ts CHANGED
@@ -1,21 +1,25 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { ServiceMap } from "effect-app"
3
- import { RpcX } from "./rpc.js"
2
+ import * as Context from "effect-app/Context"
3
+ import { RpcMiddleware } from "./rpc.js"
4
4
 
5
- export class DevMode extends ServiceMap.Reference("DevMode", { defaultValue: () => false }) {}
5
+ export class DevMode extends Context.Reference("DevMode", { defaultValue: () => false }) {}
6
6
 
7
- export class RequestCacheMiddleware
8
- extends RpcX.RpcMiddleware.Tag<RequestCacheMiddleware>()("RequestCacheMiddleware")
9
- {}
7
+ export class RequestCacheMiddleware extends RpcMiddleware.Tag<RequestCacheMiddleware>()("RequestCacheMiddleware") {}
10
8
 
11
9
  export class ConfigureInterruptibilityMiddleware
12
- extends RpcX.RpcMiddleware.Tag<ConfigureInterruptibilityMiddleware>()("ConfigureInterruptibilityMiddleware")
10
+ extends RpcMiddleware.Tag<ConfigureInterruptibilityMiddleware>()("ConfigureInterruptibilityMiddleware")
13
11
  {}
14
12
 
15
- export class LoggerMiddleware extends RpcX.RpcMiddleware.Tag<LoggerMiddleware>()("LoggerMiddleware") {}
13
+ export class LoggerMiddleware extends RpcMiddleware.Tag<LoggerMiddleware>()("LoggerMiddleware") {}
16
14
 
17
- export class DevModeMiddleware extends RpcX.RpcMiddleware.Tag<DevModeMiddleware>()("DevModeMiddleware") {}
15
+ export class DevModeMiddleware extends RpcMiddleware.Tag<DevModeMiddleware>()("DevModeMiddleware") {}
18
16
 
17
+ /**
18
+ * Generic middlewares attached by `makeRouter` to every request.
19
+ *
20
+ * Invalidation key wrap/unwrap is handled by the routing layer (server) and the
21
+ * api client factory (client) directly — there is no middleware tag for it.
22
+ */
19
23
  export const DefaultGenericMiddlewares = [
20
24
  RequestCacheMiddleware,
21
25
  ConfigureInterruptibilityMiddleware,
@@ -0,0 +1,226 @@
1
+ import * as Ref from "effect/Ref"
2
+ import { Rpc } from "effect/unstable/rpc"
3
+ import * as Context from "../Context.js"
4
+ import * as Effect from "../Effect.js"
5
+ import * as S from "../Schema.js"
6
+
7
+ /**
8
+ * A single segment within an `InvalidationKey` array.
9
+ * Accepts any JSON-compatible value: string, number, boolean, null,
10
+ * arrays and objects recursively — matching TanStack Query's `queryKey` element type.
11
+ */
12
+ export const InvalidationKeySegment = S.Json
13
+ export type InvalidationKeySegment = S.Schema.Type<typeof InvalidationKeySegment>
14
+
15
+ /** Schema for a single invalidation key – an array of segments compatible with TanStack Query `queryKey`. */
16
+ export const InvalidationKey = S.Array(InvalidationKeySegment)
17
+ export type InvalidationKey = S.Schema.Type<typeof InvalidationKey>
18
+
19
+ /** Schema for the full set of invalidation keys – an array of `InvalidationKey`. */
20
+ export const InvalidationKeys = S.Array(InvalidationKey)
21
+ export type InvalidationKeys = S.Schema.Type<typeof InvalidationKeys>
22
+
23
+ /** Metadata included in every command response for server-driven cache invalidation. */
24
+ export const CommandMetaData = S.Struct({ invalidateQueries: InvalidationKeys })
25
+ export type CommandMetaData = S.Schema.Type<typeof CommandMetaData>
26
+
27
+ /**
28
+ * Wraps a command's success schema so that the wire format carries both the `payload`
29
+ * (the handler's actual return value) and `metadata` (server-driven cache invalidation keys).
30
+ * Transparent to users: the server handler returns the plain payload and the client receives
31
+ * the plain payload — wrapping/unwrapping is handled internally by the routing layer.
32
+ */
33
+ export const CommandResponseWithMetaData = <S extends S.Top>(success: S) =>
34
+ S.Struct({ payload: success, metadata: CommandMetaData })
35
+
36
+ /**
37
+ * Wraps a command's failure schema so that the wire format carries both the `error`
38
+ * (the handler's actual failure value) and `metadata` (server-driven cache invalidation keys
39
+ * accumulated thus far before the failure occurred).
40
+ * Transparent to users: the server handler fails with the plain error and the client receives
41
+ * the plain error — wrapping/unwrapping is handled internally by the routing layer.
42
+ */
43
+ export const CommandFailureWithMetaData = <E extends S.Top>(error: E) =>
44
+ S.Struct({ _tag: S.Literal("CommandFailureWithMetaData"), error, metadata: CommandMetaData })
45
+
46
+ /**
47
+ * Stream chunk schema for stream responses with metadata.
48
+ * Each item is either a data value, an intermediate "metadata" signal carrying cache
49
+ * invalidation keys accumulated since the previous drain, or a final "done" signal.
50
+ * Transparent to users: stream handlers return plain values and clients receive plain values —
51
+ * wrapping/unwrapping is handled internally by the routing layer.
52
+ *
53
+ * The "done" chunk is always the last item in the stream and carries any remaining invalidation
54
+ * keys. An optional "metadata" chunk may appear after any "value" chunk and carries keys
55
+ * accumulated since the last drain (V3: mid-stream invalidation).
56
+ */
57
+ export const StreamResponseChunk = <S extends S.Top>(success: S) =>
58
+ S.Union([
59
+ S.Struct({ _tag: S.Literal("value"), value: success }),
60
+ S.Struct({ _tag: S.Literal("metadata"), metadata: CommandMetaData }),
61
+ S.Struct({ _tag: S.Literal("done"), metadata: CommandMetaData })
62
+ ])
63
+
64
+ export type StreamResponseChunk<A> =
65
+ | { readonly _tag: "value"; readonly value: A }
66
+ | { readonly _tag: "metadata"; readonly metadata: CommandMetaData }
67
+ | { readonly _tag: "done"; readonly metadata: CommandMetaData }
68
+
69
+ /**
70
+ * Stream chunk schema for stream failures with metadata.
71
+ * Used to signal a stream failure while still carrying cache invalidation keys
72
+ * accumulated thus far.
73
+ */
74
+ export const StreamFailureChunk = <E extends S.Top>(error: E) =>
75
+ S.Struct({ _tag: S.Literal("error"), error, metadata: CommandMetaData })
76
+
77
+ export type StreamFailureChunk<E> = { readonly _tag: "error"; readonly error: E; readonly metadata: CommandMetaData }
78
+
79
+ /**
80
+ * Context annotation for declaring static cache invalidation keys on a low-level `Rpc` definition.
81
+ * These keys are always included in the command response metadata, regardless of the handler logic.
82
+ *
83
+ * Prefer using `makeQueryKey` over raw string arrays to stay in sync with the actual query
84
+ * definitions without manual string maintenance:
85
+ *
86
+ * ```ts
87
+ * import { makeQueryKey } from "effect-app/client"
88
+ * import { Invalidation } from "effect-app/rpc"
89
+ * import * as UserRsc from "../User/index.js" // separate module to avoid circular deps
90
+ *
91
+ * class UpdateProfile extends Rpc.make("UpdateProfile", { ... })
92
+ * .annotate(Invalidation.Invalidates, [makeQueryKey(UserRsc.GetMe), makeQueryKey(UserRsc.GetProfile)]) {}
93
+ * ```
94
+ *
95
+ * **Circular dependency note:** if mutations and queries live in the same file you may hit a
96
+ * circular reference at evaluation time. The idiomatic fix is to move mutations into their own
97
+ * module (e.g. `User/mutations.ts`) that directly imports the relevant query classes rather than
98
+ * re-exporting them through a barrel.
99
+ *
100
+ * For the higher-level `Command`/`Query` builders from `makeRpcClient`, use the
101
+ * `invalidatesQueries` callback argument instead (it receives the same query keys at runtime).
102
+ */
103
+ export const Invalidates = Context.Reference<ReadonlyArray<InvalidationKey>>(
104
+ "effect-app/rpc/Invalidates",
105
+ { defaultValue: () => [] }
106
+ )
107
+ export type Invalidates = typeof Invalidates
108
+
109
+ /** The shape of the per-request service that accumulates invalidation keys. */
110
+ export interface InvalidationSetService {
111
+ readonly add: (key: InvalidationKey) => Effect.Effect<void>
112
+ readonly get: Effect.Effect<ReadonlyArray<InvalidationKey>>
113
+ /**
114
+ * V3: Reads all currently accumulated keys and resets the bucket to empty.
115
+ * Used by the stream routing layer to emit intermediate "metadata" chunks
116
+ * without re-sending keys that have already been forwarded to the client.
117
+ */
118
+ readonly drain: Effect.Effect<ReadonlyArray<InvalidationKey>>
119
+ }
120
+
121
+ /**
122
+ * Request-scoped service for accumulating invalidation keys dynamically inside a handler.
123
+ * Provided by `InvalidationMiddlewareLive` for every RPC call; has a no-op default so it is
124
+ * safe to use even when the HTTP middleware is absent (tests, workers, etc.).
125
+ *
126
+ * Use `InvalidationSet.use(_ => _.add(key))` (or `.useSync` for non-Effect callbacks) as a
127
+ * shorthand instead of yielding the service manually.
128
+ *
129
+ * Prefer `makeQueryKey` over raw string arrays so invalidation keys stay in sync with the
130
+ * actual query definitions automatically:
131
+ *
132
+ * ```ts
133
+ * import { makeQueryKey } from "effect-app/client"
134
+ * import * as Effect from "effect/Effect"
135
+ * import { Invalidation } from "effect-app/rpc"
136
+ * import * as CartRsc from "../Cart/queries.js"
137
+ * import * as UserRsc from "../User/queries.js"
138
+ *
139
+ * const handler = Effect.fnUntraced(function*(req: UpdateCartRequest) {
140
+ * const cart = yield* CartRepo.save(req.cart)
141
+ *
142
+ * // Stage 1 – unconditional: always invalidate after saving
143
+ * yield* Invalidation.InvalidationSet.use(_ => _.add(makeQueryKey(UserRsc.GetMe)))
144
+ *
145
+ * // Stage 2 – conditional: only if the cart changed state
146
+ * if (cart.isCheckedOut) {
147
+ * yield* Invalidation.InvalidationSet.use(_ => _.add(makeQueryKey(CartRsc.GetCartStats)))
148
+ * }
149
+ *
150
+ * return cart
151
+ * })
152
+ * ```
153
+ *
154
+ * You can combine static (`Invalidates` annotation) and dynamic (`InvalidationSet.use`) keys:
155
+ * the annotation pre-populates the set before the handler runs; dynamic additions accumulate
156
+ * throughout the handler. All keys are included in the command response metadata.
157
+ */
158
+ export const InvalidationSet = Context.Reference<InvalidationSetService>(
159
+ "effect-app/rpc/InvalidationSet",
160
+ {
161
+ defaultValue: () => ({
162
+ add: (_key: InvalidationKey) => Effect.void,
163
+ get: Effect.succeed([] as ReadonlyArray<InvalidationKey>),
164
+ drain: Effect.succeed([] as ReadonlyArray<InvalidationKey>)
165
+ })
166
+ }
167
+ )
168
+ export type InvalidationSet = typeof InvalidationSet
169
+
170
+ /** Creates a fresh `InvalidationSet` implementation backed by a `Ref`. */
171
+ export const makeInvalidationSet = (ref: Ref.Ref<ReadonlyArray<InvalidationKey>>): InvalidationSetService => ({
172
+ add: (key) => Ref.update(ref, (keys) => [...keys, key]),
173
+ get: Ref.get(ref),
174
+ drain: Ref.getAndSet(ref, [])
175
+ })
176
+
177
+ /**
178
+ * `Rpc.Custom` definition for command RPCs that wrap the success/error schemas
179
+ * with `CommandResponseWithMetaData` / `CommandFailureWithMetaData`. The wrap
180
+ * lets server forward accumulated invalidation keys on both success and
181
+ * handler-thrown failure paths. Middleware-thrown errors bypass the wrap
182
+ * (the handler never ran, so no metadata) and flow raw at the Cause level —
183
+ * the client decodes them via the `rpc.middlewares[*].error` failure-union
184
+ * channel of `Rpc.exitSchema`.
185
+ */
186
+ // eslint-disable-next-line import/namespace
187
+ export interface CommandRpc extends Rpc.Custom {
188
+ readonly out: Rpc.Custom.Out<
189
+ ReturnType<typeof CommandResponseWithMetaData<this["success"] & S.Top>>,
190
+ ReturnType<typeof CommandFailureWithMetaData<this["error"] & S.Top>>
191
+ >
192
+ }
193
+
194
+ /**
195
+ * Custom Rpc constructor for command RPCs.
196
+ * Wraps the success schema with `CommandResponseWithMetaData` and the error
197
+ * schema with `CommandFailureWithMetaData`.
198
+ */
199
+ export const makeCommandRpc = Rpc.custom<CommandRpc>(({ defect, error, success }) => ({
200
+ success: CommandResponseWithMetaData(success),
201
+ error: CommandFailureWithMetaData(error),
202
+ defect
203
+ }))
204
+
205
+ /**
206
+ * `Rpc.Custom` definition for stream RPCs that wrap the success/error schemas
207
+ * with `StreamResponseChunk` / `StreamFailureChunk`.
208
+ */
209
+ // eslint-disable-next-line import/namespace
210
+ export interface StreamRpc extends Rpc.Custom {
211
+ readonly out: Rpc.Custom.Out<
212
+ ReturnType<typeof StreamResponseChunk<this["success"] & S.Top>>,
213
+ ReturnType<typeof StreamFailureChunk<this["error"] & S.Top>>
214
+ >
215
+ }
216
+
217
+ /**
218
+ * Custom Rpc constructor for stream RPCs.
219
+ * Wraps the success schema with `StreamResponseChunk` and
220
+ * the error schema with `StreamFailureChunk`.
221
+ */
222
+ export const makeStreamRpc = Rpc.custom<StreamRpc>(({ defect, error, success }) => ({
223
+ success: StreamResponseChunk(success),
224
+ error: StreamFailureChunk(error),
225
+ defect
226
+ }))
@@ -1,14 +1,15 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { Effect, Layer, type Schema, Schema as S, type Scope, ServiceMap } from "effect"
3
2
  import { type NonEmptyArray, type NonEmptyReadonlyArray } from "effect/Array"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Layer from "effect/Layer"
5
+ import * as S from "effect/Schema"
6
+ import type * as Scope from "effect/Scope"
4
7
  import { type Simplify } from "effect/Types"
5
8
  import { Rpc, type RpcGroup, type RpcSchema } from "effect/unstable/rpc"
6
9
  import { type HandlersFrom } from "effect/unstable/rpc/RpcGroup"
7
- import { type RequestId } from "effect/unstable/rpc/RpcMessage"
8
- import { type HttpHeaders } from "../http.js"
10
+ import * as Context from "../Context.js"
9
11
  import { PreludeLogger } from "../logger.js"
10
12
  import { type TypeTestId } from "../TypeTest.js"
11
- import { typedValuesOf } from "../utils.js"
12
13
  import { type GetContextConfig, type RequestContextMapTagAny, type RpcContextMap } from "./RpcContextMap.js"
13
14
  import { type AddMiddleware, type AnyDynamic, type RpcDynamic, type RpcMiddlewareV4, type TagClassAny } from "./RpcMiddleware.js"
14
15
  import * as RpcMiddlewareX from "./RpcMiddleware.js"
@@ -61,13 +62,13 @@ export interface MiddlewareMaker<
61
62
  }
62
63
  >
63
64
  {
64
- readonly layer: Layer.Layer<Self, never, ServiceMap.Service.Identifier<MiddlewareProviders[number]>>
65
+ readonly layer: Layer.Layer<Self, never, Context.Service.Identifier<MiddlewareProviders[number]>>
65
66
  readonly requestContext: RequestContextTag<RequestContextMap>
66
67
  readonly requestContextMap: RequestContextMap
67
68
  }
68
69
 
69
70
  export interface RequestContextTag<RequestContextMap extends Record<string, RpcContextMap.Any>>
70
- extends ServiceMap.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>
71
+ extends Context.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>
71
72
  {}
72
73
 
73
74
  export namespace MiddlewareMaker {
@@ -169,9 +170,9 @@ export interface BuildingMiddleware<
169
170
  > {
170
171
  rpc: <
171
172
  const Tag extends string,
172
- Payload extends Schema.Top | Schema.Struct.Fields = typeof Schema.Void,
173
- Success extends Schema.Top = typeof Schema.Void,
174
- Error extends Schema.Top = typeof Schema.Never,
173
+ Payload extends S.Top | S.Struct.Fields = typeof S.Void,
174
+ Success extends S.Top = typeof S.Void,
175
+ Error extends S.Top = typeof S.Never,
175
176
  const Stream extends boolean = false,
176
177
  Config extends GetContextConfig<RequestContextMap> = {}
177
178
  >(tag: Tag, options?: {
@@ -180,16 +181,16 @@ export interface BuildingMiddleware<
180
181
  readonly error?: Error
181
182
  readonly stream?: Stream
182
183
  readonly config?: Config
183
- readonly primaryKey?: [Payload] extends [Schema.Struct.Fields] ? ((
184
- payload: Payload extends Schema.Struct.Fields ? Simplify<Schema.Struct<Payload>["Type"]> : Payload["Type"]
184
+ readonly primaryKey?: [Payload] extends [S.Struct.Fields] ? ((
185
+ payload: Payload extends S.Struct.Fields ? Simplify<S.Struct<Payload>["Type"]> : Payload["Type"]
185
186
  ) => string)
186
187
  : never
187
188
  }) =>
188
189
  & Rpc.Rpc<
189
190
  Tag,
190
- Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> : Payload,
191
+ Payload extends S.Struct.Fields ? S.Struct<Payload> : Payload,
191
192
  Stream extends true ? RpcSchema.Stream<Success, Error> : Success,
192
- Stream extends true ? typeof Schema.Never : Error
193
+ Stream extends true ? typeof S.Never : Error
193
194
  >
194
195
  & { readonly config: Config }
195
196
 
@@ -258,10 +259,10 @@ export type MiddlewaresBuilder<
258
259
  : { new(_: never): {} }
259
260
  : { new(_: never): {} })
260
261
 
261
- const middlewareMaker = <
262
+ const middlewareMaker = Effect.fnUntraced(function*<
262
263
  MiddlewareProviders extends ReadonlyArray<MiddlewareMaker.Any>
263
- >(middlewares: MiddlewareProviders): Effect.Effect<
264
- RpcMiddlewareV4<
264
+ >(middlewares: MiddlewareProviders) {
265
+ type Middleware = RpcMiddlewareV4<
265
266
  MiddlewareMaker.ManyProvided<MiddlewareProviders>,
266
267
  MiddlewareMaker.ManyErrors<MiddlewareProviders>,
267
268
  Exclude<
@@ -270,42 +271,32 @@ const middlewareMaker = <
270
271
  > extends never ? never
271
272
  : Exclude<MiddlewareMaker.ManyRequired<MiddlewareProviders>, MiddlewareMaker.ManyProvided<MiddlewareProviders>>
272
273
  >
273
- > => {
274
+ type Next = Parameters<Middleware>[0]
275
+ type Options = Parameters<Middleware>[1]
276
+
274
277
  // we want to run them in reverse order because latter middlewares will provide context to former ones
275
- middlewares = middlewares.toReversed() as any
276
-
277
- return Effect.gen(function*() {
278
- const context = yield* Effect.services()
279
-
280
- // returns a Effect/RpcMiddlewareV4 with Scope.Scope in requirements
281
- // v4: wrap middleware takes (effect, options) as two params instead of a single options bag
282
- return (
283
- next: Effect.Effect<any, any, any>,
284
- options: {
285
- readonly clientId: number
286
- readonly requestId: RequestId
287
- readonly rpc: Rpc.AnyWithProps
288
- readonly payload: unknown
289
- readonly headers: HttpHeaders.Headers
290
- }
291
- ) => {
292
- // we start with the actual handler
293
- let handler = next
294
-
295
- // inspired from Effect/RpcMiddleware
296
- for (const tag of middlewares) {
297
- // use the tag to get the middleware from context
298
- const middleware = ServiceMap.getUnsafe(context, tag)
299
-
300
- // wrap the current handler, allowing the middleware to run before and after it
301
- handler = PreludeLogger.logDebug("Applying middleware wrap " + tag.key).pipe(
302
- Effect.andThen(middleware(handler, options))
303
- ) as any
304
- }
305
- return handler
278
+ const reversed = middlewares.toReversed()
279
+ const context = yield* Effect.context()
280
+
281
+ // returns a Effect/RpcMiddlewareV4 with Scope.Scope in requirements
282
+ // v4: wrap middleware takes (effect, options) as two params instead of a single options bag
283
+ return (next: Next, options: Options) => {
284
+ // we start with the actual handler
285
+ let handler = next
286
+
287
+ // inspired from Effect/RpcMiddleware
288
+ for (const tag of reversed) {
289
+ // use the tag to get the middleware from context
290
+ const middleware = Context.getUnsafe(context, tag)
291
+
292
+ // wrap the current handler, allowing the middleware to run before and after it
293
+ handler = PreludeLogger.logDebug("Applying middleware wrap " + tag.key).pipe(
294
+ Effect.andThen(middleware(handler, options))
295
+ ) as any
306
296
  }
307
- }) as any
308
- }
297
+ return handler
298
+ }
299
+ })
309
300
 
310
301
  const makeMiddlewareBasic = <Self>() =>
311
302
  // by setting RequestContextMap beforehand, execute contextual typing does not fuck up itself to anys
@@ -321,7 +312,26 @@ const makeMiddlewareBasic = <Self>() =>
321
312
  // reverse middlewares and wrap one after the other
322
313
  const middleware = middlewareMaker(make)
323
314
 
324
- const failures = make.map((_) => _.error).filter(Boolean)
315
+ // Per-middleware error: union of the static `error` on the tag (if any) AND
316
+ // the rcm config entry pointed at by the middleware's `dynamic.key` (if any).
317
+ // Reason: middlewares declared with `dynamic: RequestContextMap.get("foo")`
318
+ // don't set a static `error` field — at runtime their `.error` defaults to
319
+ // `S.Never`. Without pulling from rcm, the composite middleware's
320
+ // `.error` collapses to `Never`, and `Rpc.exitSchema` (which walks
321
+ // `rpc.middlewares[*].error` to build the wire failure union) can't decode
322
+ // the actual middleware-thrown error type. Critical for stream rpcs whose
323
+ // top-level `errorSchema` is force-set to `Never` by effect-rpc.
324
+ const isMeaningfulError = (e: S.Top | undefined): e is S.Top => e !== undefined && e !== null && e !== S.Never
325
+ const rcmRecord = rcm as Record<string, RpcContextMap.Any>
326
+ const failures: Array<S.Top> = make.flatMap((_) => {
327
+ const out: Array<S.Top> = []
328
+ if (isMeaningfulError(_.error)) out.push(_.error)
329
+ const key = _.dynamic?.key as string | undefined
330
+ if (key && rcmRecord[key] && isMeaningfulError(rcmRecord[key].error)) {
331
+ out.push(rcmRecord[key].error)
332
+ }
333
+ return out
334
+ })
325
335
  const provides = make.flatMap((_) => !_.provides ? [] : Array.isArray(_.provides) ? _.provides : [_.provides])
326
336
  const requires = make
327
337
  .flatMap((_) => !_.requires ? [] : Array.isArray(_.requires) ? _.requires : [_.requires])
@@ -357,17 +367,18 @@ const makeMiddlewareBasic = <Self>() =>
357
367
  .effect(
358
368
  MiddlewareMaker,
359
369
  middleware as Effect.Effect<
360
- any,
361
- Effect.Error<typeof middleware>,
362
- Effect.Services<typeof middleware>
370
+ any
363
371
  >
372
+ // todo; they dont change the type..
373
+ // Effect.Error<typeof middleware>,
374
+ // Effect.Services<typeof middleware>
364
375
  )
365
376
 
366
377
  // add to the tag a default implementation
367
378
  return Object.assign(MiddlewareMaker, {
368
379
  layer,
369
380
  // tag to be used to retrieve the RequestContextConfig from Rpc annotations
370
- requestContext: ServiceMap.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>(
381
+ requestContext: Context.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>(
371
382
  "RequestContextConfig"
372
383
  ),
373
384
  requestContextMap: rcm
@@ -379,8 +390,8 @@ export const Tag = <Self>() =>
379
390
  const Id extends string,
380
391
  RequestContextMap extends RequestContextMapTagAny
381
392
  >(id: Id, rcm: RequestContextMap): MiddlewaresBuilder<Self, Id, RequestContextMap["config"]> => {
382
- let allMiddleware: MiddlewareMaker.Any[] = []
383
- const requestContext = ServiceMap.Service<"RequestContextConfig", GetContextConfig<RequestContextMap["config"]>>(
393
+ const allMiddleware: MiddlewareMaker.Any[] = []
394
+ const requestContext = Context.Service<"RequestContextConfig", GetContextConfig<RequestContextMap["config"]>>(
384
395
  "RequestContextConfig"
385
396
  )
386
397
  const it = {
@@ -388,9 +399,9 @@ export const Tag = <Self>() =>
388
399
  // rpc with config
389
400
  rpc: <
390
401
  const Tag extends string,
391
- Payload extends Schema.Top | Schema.Struct.Fields = typeof Schema.Void,
392
- Success extends Schema.Top = typeof Schema.Void,
393
- Error extends Schema.Top = typeof Schema.Never,
402
+ Payload extends S.Top | S.Struct.Fields = typeof S.Void,
403
+ Success extends S.Top = typeof S.Void,
404
+ Error extends S.Top = typeof S.Never,
394
405
  const Stream extends boolean = false,
395
406
  Config extends GetContextConfig<RequestContextMap["config"]> = {}
396
407
  >(tag: Tag, options?: {
@@ -399,35 +410,32 @@ export const Tag = <Self>() =>
399
410
  readonly error?: Error
400
411
  readonly stream?: Stream
401
412
  readonly config?: Config
402
- readonly primaryKey?: [Payload] extends [Schema.Struct.Fields] ? ((
403
- payload: Payload extends Schema.Struct.Fields ? Simplify<Schema.Struct<Payload>["Type"]> : Payload["Type"]
413
+ readonly primaryKey?: [Payload] extends [S.Struct.Fields] ? ((
414
+ payload: Payload extends S.Struct.Fields ? Simplify<S.Struct<Payload>["Type"]> : Payload["Type"]
404
415
  ) => string)
405
416
  : never
406
417
  }):
407
418
  & Rpc.Rpc<
408
419
  Tag,
409
- Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> : Payload,
420
+ Payload extends S.Struct.Fields ? S.Struct<Payload> : Payload,
410
421
  // TODO: enhance `Error`. type based on middleware config.
411
422
  Stream extends true ? RpcSchema.Stream<Success, Error> : Success,
412
- Stream extends true ? typeof Schema.Never : Error
423
+ Stream extends true ? typeof S.Never : Error
413
424
  >
414
425
  & { config: Config } =>
415
426
  {
416
427
  const config = options?.config ?? {} as Config
417
428
 
418
- // based on the config, we must enhance (union) or set failures.
419
- // TODO: we should only include errors that are relevant based on the middleware config.ks
420
- const error = options?.error
421
- const errors = typedValuesOf(rcm.config).map((_) => _.error).filter((_) => _ && _ !== S.Never) // TODO: only the errors relevant based on config
422
- const allErrors = error ? [error, ...errors] : errors
423
- const [firstError, ...restErrors] = allErrors
424
- const newError = firstError ? S.Union([firstError, ...restErrors]) : S.Never
425
-
429
+ // The rpc's `error` schema carries ONLY the request's own declared errors.
430
+ // Middleware errors (rcm-derived) reach the wire via the middleware tag
431
+ // attached to the rpc group later (`RpcGroup.middleware(...)` at the
432
+ // routing/client level), and are unioned into the failure schema by
433
+ // `Rpc.exitSchema`'s `rpc.middlewares[*].error` walk.
426
434
  // @ts-expect-error — TypeScript can't prove Simplify<T> ≡ { [K in keyof T]: T[K] } for unresolved generics (primaryKey)
427
435
  const rpc = Rpc.make(tag, {
428
436
  ...options?.payload !== undefined ? { payload: options.payload } : {},
429
437
  ...options?.success !== undefined ? { success: options.success } : {},
430
- error: newError,
438
+ ...options?.error !== undefined ? { error: options.error } : {},
431
439
  ...options?.stream !== undefined ? { stream: options.stream } : {},
432
440
  ...options?.primaryKey !== undefined ? { primaryKey: options.primaryKey } : {}
433
441
  }) as any
@@ -437,7 +445,7 @@ export const Tag = <Self>() =>
437
445
  middleware: (...middlewares: any[]) => {
438
446
  for (const mw of middlewares) {
439
447
  // recall that we run middlewares in reverse order
440
- allMiddleware = [mw, ...allMiddleware]
448
+ allMiddleware.unshift(mw)
441
449
  }
442
450
  return allMiddleware.filter((m) => !!m.dynamic).length !== Object.keys(rcm.config).length
443
451
  // for sure, until all the dynamic middlewares are provided it's non sensical to call makeMiddlewareBasic
package/src/rpc/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The extensions use V4 format of RPC middleware:
4
4
 
5
5
  - supports `requires` besides `provides`
6
- - `requires` and `provides` should be set as second generic argument: `Tag<Self, Config>`.
6
+ - `requires` and `provides` should be set as second generic argument: `Tag<Self, Config>`.
7
7
  - `wrap: true` is the default, there is no classic `provides: Tag`
8
8
 
9
9
  ## Features
@@ -34,7 +34,7 @@ NOTE: perhaps not as useful anymore if support for dynamic middleware gets integ
34
34
 
35
35
  ## Examples
36
36
 
37
- See [tests](../../../infra/test/rpc-multi-middleware.test.ts)
37
+ See [tests](../../../infra/test/rpc-multi-middleware.test.ts)
38
38
 
39
39
  ## Future
40
40