effect-app 4.0.0-beta.18 → 4.0.0-beta.180

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 (218) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/Array.d.ts +1 -1
  3. package/dist/Chunk.d.ts +1 -1
  4. package/dist/Chunk.d.ts.map +1 -1
  5. package/dist/Config/SecretURL.d.ts +1 -1
  6. package/dist/Config/SecretURL.d.ts.map +1 -1
  7. package/dist/Config/SecretURL.js +2 -2
  8. package/dist/Config/internal/configSecretURL.d.ts +1 -1
  9. package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
  10. package/dist/Config.d.ts +7 -0
  11. package/dist/Config.d.ts.map +1 -0
  12. package/dist/Config.js +6 -0
  13. package/dist/ConfigProvider.d.ts +39 -0
  14. package/dist/ConfigProvider.d.ts.map +1 -0
  15. package/dist/ConfigProvider.js +42 -0
  16. package/dist/Context.d.ts +40 -0
  17. package/dist/Context.d.ts.map +1 -0
  18. package/dist/Context.js +67 -0
  19. package/dist/Effect.d.ts +9 -10
  20. package/dist/Effect.d.ts.map +1 -1
  21. package/dist/Effect.js +3 -6
  22. package/dist/Function.d.ts +1 -1
  23. package/dist/Function.d.ts.map +1 -1
  24. package/dist/Inputify.type.d.ts +1 -1
  25. package/dist/Layer.d.ts +6 -5
  26. package/dist/Layer.d.ts.map +1 -1
  27. package/dist/Layer.js +1 -1
  28. package/dist/NonEmptySet.d.ts +1 -1
  29. package/dist/NonEmptySet.d.ts.map +1 -1
  30. package/dist/Operations.d.ts +369 -47
  31. package/dist/Operations.d.ts.map +1 -1
  32. package/dist/Operations.js +10 -10
  33. package/dist/Option.d.ts +1 -1
  34. package/dist/Option.d.ts.map +1 -1
  35. package/dist/Pure.d.ts +5 -5
  36. package/dist/Pure.d.ts.map +1 -1
  37. package/dist/Pure.js +13 -13
  38. package/dist/Schema/Class.d.ts +69 -20
  39. package/dist/Schema/Class.d.ts.map +1 -1
  40. package/dist/Schema/Class.js +190 -22
  41. package/dist/Schema/FastCheck.d.ts +1 -1
  42. package/dist/Schema/FastCheck.d.ts.map +1 -1
  43. package/dist/Schema/Methods.d.ts +1 -1
  44. package/dist/Schema/SchemaParser.d.ts +5 -0
  45. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  46. package/dist/Schema/SchemaParser.js +6 -0
  47. package/dist/Schema/SpecialJsonSchema.d.ts +33 -0
  48. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  49. package/dist/Schema/SpecialJsonSchema.js +122 -0
  50. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  51. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  52. package/dist/Schema/SpecialOpenApi.js +123 -0
  53. package/dist/Schema/brand.d.ts +7 -2
  54. package/dist/Schema/brand.d.ts.map +1 -1
  55. package/dist/Schema/brand.js +1 -1
  56. package/dist/Schema/email.d.ts +1 -1
  57. package/dist/Schema/email.d.ts.map +1 -1
  58. package/dist/Schema/email.js +7 -4
  59. package/dist/Schema/ext.d.ts +118 -45
  60. package/dist/Schema/ext.d.ts.map +1 -1
  61. package/dist/Schema/ext.js +129 -53
  62. package/dist/Schema/moreStrings.d.ts +111 -11
  63. package/dist/Schema/moreStrings.d.ts.map +1 -1
  64. package/dist/Schema/moreStrings.js +14 -15
  65. package/dist/Schema/numbers.d.ts +127 -15
  66. package/dist/Schema/numbers.d.ts.map +1 -1
  67. package/dist/Schema/numbers.js +10 -12
  68. package/dist/Schema/phoneNumber.d.ts +1 -1
  69. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  70. package/dist/Schema/phoneNumber.js +6 -3
  71. package/dist/Schema/schema.d.ts +1 -1
  72. package/dist/Schema/strings.d.ts +37 -5
  73. package/dist/Schema/strings.d.ts.map +1 -1
  74. package/dist/Schema/strings.js +1 -5
  75. package/dist/Schema.d.ts +102 -56
  76. package/dist/Schema.d.ts.map +1 -1
  77. package/dist/Schema.js +128 -64
  78. package/dist/Set.d.ts +1 -1
  79. package/dist/Set.d.ts.map +1 -1
  80. package/dist/TypeTest.d.ts +1 -1
  81. package/dist/Types.d.ts +1 -1
  82. package/dist/Widen.type.d.ts +1 -1
  83. package/dist/_ext/Array.d.ts +1 -1
  84. package/dist/_ext/Array.d.ts.map +1 -1
  85. package/dist/_ext/date.d.ts +1 -1
  86. package/dist/_ext/misc.d.ts +1 -1
  87. package/dist/_ext/ord.ext.d.ts +1 -1
  88. package/dist/_ext/ord.ext.d.ts.map +1 -1
  89. package/dist/builtin.d.ts +1 -1
  90. package/dist/builtin.d.ts.map +1 -1
  91. package/dist/client/InvalidationKeys.d.ts +29 -0
  92. package/dist/client/InvalidationKeys.d.ts.map +1 -0
  93. package/dist/client/InvalidationKeys.js +33 -0
  94. package/dist/client/apiClientFactory.d.ts +17 -31
  95. package/dist/client/apiClientFactory.d.ts.map +1 -1
  96. package/dist/client/apiClientFactory.js +81 -26
  97. package/dist/client/clientFor.d.ts +63 -10
  98. package/dist/client/clientFor.d.ts.map +1 -1
  99. package/dist/client/clientFor.js +9 -1
  100. package/dist/client/errors.d.ts +49 -25
  101. package/dist/client/errors.d.ts.map +1 -1
  102. package/dist/client/errors.js +43 -17
  103. package/dist/client/makeClient.d.ts +360 -30
  104. package/dist/client/makeClient.d.ts.map +1 -1
  105. package/dist/client/makeClient.js +63 -23
  106. package/dist/client.d.ts +2 -1
  107. package/dist/client.d.ts.map +1 -1
  108. package/dist/client.js +2 -1
  109. package/dist/faker.d.ts +1 -1
  110. package/dist/faker.d.ts.map +1 -1
  111. package/dist/http/Request.d.ts +2 -2
  112. package/dist/http/Request.d.ts.map +1 -1
  113. package/dist/http/Request.js +5 -5
  114. package/dist/http/internal/lib.d.ts +1 -1
  115. package/dist/http.d.ts +1 -1
  116. package/dist/ids.d.ts +3 -3
  117. package/dist/ids.d.ts.map +1 -1
  118. package/dist/ids.js +3 -2
  119. package/dist/index.d.ts +5 -8
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +6 -8
  122. package/dist/logger.d.ts +1 -1
  123. package/dist/middleware.d.ts +16 -9
  124. package/dist/middleware.d.ts.map +1 -1
  125. package/dist/middleware.js +13 -9
  126. package/dist/rpc/Invalidation.d.ts +490 -0
  127. package/dist/rpc/Invalidation.d.ts.map +1 -0
  128. package/dist/rpc/Invalidation.js +129 -0
  129. package/dist/rpc/MiddlewareMaker.d.ts +5 -4
  130. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  131. package/dist/rpc/MiddlewareMaker.js +26 -27
  132. package/dist/rpc/RpcContextMap.d.ts +3 -3
  133. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  134. package/dist/rpc/RpcContextMap.js +4 -4
  135. package/dist/rpc/RpcMiddleware.d.ts +5 -4
  136. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  137. package/dist/rpc/RpcMiddleware.js +1 -1
  138. package/dist/rpc.d.ts +2 -2
  139. package/dist/rpc.d.ts.map +1 -1
  140. package/dist/rpc.js +2 -2
  141. package/dist/transform.d.ts +1 -1
  142. package/dist/transform.d.ts.map +1 -1
  143. package/dist/transform.js +3 -3
  144. package/dist/utils/effectify.d.ts +1 -1
  145. package/dist/utils/extend.d.ts +1 -1
  146. package/dist/utils/extend.d.ts.map +1 -1
  147. package/dist/utils/gen.d.ts +2 -2
  148. package/dist/utils/gen.d.ts.map +1 -1
  149. package/dist/utils/logLevel.d.ts +2 -2
  150. package/dist/utils/logLevel.d.ts.map +1 -1
  151. package/dist/utils/logger.d.ts +3 -3
  152. package/dist/utils/logger.d.ts.map +1 -1
  153. package/dist/utils/logger.js +3 -3
  154. package/dist/utils.d.ts +30 -10
  155. package/dist/utils.d.ts.map +1 -1
  156. package/dist/utils.js +10 -4
  157. package/dist/validation/validators.d.ts +1 -1
  158. package/dist/validation/validators.d.ts.map +1 -1
  159. package/dist/validation.d.ts +1 -1
  160. package/dist/validation.d.ts.map +1 -1
  161. package/eslint.config.mjs +2 -2
  162. package/package.json +47 -19
  163. package/src/Config/SecretURL.ts +2 -1
  164. package/src/Config.ts +14 -0
  165. package/src/ConfigProvider.ts +48 -0
  166. package/src/{ServiceMap.ts → Context.ts} +52 -59
  167. package/src/Effect.ts +12 -14
  168. package/src/Layer.ts +5 -4
  169. package/src/Operations.ts +14 -14
  170. package/src/Pure.ts +17 -18
  171. package/src/Schema/Class.ts +279 -62
  172. package/src/Schema/SchemaParser.ts +12 -0
  173. package/src/Schema/SpecialJsonSchema.ts +137 -0
  174. package/src/Schema/SpecialOpenApi.ts +130 -0
  175. package/src/Schema/brand.ts +9 -1
  176. package/src/Schema/email.ts +7 -2
  177. package/src/Schema/ext.ts +210 -80
  178. package/src/Schema/moreStrings.ts +22 -20
  179. package/src/Schema/numbers.ts +14 -16
  180. package/src/Schema/phoneNumber.ts +5 -1
  181. package/src/Schema/strings.ts +4 -8
  182. package/src/Schema.ts +265 -105
  183. package/src/client/InvalidationKeys.ts +50 -0
  184. package/src/client/apiClientFactory.ts +203 -119
  185. package/src/client/clientFor.ts +112 -23
  186. package/src/client/errors.ts +52 -26
  187. package/src/client/makeClient.ts +339 -63
  188. package/src/client.ts +1 -0
  189. package/src/http/Request.ts +7 -4
  190. package/src/ids.ts +2 -1
  191. package/src/index.ts +5 -10
  192. package/src/middleware.ts +12 -10
  193. package/src/rpc/Invalidation.ts +174 -0
  194. package/src/rpc/MiddlewareMaker.ts +36 -47
  195. package/src/rpc/README.md +2 -2
  196. package/src/rpc/RpcContextMap.ts +6 -5
  197. package/src/rpc/RpcMiddleware.ts +5 -4
  198. package/src/rpc.ts +1 -1
  199. package/src/transform.ts +2 -2
  200. package/src/utils/gen.ts +1 -1
  201. package/src/utils/logger.ts +2 -2
  202. package/src/utils.ts +47 -11
  203. package/test/dist/rpc.test.d.ts.map +1 -1
  204. package/test/dist/secretURL.test.d.ts.map +1 -0
  205. package/test/dist/special.test.d.ts.map +1 -0
  206. package/test/rpc.test.ts +38 -6
  207. package/test/schema.test.ts +591 -17
  208. package/test/secretURL.test.ts +157 -0
  209. package/test/special.test.ts +1023 -0
  210. package/test/utils.test.ts +6 -6
  211. package/tsconfig.base.json +3 -4
  212. package/tsconfig.json +0 -1
  213. package/tsconfig.json.bak +2 -2
  214. package/tsconfig.src.json +29 -29
  215. package/tsconfig.test.json +2 -2
  216. package/dist/ServiceMap.d.ts +0 -44
  217. package/dist/ServiceMap.d.ts.map +0 -1
  218. package/dist/ServiceMap.js +0 -91
