effect-app 4.0.0-beta.2 → 4.0.0-beta.200

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 (224) hide show
  1. package/CHANGELOG.md +960 -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 +7 -6
  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/Option.d.ts +1 -1
  31. package/dist/Option.d.ts.map +1 -1
  32. package/dist/Pure.d.ts +5 -5
  33. package/dist/Pure.d.ts.map +1 -1
  34. package/dist/Pure.js +13 -13
  35. package/dist/Schema/Class.d.ts +66 -20
  36. package/dist/Schema/Class.d.ts.map +1 -1
  37. package/dist/Schema/Class.js +189 -22
  38. package/dist/Schema/FastCheck.d.ts +1 -1
  39. package/dist/Schema/FastCheck.d.ts.map +1 -1
  40. package/dist/Schema/Methods.d.ts +1 -1
  41. package/dist/Schema/SchemaParser.d.ts +5 -0
  42. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  43. package/dist/Schema/SchemaParser.js +6 -0
  44. package/dist/Schema/SpecialJsonSchema.d.ts +33 -0
  45. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -0
  46. package/dist/Schema/SpecialJsonSchema.js +122 -0
  47. package/dist/Schema/SpecialOpenApi.d.ts +32 -0
  48. package/dist/Schema/SpecialOpenApi.d.ts.map +1 -0
  49. package/dist/Schema/SpecialOpenApi.js +123 -0
  50. package/dist/Schema/brand.d.ts +14 -6
  51. package/dist/Schema/brand.d.ts.map +1 -1
  52. package/dist/Schema/brand.js +1 -1
  53. package/dist/Schema/email.d.ts +1 -1
  54. package/dist/Schema/email.d.ts.map +1 -1
  55. package/dist/Schema/email.js +9 -5
  56. package/dist/Schema/ext.d.ts +121 -48
  57. package/dist/Schema/ext.d.ts.map +1 -1
  58. package/dist/Schema/ext.js +134 -52
  59. package/dist/Schema/moreStrings.d.ts +117 -17
  60. package/dist/Schema/moreStrings.d.ts.map +1 -1
  61. package/dist/Schema/moreStrings.js +19 -18
  62. package/dist/Schema/numbers.d.ts +127 -15
  63. package/dist/Schema/numbers.d.ts.map +1 -1
  64. package/dist/Schema/numbers.js +10 -12
  65. package/dist/Schema/phoneNumber.d.ts +1 -1
  66. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  67. package/dist/Schema/phoneNumber.js +8 -4
  68. package/dist/Schema/schema.d.ts +1 -1
  69. package/dist/Schema/strings.d.ts +37 -5
  70. package/dist/Schema/strings.d.ts.map +1 -1
  71. package/dist/Schema/strings.js +1 -5
  72. package/dist/Schema.d.ts +159 -58
  73. package/dist/Schema.d.ts.map +1 -1
  74. package/dist/Schema.js +136 -68
  75. package/dist/Set.d.ts +1 -1
  76. package/dist/Set.d.ts.map +1 -1
  77. package/dist/TypeTest.d.ts +1 -1
  78. package/dist/Types.d.ts +1 -1
  79. package/dist/Widen.type.d.ts +1 -1
  80. package/dist/_ext/Array.d.ts +1 -1
  81. package/dist/_ext/Array.d.ts.map +1 -1
  82. package/dist/_ext/date.d.ts +1 -1
  83. package/dist/_ext/misc.d.ts +1 -1
  84. package/dist/_ext/ord.ext.d.ts +1 -1
  85. package/dist/_ext/ord.ext.d.ts.map +1 -1
  86. package/dist/builtin.d.ts +1 -1
  87. package/dist/builtin.d.ts.map +1 -1
  88. package/dist/client/InvalidationKeys.d.ts +29 -0
  89. package/dist/client/InvalidationKeys.d.ts.map +1 -0
  90. package/dist/client/InvalidationKeys.js +33 -0
  91. package/dist/client/apiClientFactory.d.ts +18 -32
  92. package/dist/client/apiClientFactory.d.ts.map +1 -1
  93. package/dist/client/apiClientFactory.js +98 -36
  94. package/dist/client/clientFor.d.ts +51 -17
  95. package/dist/client/clientFor.d.ts.map +1 -1
  96. package/dist/client/clientFor.js +9 -1
  97. package/dist/client/errors.d.ts +49 -25
  98. package/dist/client/errors.d.ts.map +1 -1
  99. package/dist/client/errors.js +43 -17
  100. package/dist/client/makeClient.d.ts +468 -32
  101. package/dist/client/makeClient.d.ts.map +1 -1
  102. package/dist/client/makeClient.js +61 -34
  103. package/dist/client.d.ts +2 -1
  104. package/dist/client.d.ts.map +1 -1
  105. package/dist/client.js +2 -1
  106. package/dist/faker.d.ts +1 -1
  107. package/dist/faker.d.ts.map +1 -1
  108. package/dist/http/Request.d.ts +2 -2
  109. package/dist/http/Request.d.ts.map +1 -1
  110. package/dist/http/Request.js +5 -5
  111. package/dist/http/internal/lib.d.ts +1 -1
  112. package/dist/http.d.ts +1 -1
  113. package/dist/ids.d.ts +9 -9
  114. package/dist/ids.d.ts.map +1 -1
  115. package/dist/ids.js +3 -2
  116. package/dist/index.d.ts +5 -9
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +6 -9
  119. package/dist/logger.d.ts +1 -1
  120. package/dist/middleware.d.ts +16 -9
  121. package/dist/middleware.d.ts.map +1 -1
  122. package/dist/middleware.js +13 -9
  123. package/dist/rpc/Invalidation.d.ts +397 -0
  124. package/dist/rpc/Invalidation.d.ts.map +1 -0
  125. package/dist/rpc/Invalidation.js +150 -0
  126. package/dist/rpc/MiddlewareMaker.d.ts +6 -5
  127. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  128. package/dist/rpc/MiddlewareMaker.js +51 -28
  129. package/dist/rpc/RpcContextMap.d.ts +3 -3
  130. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  131. package/dist/rpc/RpcContextMap.js +4 -4
  132. package/dist/rpc/RpcMiddleware.d.ts +6 -5
  133. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  134. package/dist/rpc/RpcMiddleware.js +1 -1
  135. package/dist/rpc.d.ts +2 -2
  136. package/dist/rpc.d.ts.map +1 -1
  137. package/dist/rpc.js +2 -2
  138. package/dist/transform.d.ts +1 -1
  139. package/dist/transform.d.ts.map +1 -1
  140. package/dist/transform.js +3 -3
  141. package/dist/utils/effectify.d.ts +1 -1
  142. package/dist/utils/extend.d.ts +1 -1
  143. package/dist/utils/extend.d.ts.map +1 -1
  144. package/dist/utils/gen.d.ts +2 -2
  145. package/dist/utils/gen.d.ts.map +1 -1
  146. package/dist/utils/logLevel.d.ts +2 -2
  147. package/dist/utils/logLevel.d.ts.map +1 -1
  148. package/dist/utils/logger.d.ts +3 -3
  149. package/dist/utils/logger.d.ts.map +1 -1
  150. package/dist/utils/logger.js +3 -3
  151. package/dist/utils.d.ts +48 -10
  152. package/dist/utils.d.ts.map +1 -1
  153. package/dist/utils.js +33 -8
  154. package/dist/validation/validators.d.ts +1 -1
  155. package/dist/validation/validators.d.ts.map +1 -1
  156. package/dist/validation.d.ts +1 -1
  157. package/dist/validation.d.ts.map +1 -1
  158. package/package.json +46 -28
  159. package/src/Config/SecretURL.ts +2 -1
  160. package/src/Config.ts +14 -0
  161. package/src/ConfigProvider.ts +48 -0
  162. package/src/{ServiceMap.ts → Context.ts} +58 -64
  163. package/src/Effect.ts +12 -14
  164. package/src/Layer.ts +6 -5
  165. package/src/Pure.ts +17 -18
  166. package/src/Schema/Class.ts +268 -62
  167. package/src/Schema/SchemaParser.ts +12 -0
  168. package/src/Schema/SpecialJsonSchema.ts +137 -0
  169. package/src/Schema/SpecialOpenApi.ts +130 -0
  170. package/src/Schema/brand.ts +21 -7
  171. package/src/Schema/email.ts +10 -3
  172. package/src/Schema/ext.ts +226 -86
  173. package/src/Schema/moreStrings.ts +37 -32
  174. package/src/Schema/numbers.ts +14 -16
  175. package/src/Schema/phoneNumber.ts +8 -2
  176. package/src/Schema/strings.ts +4 -8
  177. package/src/Schema.ts +350 -107
  178. package/src/client/InvalidationKeys.ts +50 -0
  179. package/src/client/apiClientFactory.ts +227 -136
  180. package/src/client/clientFor.ts +86 -29
  181. package/src/client/errors.ts +61 -26
  182. package/src/client/makeClient.ts +530 -80
  183. package/src/client.ts +1 -0
  184. package/src/http/Request.ts +7 -4
  185. package/src/ids.ts +3 -2
  186. package/src/index.ts +5 -11
  187. package/src/middleware.ts +12 -10
  188. package/src/rpc/Invalidation.ts +221 -0
  189. package/src/rpc/MiddlewareMaker.ts +61 -51
  190. package/src/rpc/README.md +2 -2
  191. package/src/rpc/RpcContextMap.ts +6 -5
  192. package/src/rpc/RpcMiddleware.ts +6 -5
  193. package/src/rpc.ts +1 -1
  194. package/src/transform.ts +2 -2
  195. package/src/utils/gen.ts +1 -1
  196. package/src/utils/logger.ts +2 -2
  197. package/src/utils.ts +73 -15
  198. package/test/dist/moreStrings.test.d.ts.map +1 -0
  199. package/test/dist/rpc.test.d.ts.map +1 -1
  200. package/test/dist/secretURL.test.d.ts.map +1 -0
  201. package/test/dist/special.test.d.ts.map +1 -0
  202. package/test/moreStrings.test.ts +17 -0
  203. package/test/rpc.test.ts +38 -6
  204. package/test/schema.test.ts +609 -4
  205. package/test/secretURL.test.ts +157 -0
  206. package/test/special.test.ts +1023 -0
  207. package/test/utils.test.ts +6 -6
  208. package/tsconfig.base.json +3 -4
  209. package/tsconfig.json +0 -1
  210. package/tsconfig.json.bak +2 -2
  211. package/tsconfig.src.json +29 -29
  212. package/tsconfig.test.json +2 -2
  213. package/dist/Operations.d.ts +0 -87
  214. package/dist/Operations.d.ts.map +0 -1
  215. package/dist/Operations.js +0 -29
  216. package/dist/ServiceMap.d.ts +0 -44
  217. package/dist/ServiceMap.d.ts.map +0 -1
  218. package/dist/ServiceMap.js +0 -91
  219. package/dist/Struct.d.ts +0 -44
  220. package/dist/Struct.d.ts.map +0 -1
  221. package/dist/Struct.js +0 -29
  222. package/eslint.config.mjs +0 -26
  223. package/src/Operations.ts +0 -55
  224. package/src/Struct.ts +0 -54
