effect 4.0.3 → 4.0.4

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 (207) hide show
  1. package/AGENTS.md +24 -0
  2. package/CLAUDE.md +24 -0
  3. package/ai-docs/package.json +2 -2
  4. package/ai-docs/src/52_graphql/10_basics.ts +111 -0
  5. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  6. package/ai-docs/src/52_graphql/30_batching.ts +116 -0
  7. package/ai-docs/src/52_graphql/40_aliased-batching.ts +122 -0
  8. package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
  9. package/ai-docs/src/52_graphql/index.md +6 -0
  10. package/dist/Context.d.ts.map +1 -1
  11. package/dist/Context.js +9 -0
  12. package/dist/Context.js.map +1 -1
  13. package/dist/RequestResolver.d.ts.map +1 -1
  14. package/dist/RequestResolver.js +4 -10
  15. package/dist/RequestResolver.js.map +1 -1
  16. package/dist/Scheduler.d.ts +9 -0
  17. package/dist/Scheduler.d.ts.map +1 -1
  18. package/dist/Scheduler.js +9 -0
  19. package/dist/Scheduler.js.map +1 -1
  20. package/dist/Schema.d.ts +8 -0
  21. package/dist/Schema.d.ts.map +1 -1
  22. package/dist/Schema.js +7 -0
  23. package/dist/Schema.js.map +1 -1
  24. package/dist/SchemaGetter.d.ts +4 -0
  25. package/dist/SchemaGetter.d.ts.map +1 -1
  26. package/dist/SchemaGetter.js +52 -33
  27. package/dist/SchemaGetter.js.map +1 -1
  28. package/dist/Sink.d.ts.map +1 -1
  29. package/dist/Sink.js +38 -17
  30. package/dist/Sink.js.map +1 -1
  31. package/dist/Stream.d.ts +11 -0
  32. package/dist/Stream.d.ts.map +1 -1
  33. package/dist/Stream.js +16 -1
  34. package/dist/Stream.js.map +1 -1
  35. package/dist/Utils.js +8 -3
  36. package/dist/Utils.js.map +1 -1
  37. package/dist/ai/AnthropicStructuredOutput.js +0 -2
  38. package/dist/ai/AnthropicStructuredOutput.js.map +1 -1
  39. package/dist/ai/Chat.d.ts +2 -2
  40. package/dist/ai/Chat.d.ts.map +1 -1
  41. package/dist/ai/LanguageModel.d.ts +38 -7
  42. package/dist/ai/LanguageModel.d.ts.map +1 -1
  43. package/dist/ai/LanguageModel.js +25 -7
  44. package/dist/ai/LanguageModel.js.map +1 -1
  45. package/dist/ai/McpServer.d.ts +55 -0
  46. package/dist/ai/McpServer.d.ts.map +1 -1
  47. package/dist/ai/McpServer.js +132 -22
  48. package/dist/ai/McpServer.js.map +1 -1
  49. package/dist/ai/Prompt.d.ts.map +1 -1
  50. package/dist/ai/Prompt.js +28 -5
  51. package/dist/ai/Prompt.js.map +1 -1
  52. package/dist/ai/Response.d.ts +4 -3
  53. package/dist/ai/Response.d.ts.map +1 -1
  54. package/dist/ai/Response.js.map +1 -1
  55. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  56. package/dist/cluster/ClusterWorkflowEngine.js +46 -8
  57. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  58. package/dist/cluster/Sharding.d.ts.map +1 -1
  59. package/dist/cluster/Sharding.js +6 -4
  60. package/dist/cluster/Sharding.js.map +1 -1
  61. package/dist/encoding/Sse.d.ts +6 -0
  62. package/dist/encoding/Sse.d.ts.map +1 -1
  63. package/dist/encoding/Sse.js +62 -70
  64. package/dist/encoding/Sse.js.map +1 -1
  65. package/dist/graphql/GraphQL.d.ts +349 -0
  66. package/dist/graphql/GraphQL.d.ts.map +1 -0
  67. package/dist/graphql/GraphQL.js +207 -0
  68. package/dist/graphql/GraphQL.js.map +1 -0
  69. package/dist/graphql/GraphQLClient.d.ts +562 -0
  70. package/dist/graphql/GraphQLClient.d.ts.map +1 -0
  71. package/dist/graphql/GraphQLClient.js +498 -0
  72. package/dist/graphql/GraphQLClient.js.map +1 -0
  73. package/dist/graphql/GraphQLClientError.d.ts +445 -0
  74. package/dist/graphql/GraphQLClientError.d.ts.map +1 -0
  75. package/dist/graphql/GraphQLClientError.js +479 -0
  76. package/dist/graphql/GraphQLClientError.js.map +1 -0
  77. package/dist/graphql/GraphQLGroup.d.ts +118 -0
  78. package/dist/graphql/GraphQLGroup.d.ts.map +1 -0
  79. package/dist/graphql/GraphQLGroup.js +109 -0
  80. package/dist/graphql/GraphQLGroup.js.map +1 -0
  81. package/dist/graphql/GraphQLMiddleware.d.ts +249 -0
  82. package/dist/graphql/GraphQLMiddleware.d.ts.map +1 -0
  83. package/dist/graphql/GraphQLMiddleware.js +130 -0
  84. package/dist/graphql/GraphQLMiddleware.js.map +1 -0
  85. package/dist/graphql/GraphQLProtocol.d.ts +731 -0
  86. package/dist/graphql/GraphQLProtocol.d.ts.map +1 -0
  87. package/dist/graphql/GraphQLProtocol.js +719 -0
  88. package/dist/graphql/GraphQLProtocol.js.map +1 -0
  89. package/dist/graphql/GraphQLResolver.d.ts +453 -0
  90. package/dist/graphql/GraphQLResolver.d.ts.map +1 -0
  91. package/dist/graphql/GraphQLResolver.js +552 -0
  92. package/dist/graphql/GraphQLResolver.js.map +1 -0
  93. package/dist/graphql/index.d.ts +40 -0
  94. package/dist/graphql/index.d.ts.map +1 -0
  95. package/dist/graphql/index.js +41 -0
  96. package/dist/graphql/index.js.map +1 -0
  97. package/dist/http/HttpClient.d.ts +15 -0
  98. package/dist/http/HttpClient.d.ts.map +1 -1
  99. package/dist/http/HttpClient.js +24 -10
  100. package/dist/http/HttpClient.js.map +1 -1
  101. package/dist/http/HttpEffect.d.ts.map +1 -1
  102. package/dist/http/HttpEffect.js +14 -7
  103. package/dist/http/HttpEffect.js.map +1 -1
  104. package/dist/http/HttpMiddleware.d.ts.map +1 -1
  105. package/dist/http/HttpMiddleware.js +31 -32
  106. package/dist/http/HttpMiddleware.js.map +1 -1
  107. package/dist/http/HttpRouter.d.ts +35 -0
  108. package/dist/http/HttpRouter.d.ts.map +1 -1
  109. package/dist/http/HttpRouter.js +35 -0
  110. package/dist/http/HttpRouter.js.map +1 -1
  111. package/dist/http/HttpServerResponse.d.ts +8 -6
  112. package/dist/http/HttpServerResponse.d.ts.map +1 -1
  113. package/dist/http/HttpServerResponse.js +27 -16
  114. package/dist/http/HttpServerResponse.js.map +1 -1
  115. package/dist/http/Multipart.d.ts.map +1 -1
  116. package/dist/http/Multipart.js +15 -1
  117. package/dist/http/Multipart.js.map +1 -1
  118. package/dist/http-api/HttpApiScalar.d.ts +5 -0
  119. package/dist/http-api/HttpApiScalar.d.ts.map +1 -1
  120. package/dist/http-api/HttpApiScalar.js.map +1 -1
  121. package/dist/internal/dateTime.js +40 -35
  122. package/dist/internal/dateTime.js.map +1 -1
  123. package/dist/internal/effect.js +8 -3
  124. package/dist/internal/effect.js.map +1 -1
  125. package/dist/internal/graphql.d.ts +2 -0
  126. package/dist/internal/graphql.d.ts.map +1 -0
  127. package/dist/internal/graphql.js +195 -0
  128. package/dist/internal/graphql.js.map +1 -0
  129. package/dist/internal/graphqlHeaders.d.ts +4 -0
  130. package/dist/internal/graphqlHeaders.d.ts.map +1 -0
  131. package/dist/internal/graphqlHeaders.js +11 -0
  132. package/dist/internal/graphqlHeaders.js.map +1 -0
  133. package/dist/internal/request.js +20 -1
  134. package/dist/internal/request.js.map +1 -1
  135. package/dist/internal/schema/toCodeDocument.js +4 -1
  136. package/dist/internal/schema/toCodeDocument.js.map +1 -1
  137. package/dist/internal/schema/toJsonSchemaDocument.js +29 -5
  138. package/dist/internal/schema/toJsonSchemaDocument.js.map +1 -1
  139. package/dist/internal/version.js +1 -1
  140. package/dist/net/Host.d.ts +49 -0
  141. package/dist/net/Host.d.ts.map +1 -1
  142. package/dist/net/Host.js +55 -0
  143. package/dist/net/Host.js.map +1 -1
  144. package/dist/net/NetAddress.d.ts +8 -0
  145. package/dist/net/NetAddress.d.ts.map +1 -1
  146. package/dist/net/NetAddress.js +8 -0
  147. package/dist/net/NetAddress.js.map +1 -1
  148. package/dist/rpc/RpcServer.d.ts.map +1 -1
  149. package/dist/rpc/RpcServer.js +36 -9
  150. package/dist/rpc/RpcServer.js.map +1 -1
  151. package/dist/socket/Socket.d.ts +6 -0
  152. package/dist/socket/Socket.d.ts.map +1 -1
  153. package/dist/socket/Socket.js.map +1 -1
  154. package/dist/workflow/DurableDeferred.d.ts.map +1 -1
  155. package/dist/workflow/DurableDeferred.js +32 -3
  156. package/dist/workflow/DurableDeferred.js.map +1 -1
  157. package/dist/workflow/WorkflowEngine.d.ts +24 -2
  158. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  159. package/dist/workflow/WorkflowEngine.js +11 -2
  160. package/dist/workflow/WorkflowEngine.js.map +1 -1
  161. package/package.json +6 -3
  162. package/src/Context.ts +9 -0
  163. package/src/RequestResolver.ts +4 -10
  164. package/src/Scheduler.ts +9 -0
  165. package/src/Schema.ts +8 -0
  166. package/src/SchemaGetter.ts +47 -35
  167. package/src/Sink.ts +45 -18
  168. package/src/Stream.ts +33 -4
  169. package/src/Utils.ts +8 -5
  170. package/src/ai/AnthropicStructuredOutput.ts +2 -2
  171. package/src/ai/Chat.ts +2 -2
  172. package/src/ai/LanguageModel.ts +96 -24
  173. package/src/ai/McpServer.ts +244 -90
  174. package/src/ai/Prompt.ts +24 -10
  175. package/src/ai/Response.ts +6 -3
  176. package/src/cluster/ClusterWorkflowEngine.ts +51 -12
  177. package/src/cluster/Sharding.ts +12 -9
  178. package/src/encoding/Sse.ts +69 -79
  179. package/src/graphql/GraphQL.ts +494 -0
  180. package/src/graphql/GraphQLClient.ts +912 -0
  181. package/src/graphql/GraphQLClientError.ts +542 -0
  182. package/src/graphql/GraphQLGroup.ts +167 -0
  183. package/src/graphql/GraphQLMiddleware.ts +253 -0
  184. package/src/graphql/GraphQLProtocol.ts +1286 -0
  185. package/src/graphql/GraphQLResolver.ts +1010 -0
  186. package/src/graphql/index.ts +48 -0
  187. package/src/http/HttpClient.ts +54 -10
  188. package/src/http/HttpEffect.ts +11 -5
  189. package/src/http/HttpMiddleware.ts +33 -34
  190. package/src/http/HttpRouter.ts +35 -0
  191. package/src/http/HttpServerResponse.ts +28 -16
  192. package/src/http/Multipart.ts +17 -1
  193. package/src/http-api/HttpApiScalar.ts +5 -0
  194. package/src/internal/dateTime.ts +13 -10
  195. package/src/internal/effect.ts +8 -3
  196. package/src/internal/graphql.ts +263 -0
  197. package/src/internal/graphqlHeaders.ts +11 -0
  198. package/src/internal/request.ts +31 -3
  199. package/src/internal/schema/toCodeDocument.ts +7 -1
  200. package/src/internal/schema/toJsonSchemaDocument.ts +34 -2
  201. package/src/internal/version.ts +1 -1
  202. package/src/net/Host.ts +72 -0
  203. package/src/net/NetAddress.ts +9 -0
  204. package/src/rpc/RpcServer.ts +55 -15
  205. package/src/socket/Socket.ts +6 -0
  206. package/src/workflow/DurableDeferred.ts +43 -5
  207. package/src/workflow/WorkflowEngine.ts +48 -4