@@ -1,3 +1,4 @@
1
+ import { Option } from "effect"
1
2
  import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"
2
3
  import * as Effect from "../Effect.js"
3
4
  import { HttpClient, HttpClientError, HttpClientRequest, HttpHeaders } from "./internal/lib.js"
@@ -24,16 +25,18 @@ export const demandJson = (client: HttpClient.HttpClient) =>
24
25
  .mapRequest(client, (_) => HttpClientRequest.acceptJson(_))
25
26
  .pipe(HttpClient.transform((r, request) =>
26
27
  Effect.tap(r, (response) =>
27
- HttpHeaders
28
- .get(response.headers, "Content-Type")
29
- ?.startsWith("application/json")
28
+ Option
29
+ .exists(
30
+ HttpHeaders.get(response.headers, "Content-Type"),
31
+ (_) => _.startsWith("application/json")
32
+ )
30
33
  ? Effect.void
31
34
  : Effect.fail(
32
35
  new HttpClientError.DecodeError({
33
36
  request,
34
37
  response,
35
38
  description: "not json response: "
36
- + HttpHeaders.get(response.headers, "Content-Type")
39
+ + Option.getOrElse(HttpHeaders.get(response.headers, "Content-Type"), () => "<missing>")
37
40
  })
38
41
  ))
39
42
  ))