@@ -1,19 +1,22 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import * as Config from "effect/Config"
3
- import { flow } from "effect/Function"
2
+ import { constant, flow } from "effect/Function"
4
3
  import * as Layer from "effect/Layer"
5
4
  import * as ManagedRuntime from "effect/ManagedRuntime"
5
+ import * as Option from "effect/Option"
6
6
  import * as Predicate from "effect/Predicate"
7
7
  import * as Schema from "effect/Schema"
8
+ import * as Stream from "effect/Stream"
8
9
  import * as Struct from "effect/Struct"
9
- import { Rpc, RpcClient, RpcGroup, RpcSerialization } from "effect/unstable/rpc"
10
+ import { Rpc, RpcClient, RpcGroup, RpcMiddleware, RpcSerialization } from "effect/unstable/rpc"
11
+ import * as Config from "../Config.js"
12
+ import * as Context from "../Context.js"
10
13
  import * as Effect from "../Effect.js"
11
14
  import { HttpClient, HttpClientRequest } from "../http.js"
12
- import * as Option from "../Option.js"
15
+ import { Invalidation } from "../rpc.js"
13
16
  import type * as S from "../Schema.js"
14
- import * as ServiceMap from "../ServiceMap.js"
15
17
  import { typedKeysOf, typedValuesOf } from "../utils.js"