@@ -0,0 +1,263 @@
1
+ import { isReadonlyArrayNonEmpty, type NonEmptyReadonlyArray } from "../Array.ts"
2
+ import * as Context from "../Context.ts"
3
+ import * as Effect from "../Effect.ts"
4
+ import type * as GraphQL from "../graphql/GraphQL.ts"
5
+ import type { DocumentTemplate } from "../graphql/GraphQL.ts"
6
+ import type { PartialResult } from "../graphql/GraphQLClient.ts"
7
+ import {
8
+ DecodeError,
9
+ EncodeError,
10
+ GraphQLClientError,
11
+ type GraphQLError,
12
+ type Reason,
13
+ ResponseError
14
+ } from "../graphql/GraphQLClientError.ts"
15
+ import type { ExecutionResult, GraphQLRequest } from "../graphql/GraphQLProtocol.ts"
16
+ import * as Schema from "../Schema.ts"
17
+
18
+ /**
19
+ * Builds a response error with raw data and transport metadata.
20
+ *
21
+ * @internal
22
+ */
23
+ export const responseError = (
24
+ result: ExecutionResult | undefined,
25
+ errors: NonEmptyReadonlyArray<GraphQLError>
26
+ ): ResponseError =>
27
+ new ResponseError({
28
+ errors,
29
+ ...(result?.data === undefined ? {} : { data: result.data as Schema.Json }),
30
+ ...(result?.extensions === undefined ? {} : { extensions: result.extensions }),
31
+ ...(result?.status === undefined ? {} : { status: result.status }),
32
+ ...(result?.headers === undefined ? {} : { headers: result.headers })
33
+ })
34
+
35
+ /**
36
+ * Raw responses for partial results, used by resolvers to preserve error
37
+ * metadata. Mock client results have no entry.
38
+ *
39
+ * @internal
40
+ */
41
+ export const partialResponses = new WeakMap<object, ExecutionResult>()
42
+
43
+ /**
44
+ * Joins two printed pieces, adding a space only where two non-punctuators
45
+ * would otherwise touch. This is the generator printer's rule, so joining a
46
+ * generated template reproduces the printed document exactly.
47
+ */
48
+ const concat = (left: string, right: string): string =>
49
+ /[\w"-]$/.test(left) && /^[\w"-]/.test(right) ? `${left} ${right}` : left + right
50
+
51
+ const concatAll = (pieces: ReadonlyArray<string>): string => pieces.reduce(concat, "")
52
+
53
+ /** Renders template parts, prefixing every name (the odd entries). */
54
+ const renderParts = (parts: DocumentTemplate.Parts, prefix: string): string =>
55
+ parts.map((part, i) => i % 2 === 1 ? prefix + part : part).join("")
56
+
57
+ const isShared = (fragment: DocumentTemplate.Fragment): boolean => fragment.body.length <= 1
58
+
59
+ /**
60
+ * Renders the template once per prefix. The empty prefix renders it as
61
+ * written; any other prefix renames variables and per-slice fragments and
62
+ * aliases root fields to the prefixed response key.
63
+ */
64
+ const render = (
65
+ kind: string,
66
+ name: string,
67
+ template: DocumentTemplate,
68
+ prefixes: ReadonlyArray<string>
69
+ ): string =>
70
+ concatAll([
71
+ kind,
72
+ name,
73
+ template.variables.length === 0 ? "" : `(${
74
+ concatAll(
75
+ prefixes.flatMap((prefix) =>
76
+ template.variables.map((variable) => `$${prefix}${variable.name}:${variable.body}`)
77
+ )
78
+ )
79
+ })`,
80
+ template.directives === undefined ? "" : renderParts(template.directives, ""),
81
+ `{${
82
+ concatAll(prefixes.flatMap((prefix) =>
83
+ template.selections.map((selection) => {
84
+ const key = prefix === "" ? selection.alias : `${prefix}${selection.alias ?? selection.name}`
85
+ return `${key === undefined ? "" : `${key}:`}${selection.name ?? ""}${renderParts(selection.body, prefix)}`
86
+ })
87
+ ))
88
+ }}`,
89
+ ...template.fragments.flatMap((fragment) =>
90
+ (isShared(fragment) ? [""] : prefixes).map((prefix) =>
91
+ concatAll(["fragment", prefix + fragment.name, renderParts(fragment.body, prefix)])
92
+ )
93
+ )
94
+ ])
95
+
96
+ /**
97
+ * Joins a template into the document it was split from.
98
+ *
99
+ * @internal
100
+ */
101
+ export const joinTemplate = (kind: string, name: string, template: DocumentTemplate): string =>
102
+ render(kind, name, template, [""])
103
+
104
+ /**
105
+ * Why the template can't be sliced for any prefixes, if it can't: an operation
106
+ * directive references a variable, or a root selection is a fragment spread or
107
+ * inline fragment.
108
+ *
109
+ * @internal
110
+ */
111
+ export const templateUnsliceableReason = (template: DocumentTemplate): string | undefined => {
112
+ if (template.directives !== undefined && template.directives.length > 1) {
113
+ return "its operation directives reference variables"
114
+ }
115
+ if (template.selections.some((selection) => selection.name === undefined)) {
116
+ return "it has a fragment spread or inline fragment at the root"
117
+ }
118
+ return undefined
119
+ }
120
+
121
+ /**
122
+ * Renders one operation that repeats the template once per prefix. Each
123
+ * slice's variables, root response keys and per-slice fragments are renamed
124
+ * with its prefix; operation directives and shared fragments appear once.
125
+ * Prefixes must start a GraphQL name, and none may be a prefix of another.
126
+ *
127
+ * Throws rather than producing an invalid document when the template is
128
+ * unsliceable (see {@link templateUnsliceableReason}) or a renamed fragment
129
+ * would take a shared fragment's name.
130
+ *
131
+ * @internal
132
+ */
133
+ export const renderSlices = (
134
+ kind: string,
135
+ name: string,
136
+ template: DocumentTemplate,
137
+ prefixes: NonEmptyReadonlyArray<string>
138
+ ): string => {
139
+ const shared = new Set(template.fragments.filter(isShared).map((fragment) => fragment.name))
140
+ const collision = template.fragments
141
+ .flatMap((fragment) => isShared(fragment) ? [] : prefixes.map((prefix) => prefix + fragment.name))
142
+ .find((renamed) => shared.has(renamed))
143
+ const reason = templateUnsliceableReason(template) ??
144
+ (collision === undefined ? undefined : `fragment ${collision} would collide with a shared fragment`)
145
+ if (reason !== undefined) throw new Error(`Cannot render slices of operation ${name}: ${reason}`)
146
+ return render(kind, name, template, prefixes)
147
+ }
148
+
149
+ /**
150
+ * Forces POST for alias batches to avoid GET URL length limits.
151
+ *
152
+ * @internal
153
+ */
154
+ export const ForcePost = Context.Reference<boolean>("effect/graphql/ForcePost", { defaultValue: () => false })
155
+
156
+ /**
157
+ * JSON with object keys sorted, so equal JSON values give equal strings.
158
+ *
159
+ * @internal
160
+ */
161
+ export const canonicalJson = (value: unknown): string =>
162
+ JSON.stringify(value, (_, value) =>
163
+ value !== null && typeof value === "object" && !Array.isArray(value)
164
+ ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, value[key]]))
165
+ : value)
166
+
167
+ /**
168
+ * Shared variable encoding, result decoding and client error mapping.
169
+ *
170
+ * @internal
171
+ */
172
+ export const makeCodec = <Op extends GraphQL.Any>(operation: Op) => {
173
+ const fail = (reason: Reason) => new GraphQLClientError({ operation: operation.name, reason })
174
+ const schemaFail = (make: (description: string) => Reason) => (error: Schema.SchemaError) =>
175
+ Effect.fail(fail(make(error.message)))
176
+ const encode = Schema.encodeEffect(operation.variables)
177
+ const decode = Schema.decodeUnknownEffect(operation.result)
178
+
179
+ const encodeVariables = (
180
+ variables: GraphQL.Variables<Op> | undefined
181
+ ): Effect.Effect<unknown, GraphQLClientError, Op["variables"]["EncodingServices"]> =>
182
+ Effect.catch(encode(variables ?? {}), schemaFail((description) => new EncodeError({ description })))
183
+
184
+ const decodeData = (
185
+ data: unknown
186
+ ): Effect.Effect<GraphQL.Result<Op>, GraphQLClientError, Op["result"]["DecodingServices"]> =>
187
+ Effect.catch(decode(data), schemaFail((description) => new DecodeError({ description })))
188
+
189
+ // GraphQL requires errors when data is null or missing. Check before decoding,
190
+ // since schemas such as Schema.Json accept null.
191
+ const failResponse = (result: ExecutionResult): Effect.Effect<never, GraphQLClientError> => {
192
+ const errors = result.errors ?? []
193
+ return Effect.fail(fail(
194
+ isReadonlyArrayNonEmpty(errors)
195
+ ? responseError(result, errors)
196
+ : new DecodeError({ description: "data is null or missing without errors" })
197
+ ))
198
+ }
199
+
200
+ const decodeStrict = (
201
+ result: ExecutionResult
202
+ ): Effect.Effect<GraphQL.Result<Op>, GraphQLClientError, Op["result"]["DecodingServices"]> =>
203
+ isReadonlyArrayNonEmpty(result.errors ?? []) || result.data === null || result.data === undefined
204
+ ? failResponse(result)
205
+ : decodeData(result.data)
206
+
207
+ const decodePartial = (
208
+ result: ExecutionResult
209
+ ): Effect.Effect<PartialResult<GraphQL.Result<Op>>, GraphQLClientError, Op["result"]["DecodingServices"]> => {
210
+ if (result.data === null || result.data === undefined) return failResponse(result)
211
+ return Effect.map(decodeData(result.data), (data): PartialResult<GraphQL.Result<Op>> => {
212
+ const partial = { data, errors: result.errors ?? [] }
213
+ partialResponses.set(partial, result)
214
+ return partial
215
+ })
216
+ }
217
+
218
+ return { fail, schemaFail, encodeVariables, failResponse, decodeStrict, decodePartial } as const
219
+ }
220
+
221
+ /**
222
+ * Each real client method's runner for batches: its middleware and protocol,
223
+ * sending as POST. Mock and custom methods have none.
224
+ *
225
+ * @internal
226
+ */
227
+ export const rawExecutors = new WeakMap<
228
+ object,
229
+ (request: GraphQLRequest) => Effect.Effect<ExecutionResult, unknown, unknown>
230
+ >()
231
+
232
+ /**
233
+ * Splits a batched response, removing prefixes from root keys and error paths.
234
+ * Returns undefined if an error matches no slice or data is null or missing.
235
+ *
236
+ * @internal
237
+ */
238
+ export const splitSlices = (
239
+ result: ExecutionResult,
240
+ template: DocumentTemplate,
241
+ prefixes: ReadonlyArray<string>
242
+ ): ReadonlyArray<ExecutionResult> | undefined => {
243
+ const sliceErrors = prefixes.map((): Array<GraphQLError> => [])
244
+ for (const error of result.errors ?? []) {
245
+ const key = error.path?.[0]
246
+ // No prefix is a prefix of another, so at most one matches.
247
+ const index = typeof key === "string" ? prefixes.findIndex((prefix) => key.startsWith(prefix)) : -1
248
+ if (index === -1) return undefined
249
+ sliceErrors[index].push(
250
+ { ...error, path: [(key as string).slice(prefixes[index].length), ...error.path!.slice(1)] } as GraphQLError
251
+ )
252
+ }
253
+ if (result.data === null || result.data === undefined) return undefined
254
+ const data = result.data as Record<string, unknown>
255
+ return prefixes.map((prefix, i) => {
256
+ const sliceData: Record<string, unknown> = {}
257
+ for (const selection of template.selections) {
258
+ const key = (selection.alias ?? selection.name)!
259
+ if (Object.hasOwn(data, prefix + key)) sliceData[key] = data[prefix + key]
260
+ }
261
+ return { ...result, data: sliceData, errors: sliceErrors[i] }
262
+ })
263
+ }
@@ -0,0 +1,11 @@
1
+ import * as Headers from "../http/Headers.ts"
2
+ import * as Schema from "../Schema.ts"
3
+ import * as SchemaGetter from "../SchemaGetter.ts"
4
+
5
+ // Keep headers branded at runtime while encoding only their string entries.
6
+ export const HeadersSchema = Schema.Record(Schema.String, Schema.String).pipe(
7
+ Schema.decodeTo(Schema.declare<Headers.Headers>(Headers.isHeaders), {
8
+ decode: SchemaGetter.transform(Headers.fromInput),
9
+ encode: SchemaGetter.transform((headers) => ({ ...headers }))
10
+ })
11
+ )
@@ -4,6 +4,8 @@ import type { Effect } from "../Effect.ts"
4
4
  import type { Exit } from "../Exit.ts"