package/src/ids.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Effect } from "effect"
1
2
  import { brandedStringId, type Codec, NonEmptyString255, StringId, type StringIdBrand, withDefaultMake } from "effect-app/Schema"
2
3
  import type { B } from "effect-app/Schema/schema"
3
4
  import type { Simplify } from "effect/Types"
@@ -17,7 +18,7 @@ export const RequestId = extendM(
17
18
  const make = StringId.make as () => NonEmptyString255
18
19
  return ({
19
20
  make,
20
- withDefault: s.pipe(S.withDefaultConstructor(make))
21
+ withDefault: S.withConstructorDefault(Effect.sync(make))(s as typeof s & S.WithoutConstructorDefault)
21
22
  })
22
23
  }
23
24
  )
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,24 +1,26 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { ServiceMap } from "effect-app"
3
- import { RpcX } from "./rpc.js"
2
+ import { Context } from "effect-app"
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") {}
14
+
15
+ export class DevModeMiddleware extends RpcMiddleware.Tag<DevModeMiddleware>()("DevModeMiddleware") {}
16
16
 
17
- export class DevModeMiddleware extends RpcX.RpcMiddleware.Tag<DevModeMiddleware>()("DevModeMiddleware") {}
17
+ /** RPC middleware that reads the `Invalidates` annotation and populates `InvalidationSet` before the handler runs. */
18
+ export class InvalidationMiddleware extends RpcMiddleware.Tag<InvalidationMiddleware>()("InvalidationMiddleware") {}
18
19
 