16
- import type { Client, ClientForOptions, Requests, RequestsAny } from "./clientFor.js"
18
+ import type { Client, ClientForOptions, ExtractModuleName, RequestsAny } from "./clientFor.js"
19
+ import { InvalidationKeysFromServer } from "./InvalidationKeys.js"
17
20
 
18
21
  export interface ApiConfig {
19
22
  url: string
@@ -31,16 +34,21 @@ export const DefaultApiConfig = Config.all({
31
34
  })
32
35
 
33
36
  export type Req = S.Top & {
34
- new(...args: any[]): any
37
+ readonly make: (...args: any[]) => any
35
38
  _tag: string
36
39
  fields: S.Struct.Fields
37
40
  success: S.Top
38
41
  error: S.Top
42
+ /** Optional final-value schema for stream requests. When set, the execute effect resolves with the last stream value decoded to this type. */
43
+ final?: S.Top
39
44
  config?: Record<string, any>
40
- readonly "~decodingServices"?: unknown
45
+ readonly id: string
46
+ readonly moduleName: string
47
+ readonly type: "command" | "query"
48
+ readonly stream: boolean
41
49
  }
42
50
 
43
- class RequestName extends ServiceMap.Reference("RequestName", {
51
+ class RequestName extends Context.Reference("RequestName", {
44
52
  defaultValue: () => ({ requestName: "Unspecified", moduleName: "Error" })
45
53
  }) {}
46
54
 
@@ -50,17 +58,17 @@ export const HttpClientLayer = (config: ApiConfig) =>
50
58
  Effect
51
59
  .gen(function*() {
52
60
  const baseClient = yield* HttpClient.HttpClient
53
- const ctx = yield* RequestName
54
61
  const client = baseClient.pipe(
55
62
  HttpClient.mapRequest(HttpClientRequest.prependUrl(config.url + "/rpc")),
56
63
  HttpClient.mapRequest(
57
64
  HttpClientRequest.setHeaders(config.headers.pipe(Option.getOrElse(() => ({}))))
58
65
  ),
59
- HttpClient.mapRequest((req) =>
60
- flow(
61
- HttpClientRequest.appendUrlParam("action", ctx.requestName),
62
- HttpClientRequest.appendUrl("/" + ctx.moduleName)
63
- )(req)
66
+ HttpClient.mapRequestEffect((req) =>
67
+ Effect.map(RequestName.asEffect(), (ctx) =>
68
+ flow(
69
+ HttpClientRequest.appendUrlParam("action", ctx.requestName),
70
+ HttpClientRequest.appendUrl("/" + ctx.moduleName)
71
+ )(req))
64
72
  )
65
73
  )
66
74
  return client
@@ -76,7 +84,7 @@ export const HttpClientFromConfigLayer = Layer.unwrap(
76
84
 
77
85
  export const RpcSerializationLayer = (config: ApiConfig) =>
78
86
  Layer.mergeAll(
79
- RpcSerialization.layerJson,
87
+ RpcSerialization.layerNdjson,
80
88
  HttpClientLayer(config)
81
89
  )
82
90
 
@@ -84,14 +92,14 @@ type RpcHandlers<M extends RequestsAny> = {
84
92
  [K in keyof M]: Rpc.Rpc<M[K]["_tag"], M[K], M[K]["success"], M[K]["error"]>
85
93
  }
86
94
 
87
- const getFiltered = <M extends Requests>(resource: M) => {
95
+ const getFiltered = <M extends RequestsAny>(resource: M) => {
88
96
  type Filtered = {
89
97
  [K in keyof M as M[K] extends Req ? K : never]: M[K] extends Req ? M[K] : never
90
98
  }
91
99
  // TODO: Record.filter
92
100
  const filtered = typedKeysOf(resource).reduce((acc, cur) => {
93
101
  if (
94
- Predicate.isObject(resource[cur])
102
+ Predicate.isObjectKeyword(resource[cur])
95
103
  && (resource[cur].success)
96
104
  ) {
97
105
  acc[cur as keyof Filtered] = resource[cur] as any
@@ -102,13 +110,13 @@ const getFiltered = <M extends Requests>(resource: M) => {
102
110
  return filtered as unknown as Filtered
103
111
  }
104
112
 
105
- export const getMeta = <M extends Requests>(resource: M) => {
106
- const meta = (resource as any).meta as { moduleName: string }
107
- if (!meta) throw new Error("No meta defined in Resource!")
108
- return meta as M["meta"]
113
+ export const getMeta = <M extends RequestsAny>(resource: M): { moduleName: ExtractModuleName<M> } => {
114
+ const first = typedValuesOf(getFiltered(resource))[0]
115
+ if (first && "moduleName" in first) return { moduleName: first.moduleName }
116
+ throw new Error("No moduleName on requests!")
109
117
  }
110
118
 
111
- export const makeRpcGroupFromRequestsAndModuleName = <M extends Requests, const ModuleName extends string>(
119
+ export const makeRpcGroupFromRequestsAndModuleName = <M extends RequestsAny, const ModuleName extends string>(
112
120
  resource: M,
113
121
  moduleName: ModuleName
114
122
  ) => {
@@ -117,7 +125,34 @@ export const makeRpcGroupFromRequestsAndModuleName = <M extends Requests, const
117
125
  const rpcs = RpcGroup
118
126
  .make(
119
127
  ...typedValuesOf(filtered).map((_) => {
120
- return Rpc.make((_ as any)._tag, { payload: _ as any, success: (_ as any).success, error: (_ as any).error })
128
+ const r = _ as any
129
+ const isStream = r.stream
130
+ const isCommand = r.type === "command"
131
+ const rpc = (isCommand
132
+ ? isStream
133
+ ? Invalidation.makeStreamRpc(r._tag, {
134
+ payload: r,
135
+ success: r.success,
136
+ error: r.error,
137
+ stream: true as const
138
+ })
139
+ : Invalidation.makeCommandRpc(r._tag, { payload: r, success: r.success, error: r.error })
140
+ : Rpc.make(r._tag, { payload: r, success: r.success, error: r.error, stream: isStream })) as any
141
+
142
+ // Stream rpcs force `errorSchema = Never` in effect-rpc and bury the
143
+ // resource error union inside `StreamFailureChunk`. Middleware-thrown
144
+ // errors (e.g. `NotLoggedInError` from auth) reach the Cause un-wrapped
145
+ // and would fail client decode. Attach a synthetic schema-only middleware
146
+ // tag carrying the resource error union so `Rpc.exitSchema` includes it
147
+ // in the failure union via the `rpc.middlewares[*].error` channel.
148
+ if (isStream && r.error) {
149
+ const ErrorBag = RpcMiddleware.Service<any>()(
150
+ `${moduleName}.${r._tag}.ClientErrorBag`,
151
+ { error: r.error }
152
+ )
153
+ return rpc.middleware(ErrorBag as any)
154
+ }
155
+ return rpc
121
156
  })
122
157
  )
123
158
  .prefix(`${moduleName}.`) as unknown as RpcGroup.RpcGroup<
@@ -126,155 +161,211 @@ export const makeRpcGroupFromRequestsAndModuleName = <M extends Requests, const
126
161
  return rpcs
127
162
  }
128
163
 
129
- export const makeRpcGroup = <
130
- M extends Requests,
131
- const ModuleName extends string
132
- >(
133
- resource: M & { meta: { moduleName: ModuleName } }
134
- ) => makeRpcGroupFromRequestsAndModuleName(resource, resource.meta.moduleName)
135
-
136
- const makeRpcTag = <M extends Requests>(resource: M) => {
164
+ const makeRpcTag = <M extends RequestsAny>(resource: M) => {
137
165
  const meta = getMeta(resource)
138
166
  const rpcs = makeRpcGroupFromRequestsAndModuleName(resource, meta.moduleName)
139
167
 
140
168
  // Use Object.assign instead of class extension to avoid TS2509 with complex generic return types.
141
169
  // The first type arg is `any` because this is a dynamically created tag — its identity is the string key.
142
- const TheClient = ServiceMap.Opaque<
170
+ const TheClient = Context.Opaque<
143
171
  any,
144
172
  RpcClient.RpcClient<RpcGroup.Rpcs<typeof rpcs>>
145
173
  >()(`RpcClient.${meta.moduleName}`)
146
174
  // Use Layer.effect directly (not TheClient.toLayer) so TypeScript properly excludes Scope
147
175
  const layer = Layer.effect(
148
176
  TheClient,
149
- Effect.map(
150
- RpcClient.make(rpcs, { spanPrefix: "RpcClient." + meta.moduleName }),
151
- (cl) => (cl as any)[meta.moduleName]
152
- )
177
+ RpcClient.make(rpcs, { spanPrefix: "RpcClient." + meta.moduleName })
153
178
  )
154
179
  return Object.assign(TheClient, { layer })
155
180
  }
156
181
 
157
182
  const makeApiClientFactory = Effect
158
183
  .gen(function*() {
159
- const ctx = yield* Effect.services<RpcSerialization.RpcSerialization | HttpClient.HttpClient>()
160
- const makeClientFor = <M extends Requests>(
184
+ const ctx = yield* Effect.context<RpcSerialization.RpcSerialization | HttpClient.HttpClient>()
185
+ const makeClientFor = Effect.fnUntraced(function*<M extends RequestsAny>(
161
186
  resource: M,
162
187
  requestLevelLayers = Layer.empty,
163
188
  options?: ClientForOptions
164
- ) =>
165
- Effect.gen(function*() {
166
- const TheClient = makeRpcTag(resource)
167
-
168
- const meta = getMeta(resource)
169
-
170
- // TODO: somehow we need a protocol per REQUEST kind of it seems ...
171
- // otherwise it locks up on the client, navigation remains empty...
172
- const clientLayer = TheClient.layer.pipe(
173
- // add ApiClientFactory for nested schemas
174
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
175
- Layer.provide(Layer.succeed(ApiClientFactory, makeClientForCached as any)),
176
- Layer.provide(
177
- RpcClient
178
- .layerProtocolHttp({
179
- url: "" // why not here set meta.moduleName as root?
189
+ ) {
190
+ const TheClient = makeRpcTag(resource)
191
+
192
+ const meta = getMeta(resource)
193
+
194
+ // TODO: somehow we need a protocol per REQUEST kind of it seems ...
195
+ // otherwise it locks up on the client, navigation remains empty...
196
+ const clientLayer = TheClient.layer.pipe(
197
+ // add ApiClientFactory for nested schemas
198
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
199
+ Layer.provide(Layer.succeed(ApiClientFactory, makeClientForCached as any)),
200
+ Layer.provide(
201
+ RpcClient
202
+ .layerProtocolHttp({ url: "" }) // why not here set meta.moduleName as root?
203
+ .pipe(
204
+ Layer.provideMerge(Layer.succeedContext(ctx))
205
+ )
206
+ )
207
+ )
208
+ const mr = ManagedRuntime.make(clientLayer)
209
+
210
+ const filtered = getFiltered(resource)
211
+
212
+ const unwrapCommand = (eff: Effect.Effect<any, any, any>): Effect.Effect<any, any, any> =>
213
+ eff.pipe(
214
+ Effect.flatMap((result: any) =>
215
+ Effect.gen(function*() {
216
+ const keys: ReadonlyArray<Invalidation.InvalidationKey> = result?.metadata?.invalidateQueries ?? []
217
+ const invalidationKeys = yield* InvalidationKeysFromServer
218
+ yield* Effect.forEach(keys, (key) => invalidationKeys.add(key), { discard: true })
219
+ return result.payload
220
+ })
221
+ ),
222
+ // V2: unwrap CommandFailureWithMetaData failures — forward keys, re-fail with the
223
+ // original error so callers see the unmodified error type.
224
+ Effect.catch((result: any) =>
225
+ result?._tag === "CommandFailureWithMetaData"
226
+ ? Effect.gen(function*() {
227
+ const keys: ReadonlyArray<Invalidation.InvalidationKey> = result.metadata?.invalidateQueries ?? []
228
+ const invalidationKeys = yield* InvalidationKeysFromServer
229
+ yield* Effect.forEach(keys, (key) => invalidationKeys.add(key), { discard: true })
230
+ return yield* Effect.fail(result.error)
180
231
  })
181
- .pipe(
182
- Layer.provideMerge(Layer.succeedServices(ctx))
183
- )
232
+ : Effect.fail(result)
184
233
  )
185
234
  )
186
- const mr = ManagedRuntime.make(clientLayer)
187
-
188
- const filtered = getFiltered(resource)
189
- return {
190
- mr,
191
- client: (typedKeysOf(filtered)
192
- .reduce((prev, cur) => {
193
- const h = filtered[cur]!
194
235
 
195
- const Request = h
236
+ return {
237
+ mr,
238
+ client: typedKeysOf(filtered)
239
+ .reduce((prev, cur) => {
240
+ const h = filtered[cur]!
241
+
242
+ const Request = h
243
+
244
+ const id = `${meta.moduleName}.${cur as string}`
245
+ .replaceAll(".js", "")
246
+
247
+ const requestMeta = {
248
+ Request,
249
+ id,
250
+ options
251
+ }
252
+
253
+ const requestNameLayer = Layer.succeed(RequestName, {
254
+ requestName: cur as string,
255
+ moduleName: meta.moduleName
256
+ })
257
+
258
+ const layers = requestLevelLayers.pipe(Layer.provideMerge(requestNameLayer))
259
+
260
+ const fields = Struct.omit(Request.fields, ["_tag"] as const)
261
+ const requestAttr = `${meta.moduleName}.${h._tag}`
262
+ const isCommand = h.type === "command"
263
+ const isStream = h.stream
264
+
265
+ const buildEffect = (input: any) =>
266
+ mr.contextEffect.pipe(
267
+ Effect.flatMap((svcs) => {
268
+ const rpcEffect = TheClient
269
+ .use((client) => (client as any)[requestAttr]!(Request.make(input)) as Effect.Effect<any, any>)
270
+ .pipe(
271
+ Effect.provide(layers),
272
+ Effect.provide(svcs)
273
+ )
274
+ return isCommand ? unwrapCommand(rpcEffect) : rpcEffect
275
+ })
276
+ )
196
277
 
197
- const id = `${meta.moduleName}.${cur as string}`
198
- .replaceAll(".js", "")
278
+ const buildStream = (input: any) =>
279
+ Stream.unwrap(
280
+ mr.contextEffect.pipe(
281
+ Effect.flatMap((svcs) =>
282
+ TheClient
283
+ .useSync((client) => {
284
+ const rpcStream = (client as any)[requestAttr]!(
285
+ Request.make(input)
286
+ ) as Stream.Stream<any, any, any>
287
+ return rpcStream.pipe(
288
+ // Collect server invalidation keys from the "done" chunk, then discard it.
289
+ Stream.tap((item: any) =>
290
+ item._tag === "done" || item._tag === "metadata"
291
+ ? InvalidationKeysFromServer.use((svc) =>
292
+ Effect.forEach(
293
+ (item.metadata as Invalidation.CommandMetaData).invalidateQueries,
294
+ svc.add,
295
+ { discard: true }
296
+ )
297
+ )
298
+ : Effect.void
299
+ ),
300
+ Stream.filter((item: any) => item._tag === "value"),
301
+ Stream.map((item: any) => item.value),
302
+ // V2: unwrap StreamFailureChunk — forward keys from failures too,
303
+ // then re-fail with the original error so callers see the unmodified
304
+ // error type.
305
+ Stream.catch((err: any) =>
306
+ err?._tag === "error" && err?.metadata
307
+ ? Stream.fromEffect(
308
+ InvalidationKeysFromServer.use((svc) =>
309
+ Effect
310
+ .forEach(
311
+ (err.metadata as Invalidation.CommandMetaData).invalidateQueries,
312
+ svc.add,
313
+ { discard: true }
314
+ )
315
+ .pipe(Effect.flatMap(() => Effect.fail(err.error)))
316
+ )
317
+ )
318
+ : Stream.fail(err)
319
+ ),
320
+ Stream.provide(layers),
321
+ Stream.provide(svcs)
322
+ )
323
+ })
324
+ .pipe(Effect.provide(svcs))
325
+ )
326
+ )
327
+ )
199
328
 
200
- const requestMeta = {
201
- Request,
202
- id,
203
- options
329
+ // @ts-expect-error doc
330
+ prev[cur] = Object.keys(fields).length === 0
331
+ ? {
332
+ handler: isStream ? constant(buildStream({})) : constant(buildEffect({})),
333
+ ...requestMeta
334
+ }
335
+ : {
336
+ handler: isStream
337
+ ? (req: any) => buildStream(req)
338
+ : (req: any) => buildEffect(req),
339
+ ...requestMeta
204
340
  }
205
341
 
206
- const requestNameLayer = Layer.succeed(RequestName, {
207
- requestName: cur as string,
208
- moduleName: meta.moduleName
209
- })
210
-
211
- const layers = requestLevelLayers.pipe(Layer.provideMerge(requestNameLayer))
212
-
213
- const fields = Struct.omit(Request.fields, ["_tag"] as const)
214
- const requestAttr = h._tag
215
- // @ts-expect-error doc
216
- prev[cur] = Object.keys(fields).length === 0
217
- ? {
218
- handler: mr.servicesEffect.pipe(
219
- Effect.flatMap((svcs) =>
220
- TheClient
221
- .use((client) => (client as any)[requestAttr]!(new Request()) as Effect.Effect<any, any, never>)
222
- .pipe(
223
- Effect.provide(layers),
224
- Effect.provide(svcs)
225
- )
226
- )
227
- ),
228
- ...requestMeta
229
- }
230
- : {
231
- handler: (req: any) =>
232
- mr.servicesEffect.pipe(
233
- Effect.flatMap((svcs) =>
234
- TheClient
235
- .use((client) =>
236
- (client as any)[requestAttr]!(new Request(req)) as Effect.Effect<any, any, never>
237
- )
238
- .pipe(
239
- Effect.provide(layers),
240
- Effect.provide(svcs)
241
- )
242
- )
243
- ),
244
-
245
- ...requestMeta
246
- }
247
-
248
- return prev
249
- }, {} as Client<M, M["meta"]["moduleName"]>))
250
- }
251
- })
342
+ return prev
343
+ }, {} as Client<M, ExtractModuleName<M>>)
344
+ }
345
+ })
252
346
 
253
347
  const register: ManagedRuntime.ManagedRuntime<any, any>[] = []
254
348
  yield* Effect.addFinalizer(() => Effect.forEach(register, (mr) => mr.disposeEffect))
255
349
 
256
350
  const cacheL = new Map<any, Map<any, Client<any, any>>>()
257
351
 
258
- function makeClientForCached(requestLevelLayers: Layer.Layer<never, never, never>, options?: ClientForOptions) {
352
+ function makeClientForCached(requestLevelLayers: Layer.Layer<never>, options?: ClientForOptions) {
259
353
  let cache = cacheL.get(requestLevelLayers)
260
354
  if (!cache) {
261
355
  cache = new Map<any, Client<any, any>>()
262
356
  cacheL.set(requestLevelLayers, cache)
263
357
  }
264
358
 
265
- return <M extends Requests>(
266
- models: M
267
- ): Effect.Effect<Client<M, M["meta"]["moduleName"]>> =>
268
- Effect.gen(function*() {
269
- const found = cache.get(models)
270
- if (found) {
271
- return found
272
- }
273
- const m = yield* makeClientFor(models, requestLevelLayers, options)
274
- cache.set(models, m.client)
275
- register.push(m.mr)
276
- return m.client
277
- })
359
+ return Effect.fnUntraced(function*<M extends RequestsAny>(models: M) {
360
+ const found = cache.get(models) as Client<M, ExtractModuleName<M>> | undefined
361
+ if (found) {
362
+ return found
363
+ }
364
+ const m = yield* makeClientFor(models, requestLevelLayers, options)
365
+ cache.set(models, m.client)
366
+ register.push(m.mr)
367
+ return m.client
368
+ })
278
369
  }
279
370
 
280
371
  return makeClientForCached
@@ -284,7 +375,7 @@ const makeApiClientFactory = Effect
284
375
  * Used to create clients for resource modules.
285
376
  */
286
377
  export class ApiClientFactory
287
- extends ServiceMap.Opaque<ApiClientFactory, Effect.Success<typeof makeApiClientFactory>>()("ApiClientFactory")
378
+ extends Context.Opaque<ApiClientFactory, Effect.Success<typeof makeApiClientFactory>>()("ApiClientFactory")
288
379
  {
289
380
  static readonly layer = (config: ApiConfig) =>
290
381
  ApiClientFactory.toLayer(makeApiClientFactory).pipe(Layer.provide(RpcSerializationLayer(config)))
@@ -296,8 +387,8 @@ export class ApiClientFactory
296
387
  )
297
388
 
298
389
  static readonly makeFor =
299
- (requestLevelLayers: Layer.Layer<never, never, never>, options?: ClientForOptions) =>
300
- <M extends Requests>(
390
+ (requestLevelLayers: Layer.Layer<never>, options?: ClientForOptions) =>
391
+ <M extends RequestsAny>(
301
392
  resource: M
302
393
  ) =>
303
394
  ApiClientFactory.use((apiClientFactory) => {
@@ -2,7 +2,7 @@
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
 
4
4
  import * as Record from "effect/Record"
5
- import type * as Request from "effect/Request"
5
+ import type * as Stream from "effect/Stream"
6
6
  import type { Path } from "path-parser"
7
7
  import qs from "query-string"
8
8
  import type * as Effect from "../Effect.js"
@@ -48,9 +48,14 @@ export function makePathWithBody(
48
48
  return path.build(pars, { ignoreSearch: true, ignoreConstraints: true })
49
49
  }
50
50
 
51
- export type Requests<ModuleName extends string = string> = { meta: { moduleName: ModuleName } } & RequestsAny
51
+ export type Requests = RequestsAny
52
52
  export type RequestsAny = Record<string, any>
53
53
 
54
+ export type ExtractModuleName<M extends RequestsAny> =
55
+ { [K in keyof M]: M[K] extends { moduleName: infer N extends string } ? N : never }[keyof M] extends
56
+ infer R extends string ? R
57
+ : string
58
+
54
59
  export type Client<M extends RequestsAny, ModuleName extends string> = RequestHandlers<
55
60
  never,
56
61
  never,
@@ -66,48 +71,100 @@ export type ExtractEResponse<T> = T extends S.Codec<any> ? S.Codec.Encoded<T>
66
71
  : T extends unknown ? void
67
72
  : never
68
73
 
69
- type IsEmpty<T> = keyof T extends never ? true
70
- : false
71
-
72
- // v4: Request.RequestTypeId, S.symbolSerializable, S.symbolWithResult removed — use keyof Request to filter internal props
73
- type Cruft = "_tag" | keyof Request.Request<any, any, any>
74
-
75
74
  export interface ClientForOptions {
76
75
  readonly skipQueryKey?: readonly string[]
77
76
  }
78
77
 
79
- export interface RequestHandler<A, E, R, Request extends Req, Id extends string> {
80
- handler: Effect.Effect<A, E, R>
78
+ // $Project/$Configuration.Index
79
+ // -> "$Project", "$Configuration", "Index"
80
+ export const makeQueryKey = ({ id, options }: { id: string; options?: ClientForOptions }) =>
81
+ id
82
+ .split("/")
83
+ .filter((segment: string) => !options || !options.skipQueryKey?.includes(segment))
84
+ .map((segment: string) => "$" + segment)
85
+ .join(".")
86
+ .split(".")
87
+
88
+ export interface RequestHandlerWithInput<I, A, E, R, Request extends Req, Id extends string> {
89
+ handler: (i: I) => Effect.Effect<A, E, R>
81
90
  id: Id
82
91
  options?: ClientForOptions
83
92
  Request: Request
84
93
  }
85
94
 
86
- export interface RequestHandlerWithInput<I, A, E, R, Request extends Req, Id extends string> {
87
- handler: (i: I) => Effect.Effect<A, E, R>
95
+ /** Type alias: a no-input handler is simply `RequestHandlerWithInput<void, …>`. */
96
+ export type RequestHandler<A, E, R, Request extends Req, Id extends string> = RequestHandlerWithInput<
97
+ void,
98
+ A,
99
+ E,
100
+ R,
101
+ Request,
102
+ Id
103
+ >
104
+
105
+ export interface RequestStreamHandlerWithInput<I, A, E, R, Request extends Req, Id extends string, Final = A> {
106
+ handler: (i: I) => Stream.Stream<A, E, R>
88
107
  id: Id
89
108
  options?: ClientForOptions
90
109
  Request: Request
110
+ /**
111
+ * Phantom type property (never set at runtime) that carries the `Final` type to
112
+ * `StreamMutationWithExtensions`. The tilde prefix follows the Effect convention for
113
+ * phantom/virtual properties and prevents accidental runtime access.
114
+ * Stream failures bubble through the execute effect's typed error channel `E`;
115
+ * the reactive `AsyncResult` ref also mirrors the failure for live progress UI.
116
+ */
117
+ readonly "~final"?: Final
91
118
  }
92
119
 
120
+ /** Type alias: a no-input stream handler is simply `RequestStreamHandlerWithInput<void, …>`. */
121
+ export type RequestStreamHandler<A, E, R, Request extends Req, Id extends string, Final = A> =
122
+ RequestStreamHandlerWithInput<void, A, E, R, Request, Id, Final>
123
+
93
124
  // make sure this is exported or d.ts of apiClientFactory breaks?!
94
- type ReqDecodingServices<M> = M extends { readonly "~decodingServices": infer DS } ? DS : never
125
+ export type RequestInputFromMake<I extends { readonly make: (...args: any[]) => any }> = Parameters<I["make"]> extends
126
+ [] ? void : Parameters<I["make"]>[0]
127
+
128
+ // Has no input only when the request schema declares no payload fields (the auto-added
129
+ // `_tag` field is ignored). Any payload fields (even all-optional) produce a function handler.
130
+ type HasNoFields<I> = I extends { readonly fields: infer F extends S.Struct.Fields }
131
+ ? [Exclude<keyof F, "_tag">] extends [never] ? true : false
132
+ : false
133
+
134
+ type RequestInput<I extends { readonly make: (...args: any[]) => any }> = Parameters<I["make"]>[0]
135
+
136
+ /**
137
+ * Caller-facing input type for a request. `void` when the request schema has no fields;
138
+ * otherwise `make`'s first param type.
139
+ */
140
+ export type HandlerInput<I extends { readonly make: (...args: any[]) => any }> = HasNoFields<I> extends true ? void
141
+ : RequestInput<I>
142
+
143
+ /** Extracts the final-value type from a stream request. Defaults to the success type when no `final` schema is set. */
144
+ type FinalTypeOf<T extends Req> = T extends { readonly final: infer F extends S.Top } ? S.Schema.Type<F>
145
+ : S.Schema.Type<T["success"]>
146
+
147
+ type RequestHandlerFor<R, E, T extends Req, Id extends string> = T["stream"] extends true
148
+ ? RequestStreamHandlerWithInput<
149
+ HandlerInput<T>,
150
+ S.Schema.Type<T["success"]>,
151
+ S.Schema.Type<T["error"]> | E,
152
+ R | S.Codec.DecodingServices<T["success"]> | S.Codec.DecodingServices<T["error"]>,
153
+ T,
154
+ Id,
155
+ FinalTypeOf<T>
156
+ >
157
+ : RequestHandlerWithInput<
158
+ HandlerInput<T>,
159
+ S.Schema.Type<T["success"]>,
160
+ S.Schema.Type<T["error"]> | E,
161
+ R | S.Codec.DecodingServices<T["success"]> | S.Codec.DecodingServices<T["error"]>,
162
+ T,
163
+ Id
164
+ >
95
165
 
96
166
  export type RequestHandlers<R, E, M extends RequestsAny, ModuleName extends string> = {
97
- [K in keyof M as M[K] extends Req ? K : never]: IsEmpty<Omit<S.Schema.Type<M[K]>, Cruft>> extends true
98
- ? RequestHandler<
99
- S.Schema.Type<M[K]["success"]>,
100
- S.Schema.Type<M[K]["error"]> | E,
101
- R | ReqDecodingServices<M[K]>,
102
- M[K],
103
- `${ModuleName}.${K & string}`
104
- >
105
- : RequestHandlerWithInput<
106
- Omit<S.Schema.Type<M[K]>, Cruft>,
107
- S.Schema.Type<M[K]["success"]>,
108
- S.Schema.Type<M[K]["error"]> | E,
109
- R | ReqDecodingServices<M[K]>,
110
- M[K],
111
- `${ModuleName}.${K & string}`
112
- >
167
+ [K in keyof M as M[K] extends Req ? K : never]: Extract<M[K], Req> extends infer T extends Req
168
+ ? RequestHandlerFor<R, E, T, `${ModuleName}.${K & string}`>
169
+ : never
113
170
  }