5
5
  import type { Fiber } from "../Fiber.ts"
6
6
  import { dual } from "../Function.ts"
7
+ import * as MutableHashMap from "../MutableHashMap.ts"
8
+ import * as Option from "../Option.ts"
7
9
  import type * as Request from "../Request.ts"
8
10
  import { makeEntry } from "../Request.ts"
9
11
  import type { RequestResolver } from "../RequestResolver.ts"
@@ -67,10 +69,36 @@ export const requestUnsafe = <A extends Request.Any>(
67
69
  return () => removeEntryUnsafe(options.resolver, entry)
68
70
  }
69
71
 
72
+ /**
73
+ * Pending batches of resolvers whose `batchKey` is in this set match keys with
74
+ * `Equal` instead of by reference.
75
+ *
76
+ * @internal
77
+ */
78
+ export const equalBatchKeys = new WeakSet<Function>()
79
+
80
+ interface BatchMap {
81
+ get(key: unknown): Batch | undefined
82
+ has(key: unknown): boolean
83
+ set(key: unknown, batch: Batch): void
84
+ delete(key: unknown): void
85
+ }
86
+
87
+ const makeBatchMap = (resolver: RequestResolver<any>): BatchMap => {
88
+ if (!equalBatchKeys.has(resolver.batchKey)) return new Map()
89
+ const map = MutableHashMap.empty<unknown, Batch>()
90
+ return {
91
+ get: (key) => Option.getOrUndefined(MutableHashMap.get(map, key)),
92
+ has: (key) => MutableHashMap.has(map, key),
93
+ set: (key, batch) => MutableHashMap.set(map, key, batch),
94
+ delete: (key) => MutableHashMap.remove(map, key)
95
+ }
96
+ }
97
+
70
98
  interface Batch {
71
99
  key: unknown
72
100
  resolver: RequestResolver<any>
73
- map: Map<unknown, Batch>
101
+ map: BatchMap
74
102
  readonly entrySet: Set<Request.Entry<any>>
75
103
  readonly entries: Set<Request.Entry<any>>
76
104
  readonly delayEffect: Effect<void>
@@ -79,7 +107,7 @@ interface Batch {
79
107
  }
80
108
 
81
109
  const batchPool: Array<Batch> = []
82
- const pendingBatches = new WeakMap<RequestResolver<any>, Map<unknown, Batch>>()
110
+ const pendingBatches = new WeakMap<RequestResolver<any>, BatchMap>()
83
111
 
84
112
  const addEntry = <A extends Request.Any>(
85
113
  resolver: RequestResolver<A>,
@@ -93,7 +121,7 @@ const addEntry = <A extends Request.Any>(
93
121
  ) => {
94
122
  let batchMap = pendingBatches.get(resolver)
95
123
  if (!batchMap) {
96
- batchMap = new Map<object, Batch>()
124
+ batchMap = makeBatchMap(resolver)
97
125
  pendingBatches.set(resolver, batchMap)
98
126
  }
99
127
  let batch: Batch | undefined
@@ -48,6 +48,8 @@ function renderEmittableAnnotation(input: unknown): string | undefined {
48
48
  return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`
49
49
  }
50
50
 
51
+ const canonicalFiniteCheck = `Schema.isFinite().annotate({ "expected": "a finite number" })`
52
+
51
53
  function renderAnnotations(
52
54
  annotations: Schema.Annotations.Annotations | undefined
53
55
  ): string | undefined {
@@ -341,7 +343,11 @@ export function toCodeDocument(
341
343
  let runtime = base.runtime + runtimeAnnotate(representation.annotations)
342
344
  for (let index = 0; index < representation.checks.length; index++) {
343
345
  const check = representation.checks[index]
344
- runtime += `.check(${compileCheck(check, [...path, "checks", index])})`
346
+ const compiledCheck = compileCheck(check, [...path, "checks", index])
347
+ // Only collapse a bare number's leading canonical check.
348
+ runtime = runtime === "Schema.Number" && compiledCheck === canonicalFiniteCheck
349
+ ? "Schema.Finite"
350
+ : `${runtime}.check(${compiledCheck})`
345
351
  }
346
352
  return makeCode(runtime, base.Type)
347
353
  }
@@ -121,6 +121,37 @@ function hasNoCollisions(left: JsonSchema.JsonSchema, rightKeys: ReadonlyArray<s
121
121
  return typeof left.$ref !== "string" && rightKeys.every((key) => !Object.hasOwn(left, key))
122
122
  }
123
123
 
124
+ // Every bound on one schema object applies, so a repeated bound reduces to its tighter value.
125
+ const lowerBoundKeywords = "|minimum|exclusiveMinimum|minLength|minItems|minProperties|"
126
+ const upperBoundKeywords = "|maximum|exclusiveMaximum|maxLength|maxItems|maxProperties|"
127
+
128
+ // Tightens repeated bounds and drops an inclusive or exclusive bound implied by the other on the same side. Returns
129
+ // `undefined` for a `$ref` schema or a non-bound collision, so the caller falls back to `allOf`.
130
+ function mergeInlineCheck(
131
+ left: JsonSchema.JsonSchema,
132
+ right: JsonSchema.JsonSchema,
133
+ rightKeys: ReadonlyArray<string>
134
+ ): JsonSchema.JsonSchema | undefined {
135
+ if (typeof left.$ref === "string") return undefined
136
+ const out: JsonSchema.JsonSchema = { ...left, ...right }
137
+ for (const key of rightKeys) {
138
+ if (!Object.hasOwn(left, key)) continue
139
+ const a = left[key]
140
+ const b = right[key]
141
+ if (typeof a !== "number" || typeof b !== "number") return undefined
142
+ if (lowerBoundKeywords.includes(`|${key}|`)) out[key] = Math.max(a, b)
143
+ else if (upperBoundKeywords.includes(`|${key}|`)) out[key] = Math.min(a, b)
144
+ else return undefined
145
+ }
146
+ if (typeof out.minimum === "number" && typeof out.exclusiveMinimum === "number") {
147
+ delete out[out.exclusiveMinimum >= out.minimum ? "minimum" : "exclusiveMinimum"]
148
+ }
149
+ if (typeof out.maximum === "number" && typeof out.exclusiveMaximum === "number") {
150
+ delete out[out.exclusiveMaximum <= out.maximum ? "maximum" : "exclusiveMaximum"]
151
+ }
152
+ return out
153
+ }
154
+
124
155
  // `format` and `content*` can affect validation, so they are not treated as pure annotations.
125
156
  const promotableAnnotationKeywords = "|title|description|default|examples|readOnly|writeOnly|"
126
157
  const inlineableAnnotatedCheckKeywords = inlineableCheckKeywords + promotableAnnotationKeywords
@@ -149,8 +180,9 @@ function appendJsonSchema(
149
180
  : appendJsonSchema(base, extracted.schema, inlineCheck)
150
181
  }
151
182
  }
152
- if (inlineCheck && hasNoCollisions(left, rightKeys)) {
153
- return { ...left, ...right }
183
+ if (inlineCheck) {
184
+ const merged = mergeInlineCheck(left, right, rightKeys)
185
+ if (merged !== undefined) return merged
154
186
  }
155
187
  const members = Array.isArray(right.allOf) && rightKeys.length === 1 ? right.allOf : [right]
156
188
  if (Array.isArray(left.allOf)) {
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = "4.0.3"
2
+ export const version = "4.0.4"
package/src/net/Host.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * @stability experimental
10
10
  * @since 4.0.0
11
11
  */
12
+ import type * as Arr from "../Array.ts"
12
13
  import type * as Brand from "../Brand.ts"
13
14
  import * as Equal from "../Equal.ts"
14
15
  import * as Hash from "../Hash.ts"
@@ -321,6 +322,28 @@ export const formatHost = (self: Host): string => typeof self === "string" ? sel
321
322
  */
322
323
  export const isFullyQualified = (self: DomainName): boolean => self.endsWith(".")
323
324
 
325
+ /**
326
+ * Appends the trailing dot to a domain name, unless it is already fully
327
+ * qualified.
328
+ *
329
+ * @stability experimental
330
+ * @category converting
331
+ * @since 4.0.0
332
+ */
333
+ export const toFullyQualified = (self: DomainName): DomainName =>
334
+ isFullyQualified(self) ? self : `${self}.` as DomainName
335
+
336
+ /**
337
+ * Removes the trailing dot of a fully qualified domain name, except from the
338
+ * root name `.`.
339
+ *
340
+ * @stability experimental
341
+ * @category converting
342
+ * @since 4.0.0
343
+ */
344
+ export const toRelative = (self: DomainName): DomainName =>
345
+ self.length > 1 && isFullyQualified(self) ? self.slice(0, -1) as DomainName : self
346
+
324
347
  const HostPortProto = {
325
348
  ...Inspectable.BaseProto,
326
349
  _tag: "HostPort",
@@ -470,3 +493,52 @@ export const formatHostPort = (self: HostPort): string => {
470
493
  const host = formatHost(self.host)
471
494
  return host.includes(":") ? `[${host}]:${self.port}` : `${host}:${self.port}`
472
495
  }
496
+
497
+ /**
498
+ * The addresses of host names, as read from a hosts file.
499
+ *
500
+ * @see {@link parseHostsFile} for reading a hosts file
501
+ * @stability experimental
502
+ * @category models
503
+ * @since 4.0.0
504
+ */
505
+ export type HostsTable = ReadonlyMap<DomainName, Arr.NonEmptyReadonlyArray<NetAddress.IpAddress>>
506
+
507
+ /**
508
+ * Parses a hosts file into the addresses of each host name. Invalid entries
509
+ * are skipped.
510
+ *
511
+ * **Example** (Parsing a hosts file)
512
+ *
513
+ * ```ts import.meta.vitest
514
+ * import { Host, NetAddress } from "effect/net"
515
+ *
516
+ * const hosts = Host.parseHostsFile(`
517
+ * 127.0.0.1 localhost
518
+ * ::1 localhost ip6-localhost # loopback
519
+ * `)
520
+ * hosts.get(Host.domainNameFromStringUnsafe("localhost"))?.map(NetAddress.formatIp) // => ["127.0.0.1", "::1"]
521
+ * ```
522
+ *
523
+ * @stability experimental
524
+ * @category decoding
525
+ * @since 4.0.0
526
+ */
527
+ export const parseHostsFile = (text: string): HostsTable => {
528
+ const hosts = new Map<DomainName, Array<NetAddress.IpAddress>>()
529
+ for (const line of text.split(/\r?\n/)) {
530
+ const comment = line.indexOf("#")
531
+ const [first, ...names] = (comment === -1 ? line : line.slice(0, comment)).trim().split(/\s+/)
532
+ const address = NetAddress.ipFromString(first)
533
+ if (Result.isFailure(address)) continue
534
+ for (const name of names) {
535
+ const domain = domainNameFromString(name)
536
+ if (Result.isFailure(domain)) continue
537
+ const key = toRelative(domain.success)
538
+ const addresses = hosts.get(key)
539
+ if (addresses === undefined) hosts.set(key, [address.success])
540
+ else if (!addresses.some((existing) => Equal.equals(existing, address.success))) addresses.push(address.success)
541
+ }
542
+ }
543
+ return hosts as unknown as HostsTable
544
+ }
@@ -864,6 +864,15 @@ export const ipv4Loopback: Ipv4Address = makeIpv4(0x7f000001)
864
864
  */
865
865
  export const ipv6Loopback: Ipv6Address = makeIpv6(0, 0, 0, 1)
866
866
 
867
+ /**
868
+ * The loopback addresses `127.0.0.1` and `::1`.
869
+ *
870
+ * @stability unstable
871
+ * @category constants
872
+ * @since 4.0.0
873
+ */
874
+ export const loopbackAddresses: readonly [Ipv4Address, Ipv6Address] = [ipv4Loopback, ipv6Loopback]
875
+
867
876
  /**
868
877
  * The unspecified IPv4 address `0.0.0.0`.
869
878
  *
@@ -494,6 +494,57 @@ export const makeNoSerialization: <Rpcs extends Rpc.Any>(
494
494
  })
495
495
  })
496
496
 
497
+ // Built-in JSON codecs depend only on the RPC schemas and can be shared.
498
+ // Other factories may capture state at construction, so keep them per server.
499
+ const isSharedCodecFor = (codecFor: RpcSerialization.CodecFor) => codecFor === RpcSerialization.json.codecFor
500
+
501
+ interface RpcCodecs {
502
+ readonly decode: (u: unknown) => Effect.Effect<any, Schema.SchemaError>
503
+ readonly encodeChunk: (
504
+ u: NonEmptyReadonlyArray<unknown>
505
+ ) => Effect.Effect<NonEmptyReadonlyArray<unknown>, Schema.SchemaError>
506
+ readonly encodeExit: (u: unknown) => Effect.Effect<ResponseExitEncoded["exit"], Schema.SchemaError>
507
+ }
508
+
509
+ const sharedRpcCodecs = new WeakMap<Rpc.AnyWithProps, RpcCodecs>()
510
+
511
+ const rpcCodecs = (codecFor: RpcSerialization.CodecFor, rpc: Rpc.AnyWithProps): RpcCodecs => {
512
+ const shared = isSharedCodecFor(codecFor)
513
+ let codecs = shared ? sharedRpcCodecs.get(rpc) : undefined
514
+ if (codecs === undefined) {
515
+ const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema)
516
+ codecs = {
517
+ decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)) as any,
518
+ encodeChunk: Schema.encodeUnknownEffect(
519
+ codecFor(
520
+ Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any)
521
+ )
522
+ ) as any,
523
+ encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) as any
524
+ }
525
+ if (shared) sharedRpcCodecs.set(rpc, codecs)
526
+ }
527
+ return codecs
528
+ }
529
+
530
+ interface DefectCodecs {
531
+ readonly encodeDefectUnsafe: (defect: unknown) => unknown
532
+ readonly encodeUnknownRequestExit: (u: unknown) => Effect.Effect<ResponseExitEncoded["exit"], Schema.SchemaError>
533
+ }
534
+
535
+ let sharedDefectCodecs: DefectCodecs | undefined
536
+
537
+ const makeDefectCodecs = (codecFor: RpcSerialization.CodecFor): DefectCodecs => ({
538
+ encodeDefectUnsafe: Schema.encodeSync(codecFor(Schema.Defect())),
539
+ // Unknown tags have no RPC schema, so use a defect-only exit schema.
540
+ encodeUnknownRequestExit: Schema.encodeUnknownEffect(
541
+ codecFor(Schema.Exit(Schema.Never, Schema.Never, Schema.Defect()))
542
+ ) as any
543
+ })
544
+
545
+ const defectCodecs = (codecFor: RpcSerialization.CodecFor): DefectCodecs =>
546
+ isSharedCodecFor(codecFor) ? (sharedDefectCodecs ??= makeDefectCodecs(codecFor)) : makeDefectCodecs(codecFor)
547
+
497
548
  // Merging the handler entry services with the request fiber services costs
498
549
  // O(services), so the merged context is cached per (entry, fiber context)
499
550
  // pair. Request fibers on persistent connections keep one context across
@@ -586,11 +637,7 @@ export const make: <Rpcs extends Rpc.Any>(
586
637
  supportsSpanPropagation,
587
638
  supportsTransferables
588
639
  } = yield* Protocol
589
- const encodeDefectUnsafe = Schema.encodeSync(codecFor(Schema.Defect()))
590
- // Unknown tags have no RPC schema, so use a defect-only exit schema.
591
- const encodeUnknownRequestExit: Schemas["encodeExit"] = Schema.encodeUnknownEffect(
592
- codecFor(Schema.Exit(Schema.Never, Schema.Never, Schema.Defect()))
593
- ) as any
640
+ const { encodeDefectUnsafe, encodeUnknownRequestExit } = defectCodecs(codecFor)
594
641
  const services = yield* Effect.context<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>>()
595
642
  const scope = yield* Scope.make()
596
643
 
@@ -666,16 +713,9 @@ export const make: <Rpcs extends Rpc.Any>(
666
713
  let schemas = schemasCache.get(rpc)
667
714
  if (!schemas) {
668
715
  const entry = Context.getOrUndefinedUnsafe(services, rpc.key) as Rpc.Handler<Rpcs["_tag"]>
669
- const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema)
670
716
  schemas = {
671
717
  tag: rpc._tag,
672
- decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)) as any,
673
- encodeChunk: Schema.encodeUnknownEffect(
674
- codecFor(
675
- Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any)
676
- )
677
- ) as any,
678
- encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) as any,
718
+ ...rpcCodecs(codecFor, rpc),
679
719
  context: entry.context
680
720
  }
681
721
  schemasCache.set(rpc, schemas)
@@ -1109,7 +1149,7 @@ export const makeProtocolWithHttpEffect: (
1109
1149
  RpcSerialization.RpcSerialization
1110
1150
  > = Effect.fnUntraced(function*(options = {}) {
1111
1151
  const serialization = yield* RpcSerialization.RpcSerialization
1112
- const encodeDefectUnsafe = Schema.encodeSync(serialization.codecFor(Schema.Defect()))
1152
+ const { encodeDefectUnsafe } = defectCodecs(serialization.codecFor)
1113
1153
  const includesFraming = serialization.includesFraming
1114
1154
  const isBinary = !serialization.contentType.includes("json")
1115
1155
 
@@ -1569,7 +1609,7 @@ const makeSocketProtocol: Effect.Effect<
1569
1609
  RpcSerialization.RpcSerialization
1570
1610
  > = Effect.gen(function*() {
1571
1611
  const serialization = yield* RpcSerialization.RpcSerialization
1572
- const encodeDefectUnsafe = Schema.encodeSync(serialization.codecFor(Schema.Defect()))
1612
+ const { encodeDefectUnsafe } = defectCodecs(serialization.codecFor)
1573
1613
  const disconnects = yield* Queue.make<number>()
1574
1614
 
1575
1615
  let clientId = 0
@@ -161,6 +161,12 @@ export interface Writer {
161
161
  * @since 4.0.0
162
162
  */
163
163
  export interface TlsUpgradeOptions {
164
+ /**
165
+ * DNS hostname used for Server Name Indication (SNI) and certificate identity
166
+ * verification during client upgrades. Server upgrades ignore this option.
167
+ * When omitted, the adapter retains its existing hostname behavior.
168
+ */
169
+ readonly servername?: string | undefined
164
170
  readonly key?:
165
171
  | Redacted.Redacted<string | Uint8Array>
166
172
  | ReadonlyArray<Redacted.Redacted<string | Uint8Array>>