19
20
  export const DefaultGenericMiddlewares = [
20
21
  RequestCacheMiddleware,
21
22
  ConfigureInterruptibilityMiddleware,
22
23
  LoggerMiddleware,
23
- DevModeMiddleware
24
+ DevModeMiddleware,
25
+ InvalidationMiddleware
24
26
  ] as const
@@ -0,0 +1,174 @@
1
+ import * as Ref from "effect/Ref"
2
+ import * as Context from "../Context.js"
3
+ import * as Effect from "../Effect.js"
4
+ import * as S from "../Schema.js"
5
+
6
+ /**
7
+ * A single segment within an `InvalidationKey` array.
8
+ * Accepts any JSON-compatible value: string, number, boolean, null,
9
+ * arrays and objects recursively — matching TanStack Query's `queryKey` element type.
10
+ */
11
+ export const InvalidationKeySegment = S.Json
12
+ export type InvalidationKeySegment = S.Schema.Type<typeof InvalidationKeySegment>
13
+
14
+ /** Schema for a single invalidation key – an array of segments compatible with TanStack Query `queryKey`. */
15
+ export const InvalidationKey = S.Array(InvalidationKeySegment)
16
+ export type InvalidationKey = S.Schema.Type<typeof InvalidationKey>
17
+
18
+ /** Schema for the full set of invalidation keys – an array of `InvalidationKey`. */
19
+ export const InvalidationKeys = S.Array(InvalidationKey)
20
+ export type InvalidationKeys = S.Schema.Type<typeof InvalidationKeys>
21
+
22
+ /** Metadata included in every command response for server-driven cache invalidation. */
23
+ export const CommandMetaData = S.Struct({ invalidateQueries: InvalidationKeys })
24
+ export type CommandMetaData = S.Schema.Type<typeof CommandMetaData>
25
+
26
+ /**
27
+ * Wraps a command's success schema so that the wire format carries both the `payload`
28
+ * (the handler's actual return value) and `metadata` (server-driven cache invalidation keys).
29
+ * Transparent to users: the server handler returns the plain payload and the client receives
30
+ * the plain payload — wrapping/unwrapping is handled internally by the routing layer.
31
+ */
32
+ export const CommandResponseWithMetaData = <S extends S.Top>(success: S) =>
33
+ S.Struct({ payload: success, metadata: CommandMetaData })
34
+
35
+ /**
36
+ * Wraps a command's failure schema so that the wire format carries both the `error`
37
+ * (the handler's actual failure value) and `metadata` (server-driven cache invalidation keys
38
+ * accumulated thus far before the failure occurred).
39
+ * Transparent to users: the server handler fails with the plain error and the client receives
40
+ * the plain error — wrapping/unwrapping is handled internally by the routing layer.
41
+ */
42
+ export const CommandFailureWithMetaData = <E extends S.Top>(error: E) =>
43
+ S.Struct({ _tag: S.Literal("CommandFailureWithMetaData"), error, metadata: CommandMetaData })
44
+
45
+ /**
46
+ * Stream chunk schema for stream responses with metadata.
47
+ * Each item is either a data value, an intermediate "metadata" signal carrying cache
48
+ * invalidation keys accumulated since the previous drain, or a final "done" signal.
49
+ * Transparent to users: stream handlers return plain values and clients receive plain values —
50
+ * wrapping/unwrapping is handled internally by the routing layer.
51
+ *
52
+ * The "done" chunk is always the last item in the stream and carries any remaining invalidation
53
+ * keys. An optional "metadata" chunk may appear after any "value" chunk and carries keys
54
+ * accumulated since the last drain (V3: mid-stream invalidation).
55
+ */
56
+ export const StreamResponseChunk = <S extends S.Top>(success: S) =>
57
+ S.Union([
58
+ S.Struct({ _tag: S.Literal("value"), value: success }),
59
+ S.Struct({ _tag: S.Literal("metadata"), metadata: CommandMetaData }),
60
+ S.Struct({ _tag: S.Literal("done"), metadata: CommandMetaData })
61
+ ])
62
+
63
+ export type StreamResponseChunk<A> =
64
+ | { readonly _tag: "value"; readonly value: A }
65
+ | { readonly _tag: "metadata"; readonly metadata: CommandMetaData }
66
+ | { readonly _tag: "done"; readonly metadata: CommandMetaData }
67
+
68
+ /**
69
+ * Stream chunk schema for stream failures with metadata.
70
+ * Used to signal a stream failure while still carrying cache invalidation keys
71
+ * accumulated thus far.
72
+ */
73
+ export const StreamFailureChunk = <E extends S.Top>(error: E) =>
74
+ S.Struct({ _tag: S.Literal("error"), error, metadata: CommandMetaData })
75
+
76
+ export type StreamFailureChunk<E> = { readonly _tag: "error"; readonly error: E; readonly metadata: CommandMetaData }
77
+
78
+ /**
79
+ * Context annotation for declaring static cache invalidation keys on a low-level `Rpc` definition.
80
+ * These keys are always included in the command response metadata, regardless of the handler logic.
81
+ *
82
+ * Prefer using `makeQueryKey` over raw string arrays to stay in sync with the actual query
83
+ * definitions without manual string maintenance:
84
+ *
85
+ * ```ts
86
+ * import { makeQueryKey } from "effect-app/client"
87
+ * import { Invalidation } from "effect-app/rpc"
88
+ * import * as UserRsc from "../User/index.js" // separate module to avoid circular deps
89
+ *
90
+ * class UpdateProfile extends Rpc.make("UpdateProfile", { ... })
91
+ * .annotate(Invalidation.Invalidates, [makeQueryKey(UserRsc.GetMe), makeQueryKey(UserRsc.GetProfile)]) {}
92
+ * ```
93
+ *
94
+ * **Circular dependency note:** if mutations and queries live in the same file you may hit a
95
+ * circular reference at evaluation time. The idiomatic fix is to move mutations into their own
96
+ * module (e.g. `User/mutations.ts`) that directly imports the relevant query classes rather than
97
+ * re-exporting them through a barrel.
98
+ *
99
+ * For the higher-level `Command`/`Query` builders from `makeRpcClient`, use the
100
+ * `invalidatesQueries` callback argument instead (it receives the same query keys at runtime).
101
+ */
102
+ export const Invalidates = Context.Reference<ReadonlyArray<InvalidationKey>>(
103
+ "effect-app/rpc/Invalidates",
104
+ { defaultValue: () => [] }
105
+ )
106
+ export type Invalidates = typeof Invalidates
107
+
108
+ /** The shape of the per-request service that accumulates invalidation keys. */
109
+ export interface InvalidationSetService {
110
+ readonly add: (key: InvalidationKey) => Effect.Effect<void>
111
+ readonly get: Effect.Effect<ReadonlyArray<InvalidationKey>>
112
+ /**
113
+ * V3: Reads all currently accumulated keys and resets the bucket to empty.
114
+ * Used by the stream routing layer to emit intermediate "metadata" chunks
115
+ * without re-sending keys that have already been forwarded to the client.
116
+ */
117
+ readonly drain: Effect.Effect<ReadonlyArray<InvalidationKey>>
118
+ }
119
+
120
+ /**
121
+ * Request-scoped service for accumulating invalidation keys dynamically inside a handler.
122
+ * Provided by `InvalidationMiddlewareLive` for every RPC call; has a no-op default so it is
123
+ * safe to use even when the HTTP middleware is absent (tests, workers, etc.).
124
+ *
125
+ * Use `InvalidationSet.use(_ => _.add(key))` (or `.useSync` for non-Effect callbacks) as a
126
+ * shorthand instead of yielding the service manually.
127
+ *
128
+ * Prefer `makeQueryKey` over raw string arrays so invalidation keys stay in sync with the
129
+ * actual query definitions automatically:
130
+ *
131
+ * ```ts
132
+ * import { makeQueryKey } from "effect-app/client"
133
+ * import { Effect } from "effect"
134
+ * import { Invalidation } from "effect-app/rpc"
135
+ * import * as CartRsc from "../Cart/queries.js"
136
+ * import * as UserRsc from "../User/queries.js"
137
+ *
138
+ * const handler = Effect.fnUntraced(function*(req: UpdateCartRequest) {
139
+ * const cart = yield* CartRepo.save(req.cart)
140
+ *
141
+ * // Stage 1 – unconditional: always invalidate after saving
142
+ * yield* Invalidation.InvalidationSet.use(_ => _.add(makeQueryKey(UserRsc.GetMe)))
143
+ *
144
+ * // Stage 2 – conditional: only if the cart changed state
145
+ * if (cart.isCheckedOut) {
146
+ * yield* Invalidation.InvalidationSet.use(_ => _.add(makeQueryKey(CartRsc.GetCartStats)))
147
+ * }
148
+ *
149
+ * return cart
150
+ * })
151
+ * ```
152
+ *
153
+ * You can combine static (`Invalidates` annotation) and dynamic (`InvalidationSet.use`) keys:
154
+ * the annotation pre-populates the set before the handler runs; dynamic additions accumulate
155
+ * throughout the handler. All keys are included in the command response metadata.
156
+ */
157
+ export const InvalidationSet = Context.Reference<InvalidationSetService>(
158
+ "effect-app/rpc/InvalidationSet",
159
+ {
160
+ defaultValue: () => ({
161
+ add: (_key: InvalidationKey) => Effect.void,
162
+ get: Effect.succeed([] as ReadonlyArray<InvalidationKey>),
163
+ drain: Effect.succeed([] as ReadonlyArray<InvalidationKey>)
164
+ })
165
+ }
166
+ )
167
+ export type InvalidationSet = typeof InvalidationSet
168
+
169
+ /** Creates a fresh `InvalidationSet` implementation backed by a `Ref`. */
170
+ export const makeInvalidationSet = (ref: Ref.Ref<ReadonlyArray<InvalidationKey>>): InvalidationSetService => ({
171
+ add: (key) => Ref.update(ref, (keys) => [...keys, key]),
172
+ get: Ref.get(ref),
173
+ drain: Ref.getAndSet(ref, [])
174
+ })
@@ -1,11 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { Effect, Layer, type Schema, Schema as S, type Scope, ServiceMap } from "effect"
2
+ import { Effect, Layer, type Schema, Schema as S, type Scope } from "effect"
3
3
  import { type NonEmptyArray, type NonEmptyReadonlyArray } from "effect/Array"
4
4
  import { type Simplify } from "effect/Types"
5
5
  import { Rpc, type RpcGroup, type RpcSchema } from "effect/unstable/rpc"
6
6
  import { type HandlersFrom } from "effect/unstable/rpc/RpcGroup"
7
- import { type RequestId } from "effect/unstable/rpc/RpcMessage"
8
- import { type HttpHeaders } from "../http.js"
7
+ import * as Context from "../Context.js"
9
8
  import { PreludeLogger } from "../logger.js"
10
9
  import { type TypeTestId } from "../TypeTest.js"
11
10
  import { typedValuesOf } from "../utils.js"
@@ -61,13 +60,13 @@ export interface MiddlewareMaker<
61
60
  }
62
61
  >
63
62
  {
64
- readonly layer: Layer.Layer<Self, never, ServiceMap.Service.Identifier<MiddlewareProviders[number]>>
63
+ readonly layer: Layer.Layer<Self, never, Context.Service.Identifier<MiddlewareProviders[number]>>
65
64
  readonly requestContext: RequestContextTag<RequestContextMap>
66
65
  readonly requestContextMap: RequestContextMap
67
66
  }
68
67
 
69
68
  export interface RequestContextTag<RequestContextMap extends Record<string, RpcContextMap.Any>>
70
- extends ServiceMap.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>
69
+ extends Context.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>
71
70
  {}
72
71
 
73
72
  export namespace MiddlewareMaker {
@@ -258,10 +257,10 @@ export type MiddlewaresBuilder<
258
257
  : { new(_: never): {} }
259
258
  : { new(_: never): {} })
260
259
 
261
- const middlewareMaker = <
260
+ const middlewareMaker = Effect.fnUntraced(function*<
262
261
  MiddlewareProviders extends ReadonlyArray<MiddlewareMaker.Any>
263
- >(middlewares: MiddlewareProviders): Effect.Effect<
264
- RpcMiddlewareV4<
262
+ >(middlewares: MiddlewareProviders) {
263
+ type Middleware = RpcMiddlewareV4<
265
264
  MiddlewareMaker.ManyProvided<MiddlewareProviders>,
266
265
  MiddlewareMaker.ManyErrors<MiddlewareProviders>,
267
266
  Exclude<
@@ -270,42 +269,32 @@ const middlewareMaker = <
270
269
  > extends never ? never
271
270
  : Exclude<MiddlewareMaker.ManyRequired<MiddlewareProviders>, MiddlewareMaker.ManyProvided<MiddlewareProviders>>
272
271
  >
273
- > => {
272
+ type Next = Parameters<Middleware>[0]
273
+ type Options = Parameters<Middleware>[1]
274
+
274
275
  // 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
276
+ const reversed = middlewares.toReversed()
277
+ const context = yield* Effect.context()
278
+
279
+ // returns a Effect/RpcMiddlewareV4 with Scope.Scope in requirements
280
+ // v4: wrap middleware takes (effect, options) as two params instead of a single options bag
281
+ return (next: Next, options: Options) => {
282
+ // we start with the actual handler
283
+ let handler = next
284
+
285
+ // inspired from Effect/RpcMiddleware
286
+ for (const tag of reversed) {
287
+ // use the tag to get the middleware from context
288
+ const middleware = Context.getUnsafe(context, tag)
289
+
290
+ // wrap the current handler, allowing the middleware to run before and after it
291
+ handler = PreludeLogger.logDebug("Applying middleware wrap " + tag.key).pipe(
292
+ Effect.andThen(middleware(handler, options))
293
+ ) as any
306
294
  }
307
- }) as any
308
- }
295
+ return handler
296
+ }
297
+ })
309
298
 
310
299
  const makeMiddlewareBasic = <Self>() =>
311
300
  // by setting RequestContextMap beforehand, execute contextual typing does not fuck up itself to anys
@@ -321,7 +310,7 @@ const makeMiddlewareBasic = <Self>() =>
321
310
  // reverse middlewares and wrap one after the other
322
311
  const middleware = middlewareMaker(make)
323
312
 
324
- const failures = make.map((_) => _.error).filter(Boolean)
313
+ const failures = make.flatMap((_) => _.error ? [_.error] : [])
325
314
  const provides = make.flatMap((_) => !_.provides ? [] : Array.isArray(_.provides) ? _.provides : [_.provides])
326
315
  const requires = make
327
316
  .flatMap((_) => !_.requires ? [] : Array.isArray(_.requires) ? _.requires : [_.requires])
@@ -367,7 +356,7 @@ const makeMiddlewareBasic = <Self>() =>
367
356
  return Object.assign(MiddlewareMaker, {
368
357
  layer,
369
358
  // tag to be used to retrieve the RequestContextConfig from Rpc annotations
370
- requestContext: ServiceMap.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>(
359
+ requestContext: Context.Service<"RequestContextConfig", GetContextConfig<RequestContextMap>>(
371
360
  "RequestContextConfig"
372
361
  ),
373
362
  requestContextMap: rcm
@@ -379,8 +368,8 @@ export const Tag = <Self>() =>
379
368
  const Id extends string,
380
369
  RequestContextMap extends RequestContextMapTagAny
381
370
  >(id: Id, rcm: RequestContextMap): MiddlewaresBuilder<Self, Id, RequestContextMap["config"]> => {
382
- let allMiddleware: MiddlewareMaker.Any[] = []
383
- const requestContext = ServiceMap.Service<"RequestContextConfig", GetContextConfig<RequestContextMap["config"]>>(
371
+ const allMiddleware: MiddlewareMaker.Any[] = []
372
+ const requestContext = Context.Service<"RequestContextConfig", GetContextConfig<RequestContextMap["config"]>>(
384
373
  "RequestContextConfig"
385
374
  )
386
375
  const it = {
@@ -437,7 +426,7 @@ export const Tag = <Self>() =>
437
426
  middleware: (...middlewares: any[]) => {
438
427
  for (const mw of middlewares) {
439
428
  // recall that we run middlewares in reverse order
440
- allMiddleware = [mw, ...allMiddleware]
429
+ allMiddleware.unshift(mw)
441
430
  }
442
431
  return allMiddleware.filter((m) => !!m.dynamic).length !== Object.keys(rcm.config).length
443
432
  // 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
 
@@ -2,14 +2,15 @@
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-return */
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
4
 
5
- import { type Schema as S, ServiceMap } from "effect"
5
+ import { type Schema as S } from "effect"
6
6
  import { type AnyWithProps } from "effect/unstable/rpc/Rpc"
7
+ import * as Context from "../Context.js"
7
8
  import { type RpcDynamic } from "./RpcMiddleware.js"
8
9
 
9
10
  type Values<T extends Record<any, any>> = T[keyof T]
10
11
 
11
12
  /**
12
- * Middleware is inactivate by default, the Key is optional in route context, and the service is optionally provided as Effect ServiceMap.
13
+ * Middleware is inactivate by default, the Key is optional in route context, and the service is optionally provided as Effect Context.
13
14
  * Unless explicitly configured as `true`.
14
15
  */
15
16
  export type RpcContextMap<Service, E> = {
@@ -22,7 +23,7 @@ export type RpcContextMap<Service, E> = {
22
23
 
23
24
  export declare namespace RpcContextMap {
24
25
  /**
25
- * Middleware is active by default, and provides the Service at Key in route context, and the Service is provided as Effect ServiceMap.
26
+ * Middleware is active by default, and provides the Service at Key in route context, and the Service is provided as Effect Context.
26
27
  * Unless explicitly omitted.
27
28
  */
28
29
  export type Inverted<Service, E> = {
@@ -97,7 +98,7 @@ export type GetEffectError<RequestContextMap extends Record<string, RpcContextMa
97
98
  }
98
99
  >
99
100
 
100
- const tag = ServiceMap.Service("RequestContextConfig")
101
+ const tag = Context.Service("RequestContextConfig")
101
102
 
102
103
  export const makeMap = <const Config extends Record<string, RpcContextMap.Any>>(config: Config) => {
103
104
  const cls = class {
@@ -109,7 +110,7 @@ export const makeMap = <const Config extends Record<string, RpcContextMap.Any>>(
109
110
  return Object.assign(cls, {
110
111
  config, /** Retrieves RequestContextConfig out of the Rpc annotations */
111
112
  getConfig: (rpc: AnyWithProps): GetContextConfig<Config> => {
112
- return ServiceMap.getOrElse(rpc.annotations, tag as any, () => ({}))
113
+ return Context.getOrElse(rpc.annotations, tag as any, () => ({}))
113
114
  },
114
115
  /** Adapter used when setting the dynamic prop on a middleware implementation */
115
116
  get: <
@@ -1,10 +1,11 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-return */
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
- import { type Effect, type Schema, type Schema as S, type Scope, type ServiceMap, type Stream } from "effect"
4
+ import { type Effect, type Schema, type Schema as S, type Scope, type Stream } from "effect"
5
5
  import { type NonEmptyReadonlyArray } from "effect/Array"
6
6
  import { type Rpc, RpcMiddleware } from "effect/unstable/rpc"
7
7
  import { type TypeId } from "effect/unstable/rpc/RpcMiddleware"
8
+ import type * as Context from "../Context.js"
8
9
  import { type GetEffectContext, type RpcContextMap } from "./RpcContextMap.js"
9
10
 
10
11
  export type RpcMiddlewareV4<Provides, E, Requires> = RpcMiddleware.RpcMiddleware<Provides, E, Requires>
@@ -102,8 +103,8 @@ export declare namespace TagClass {
102
103
  requires?: any
103
104
  provides?: any
104
105
  }
105
- > extends ServiceMap.Service<Self, Service> {
106
- new(_: never): ServiceMap.ServiceClass.Shape<Name, Service>
106
+ > extends Context.Service<Self, Service> {
107
+ new(_: never): Context.ServiceClass.Shape<Name, Service>
107
108
  readonly [TypeId]: TypeId
108
109
  readonly optional: Optional<Options>
109
110
  readonly error: FailureSchema<Options>
@@ -226,7 +227,7 @@ export type ExtractProvides<R extends Rpc.Any, Tag extends string> = R extends
226
227
  Rpc.Rpc<Tag, infer _Payload, infer _Success, infer _Error, infer _Middleware, infer _Requires> ? _Middleware extends {
227
228
  readonly provides: infer _P
228
229
  } ? [_P] extends [never] ? never
229
- : _P /*_P extends ServiceMap.Service<infer _I, infer _S> ? _I
230
+ : _P /*_P extends Context.Service<infer _I, infer _S> ? _I
230
231
  : never */
231
232
  : never
232
233
  : never
package/src/rpc.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * as RpcX from "./rpc.js"
1
+ export * as Invalidation from "./rpc/Invalidation.js"
2
2
  export * as MiddlewareMaker from "./rpc/MiddlewareMaker.js"
3
3
  export * as RpcContextMap from "./rpc/RpcContextMap.js"
4
4
  export * as RpcMiddleware from "./rpc/RpcMiddleware.js"
package/src/transform.ts CHANGED
@@ -51,8 +51,8 @@ const encodeOptsAsNullable_ = (value: any, cacheMap: Map<any, any>): any => {
51
51
 
52
52
  if (
53
53
  value instanceof Date
54
- || value instanceof Function
55
- || value instanceof Promise
54
+ || typeof value === "function"
55
+ || (typeof value === "object" && value !== null && "then" in value && typeof value.then === "function")
56
56
  ) {
57
57
  return value
58
58
  }
package/src/utils/gen.ts CHANGED
@@ -15,7 +15,7 @@ export namespace EffectGenUtils {
15
15
  : EG extends (..._: infer _3) => Generator<Yieldable<any, infer _, infer E, infer _R>, infer _A, infer _2> ? E
16
16
  : never
17
17
 
18
- export type ServiceMap<EG> = EG extends Effect<infer _A, infer _E, infer R> ? R
18
+ export type Context<EG> = EG extends Effect<infer _A, infer _E, infer R> ? R
19
19
  // there could be a case where the generator function does not yield anything, so we need to handle that
20
20
  : EG extends (..._: infer _3) => Generator<never, infer _A, infer _2> ? never
21
21
  // v4: generators can yield Yieldable (Effect, Service, etc.), all have asEffect()
@@ -2,11 +2,11 @@
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
 
4
4
  import { Effect, type LogLevel } from "effect"
5
- import * as ServiceMap from "../ServiceMap.js"
5
+ import * as Context from "../Context.js"
6
6
 
7
7
  type Levels = "info" | "debug" | "warn" | "error"
8
8
 
9
- export class LogLevels extends ServiceMap.Reference("LogLevels", {
9
+ export class LogLevels extends Context.Reference("LogLevels", {
10
10
  defaultValue: () => new Map<string, Levels>()
11
11
  }) {}
12
12