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,1286 @@
1
+ /**
2
+ * Transports for `GraphQLClient`. They accept encoded requests and return raw
3
+ * response bodies with available HTTP status and headers, without schema
4
+ * decoding or middleware.
5
+ *
6
+ * - {@link layerHttp} uses HTTP POST (or GET for queries) and graphql-sse
7
+ * subscriptions, with an optional graphql-ws subscription transport.
8
+ * - {@link layerWebSocket} multiplexes all operations over graphql-ws.
9
+ *
10
+ * {@link makeHttp} and {@link makeWebSocket} construct the same transports as effects.
11
+ *
12
+ * @stability experimental
13
+ * @since 4.0.0
14
+ */
15
+ import * as Cause from "../Cause.ts"
16
+ import * as Clock from "../Clock.ts"
17
+ import * as Context from "../Context.ts"
18
+ import * as Deferred from "../Deferred.ts"
19
+ import * as Duration from "../Duration.ts"
20
+ import * as Effect from "../Effect.ts"
21
+ import * as Sse from "../encoding/Sse.ts"
22
+ import * as Fiber from "../Fiber.ts"
23
+ import * as Headers from "../http/Headers.ts"
24
+ import * as HttpClient from "../http/HttpClient.ts"
25
+ import * as HttpClientError from "../http/HttpClientError.ts"
26
+ import * as HttpClientRequest from "../http/HttpClientRequest.ts"
27
+ import * as HttpClientResponse from "../http/HttpClientResponse.ts"
28
+ import * as UrlParams from "../http/UrlParams.ts"
29
+ import * as internalGraphQL from "../internal/graphql.ts"
30
+ import { HeadersSchema } from "../internal/graphqlHeaders.ts"
31
+ import * as Layer from "../Layer.ts"
32
+ import * as Queue from "../Queue.ts"
33
+ import * as Result from "../Result.ts"
34
+ import * as Schema from "../Schema.ts"
35
+ import type * as Scope from "../Scope.ts"
36
+ import * as Socket from "../socket/Socket.ts"
37
+ import * as Stream from "../Stream.ts"
38
+ import type { Kind } from "./GraphQL.ts"
39
+ import { GraphQLError, TransportError } from "./GraphQLClientError.ts"
40
+
41
+ /**
42
+ * The request a transport sends. Middleware sees and may rewrite it before it
43
+ * reaches the transport.
44
+ *
45
+ * **Details**
46
+ *
47
+ * `kind` is the operation type, which the HTTP transport uses to send queries
48
+ * as `GET` when `queryMethod: "GET"` is set. `variables` are already encoded
49
+ * through the operation's variables Schema.
50
+ *
51
+ * `headers` and `urlParams` are applied by the HTTP transport, to queries,
52
+ * mutations and graphql-sse subscriptions alike. A graphql-ws transport
53
+ * ignores both, because graphql-ws has no per-operation headers or URL.
54
+ * Request headers are normalized, immutable `Headers.Headers` values.
55
+ *
56
+ * @stability experimental
57
+ * @category models
58
+ * @since 4.0.0
59
+ */
60
+ export interface GraphQLRequest {
61
+ readonly kind: Kind
62
+ readonly query: string
63
+ readonly operationName: string
64
+ readonly variables: unknown
65
+ readonly extensions?: Record<string, unknown> | undefined
66
+ readonly headers: Headers.Headers
67
+ readonly urlParams: UrlParams.UrlParams
68
+ }
69
+
70
+ /**
71
+ * A raw GraphQL result before schema decoding. Middleware receive it from
72
+ * `next` and can read response `extensions`, such as rate-limit costs.
73
+ *
74
+ * **Details**
75
+ *
76
+ * `status` and `headers` are transport metadata, not part of the GraphQL
77
+ * response body: the client decodes the body without them and then copies
78
+ * what the transport reported. Middleware may read or rewrite them, and a
79
+ * failed call's `ResponseError` carries the final values. Headers use
80
+ * `Headers.Headers` at runtime and encode as a plain string record.
81
+ *
82
+ * @stability experimental
83
+ * @category schemas
84
+ * @since 4.0.0
85
+ */
86
+ export const ExecutionResult = Schema.Struct({
87
+ data: Schema.optional(Schema.Unknown),
88
+ errors: Schema.optional(Schema.Array(GraphQLError)),
89
+ extensions: Schema.optional(Schema.Record(Schema.String, Schema.Json)),
90
+ status: Schema.optional(Schema.Int),
91
+ headers: Schema.optional(HeadersSchema)
92
+ })
93
+
94
+ /**
95
+ * The decoded type of {@link ExecutionResult}.
96
+ *
97
+ * @stability experimental
98
+ * @category models
99
+ * @since 4.0.0
100
+ */
101
+ export type ExecutionResult = typeof ExecutionResult.Type
102
+
103
+ /**
104
+ * The transport service. `execute` answers a query or mutation with the raw
105
+ * JSON body, and `subscribe` turns a subscription into a stream of raw events.
106
+ * Both fail only with `TransportError`; GraphQL `errors` are left in the body
107
+ * for the client to interpret.
108
+ *
109
+ * **Details**
110
+ *
111
+ * Each answer is `{ body, status?, headers? }`. The HTTP transport sets
112
+ * `headers` on every answer, including graphql-sse events, and `status` on
113
+ * all but graphql-sse events. Headers are `Headers.Headers` values.
114
+ * graphql-ws leaves both unset.
115
+ *
116
+ * **Example** (A scripted transport for tests)
117
+ *
118
+ * ```ts import.meta.vitest
119
+ * import { Effect, Layer, Schema, Stream } from "effect"
120
+ * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
121
+ *
122
+ * const Viewer = GraphQL.query("Viewer", {
123
+ * document: "query Viewer{viewer{login}}",
124
+ * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
125
+ * })
126
+ *
127
+ * const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, {
128
+ * execute: () => Effect.succeed({ body: { data: { viewer: { login: "tim" } } } }),
129
+ * subscribe: () => Stream.empty
130
+ * })
131
+ *
132
+ * const program = Effect.gen(function*() {
133
+ * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer))
134
+ * return yield* client.Viewer()
135
+ * })
136
+ *
137
+ * await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => { viewer: { login: "tim" } }
138
+ * ```
139
+ *
140
+ * @stability experimental
141
+ * @category services
142
+ * @since 4.0.0
143
+ */
144
+ export class GraphQLProtocol extends Context.Service<GraphQLProtocol, {
145
+ readonly execute: (request: GraphQLRequest) => Effect.Effect<{
146
+ readonly body: unknown
147
+ readonly status?: number | undefined
148
+ readonly headers?: Headers.Headers | undefined
149
+ }, TransportError>
150
+ readonly subscribe: (request: GraphQLRequest) => Stream.Stream<{
151
+ readonly body: unknown
152
+ readonly status?: number | undefined
153
+ readonly headers?: Headers.Headers | undefined
154
+ }, TransportError>
155
+ }>()("effect/graphql/GraphQLProtocol") {}
156
+
157
+ const graphqlResponseContentType = "application/graphql-response+json"
158
+ const jsonContentType = "application/json"
159
+ const eventStreamContentType = "text/event-stream"
160
+
161
+ const isObject = (u: unknown): u is Record<string, unknown> => typeof u === "object" && u !== null && !Array.isArray(u)
162
+
163
+ // A gateway's `{ "errors": "Throttled" }` is not a GraphQL response, so its
164
+ // status and Retry-After still apply.
165
+ const isGraphQLBody = (body: unknown): boolean =>
166
+ isObject(body) && ("data" in body || "errors" in body) &&
167
+ (!("errors" in body) || Array.isArray(body.errors)) &&
168
+ (!("data" in body) || body.data === null || isObject(body.data))
169
+
170
+ // The JSON body of a POST and the payload of a graphql-ws `subscribe`.
171
+ // `extensions` is dropped from the JSON when unset.
172
+ const requestBody = (request: GraphQLRequest) => ({
173
+ query: request.query,
174
+ operationName: request.operationName,
175
+ variables: request.variables,
176
+ extensions: request.extensions
177
+ })
178
+
179
+ const postRequest = (url: string, request: GraphQLRequest, accept: string) =>
180
+ HttpClientRequest.post(url).pipe(
181
+ HttpClientRequest.setHeaders(request.headers),
182
+ HttpClientRequest.setHeader("accept", accept),
183
+ HttpClientRequest.appendUrlParams(request.urlParams),
184
+ HttpClientRequest.bodyJsonUnsafe(requestBody(request))
185
+ )
186
+
187
+ const graphqlGetParams = new Set(["query", "operationName", "variables", "extensions"])
188
+
189
+ // Remove all protocol params from the configured URL and caller params before
190
+ // adding the request's values, including when optional fields are unset. Split
191
+ // the URL without resolving it so relative URLs remain supported.
192
+ const getRequest = (url: string, request: GraphQLRequest, accept: string) => {
193
+ const hashIndex = url.indexOf("#")
194
+ const hash = hashIndex === -1 ? "" : url.slice(hashIndex)
195
+ const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex)
196
+ const queryIndex = withoutHash.indexOf("?")
197
+ const baseUrl = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)
198
+ const urlParams = [
199
+ ...UrlParams.fromInput(new URLSearchParams(queryIndex === -1 ? "" : withoutHash.slice(queryIndex + 1))),
200
+ ...request.urlParams
201
+ ].filter(([key]) => !graphqlGetParams.has(key))
202
+ const params: Record<string, string> = { query: request.query, operationName: request.operationName }
203
+ if (request.variables !== undefined) params.variables = JSON.stringify(request.variables)
204
+ if (request.extensions !== undefined) params.extensions = JSON.stringify(request.extensions)
205
+ return HttpClientRequest.get(baseUrl + hash).pipe(
206
+ HttpClientRequest.setHeaders(request.headers),
207
+ HttpClientRequest.setHeader("accept", accept),
208
+ HttpClientRequest.appendUrlParams(urlParams),
209
+ HttpClientRequest.setUrlParams(params)
210
+ )
211
+ }
212
+
213
+ // Reads the clock when the error arrives, so an HTTP-date `Retry-After` is
214
+ // measured from when the response was received.
215
+ const fromHttpClientError =
216
+ (clock: Clock.Clock) =>
217
+ <A>(effect: Effect.Effect<A, TransportError | HttpClientError.HttpClientError>): Effect.Effect<A, TransportError> =>
218
+ Effect.catch(effect, (error) =>
219
+ Effect.fail(
220
+ HttpClientError.isHttpClientError(error)
221
+ ? TransportError.fromHttpClientError(error, { now: clock.currentTimeMillisUnsafe() })
222
+ : error
223
+ ))
224
+
225
+ // Media types are case-insensitive.
226
+ const contentTypeOf = (response: HttpClientResponse.HttpClientResponse): string =>
227
+ (response.headers["content-type"] ?? "").toLowerCase()
228
+
229
+ const readResponse = (
230
+ response: HttpClientResponse.HttpClientResponse
231
+ ): Effect.Effect<unknown, TransportError | HttpClientError.HttpClientError> => {
232
+ const contentType = contentTypeOf(response)
233
+ // The GraphQL-over-HTTP media type is authoritative, whatever the status.
234
+ if (contentType.includes(graphqlResponseContentType)) {
235
+ return response.json
236
+ }
237
+ // Legacy servers answer with application/json; only a body with the
238
+ // GraphQL keys counts as a GraphQL response.
239
+ if (contentType.includes(jsonContentType)) {
240
+ return Effect.flatMap(response.json, (body) =>
241
+ isGraphQLBody(body)
242
+ ? Effect.succeed(body)
243
+ : Effect.as(HttpClientResponse.filterStatusOk(response), body))
244
+ }
245
+ return Effect.flatMap(
246
+ HttpClientResponse.filterStatusOk(response),
247
+ () =>
248
+ Effect.fail(
249
+ new TransportError({
250
+ description: `Expected a GraphQL response, received ${
251
+ contentType === "" ? "no Content-Type" : `Content-Type ${contentType}`
252
+ }`,
253
+ status: response.status,
254
+ headers: response.headers
255
+ })
256
+ )
257
+ )
258
+ }
259
+
260
+ const readAnswer = (response: HttpClientResponse.HttpClientResponse) =>
261
+ Effect.map(readResponse(response), (body) => ({ body, status: response.status, headers: response.headers }))
262
+
263
+ const errorMessage = (cause: unknown): string => cause instanceof Error ? cause.message : String(cause)
264
+
265
+ // The graphql-sse distinct-mode events of a `text/event-stream` body. The
266
+ // stream ends on a `complete` event, with or without a `data:` line. A body
267
+ // that ends without one means the connection was lost, so it fails with a
268
+ // retryable `TransportError`. A `retry:` line is kept and reported as
269
+ // `retryAfter` if the body fails or ends early.
270
+ const sseEvents = (
271
+ response: HttpClientResponse.HttpClientResponse
272
+ ): Stream.Stream<{ readonly body: unknown; readonly headers: Headers.Headers }, TransportError> =>
273
+ Stream.suspend(() => {
274
+ const headers = response.headers
275
+ let retryAfter: Duration.Duration | undefined
276
+ let complete = false
277
+ let events: Array<Sse.Event> = []
278
+ const parser = Sse.makeParser((event) => {
279
+ if (event._tag === "Retry") {
280
+ retryAfter = event.duration
281
+ } else {
282
+ events.push(event)
283
+ }
284
+ }, { dispatchEmptyNamedEvents: true })
285
+ return response.stream.pipe(
286
+ Stream.decodeText(),
287
+ Stream.mapError((cause) =>
288
+ new TransportError({ description: `The graphql-sse stream failed: ${cause.message}`, retryAfter, cause })
289
+ ),
290
+ Stream.mapEffect((text) => {
291
+ const error = parser.feed(text)
292
+ if (error !== undefined) {
293
+ return Effect.fail(new TransportError({ description: error.message, cause: error }))
294
+ }
295
+ const batch = events
296
+ events = []
297
+ return Effect.succeed(batch)
298
+ }),
299
+ Stream.flattenIterable,
300
+ Stream.takeWhile((event) => {
301
+ if (event.event === "complete") complete = true
302
+ return !complete
303
+ }),
304
+ Stream.filter((event) => event.event === "next"),
305
+ Stream.mapEffect((event) =>
306
+ Effect.try({
307
+ try: () => ({ body: JSON.parse(event.data) as unknown, headers }),
308
+ catch: (cause) =>
309
+ new TransportError({ description: `Invalid graphql-sse next event: ${errorMessage(cause)}`, cause })
310
+ })
311
+ ),
312
+ Stream.concat(Stream.suspend(() =>
313
+ complete ? Stream.empty : Stream.fail(
314
+ new TransportError({ description: "The graphql-sse stream ended without a complete event", retryAfter })
315
+ )
316
+ ))
317
+ )
318
+ })
319
+
320
+ interface HttpOptions<R = never> {
321
+ readonly url: string
322
+ readonly queryMethod?: "GET" | "POST" | undefined
323
+ readonly subscriptions?: {
324
+ readonly webSocket: WebSocketOptions<R>
325
+ } | undefined
326
+ }
327
+
328
+ const makeHttpWith = <R>(options: HttpOptions<R>): Effect.Effect<
329
+ GraphQLProtocol["Service"],
330
+ never,
331
+ HttpClient.HttpClient | Socket.WebSocketConstructor | Scope.Scope | R
332
+ > => {
333
+ const http = Clock.clockWith((clock) =>
334
+ HttpClient.HttpClient.useSync((client) => {
335
+ const execute = (request: GraphQLRequest) =>
336
+ Effect.flatMap(Effect.service(internalGraphQL.ForcePost), (forcePost) => {
337
+ const toRequest = options.queryMethod === "GET" && request.kind === "query" && !forcePost
338
+ ? getRequest
339
+ : postRequest
340
+ return client.execute(
341
+ toRequest(options.url, request, `${graphqlResponseContentType}, ${jsonContentType}`)
342
+ ).pipe(
343
+ Effect.flatMap(readAnswer),
344
+ fromHttpClientError(clock)
345
+ )
346
+ })
347
+ const subscribe = (request: GraphQLRequest) =>
348
+ Stream.unwrap(Effect.suspend(() =>
349
+ client.execute(postRequest(options.url, request, eventStreamContentType)).pipe(
350
+ Effect.flatMap((response) => {
351
+ const contentType = contentTypeOf(response)
352
+ if (contentType.includes(eventStreamContentType) && response.status >= 200 && response.status < 300) {
353
+ return Effect.succeed(sseEvents(response))
354
+ }
355
+ // A server that rejects the subscription answers with a single
356
+ // GraphQL response, which the client reads like a query's.
357
+ return Effect.map(readAnswer(response), Stream.succeed)
358
+ }),
359
+ fromHttpClientError(clock)
360
+ )
361
+ ))
362
+ return GraphQLProtocol.of({ execute, subscribe })
363
+ })
364
+ )
365
+ const webSocket = options.subscriptions?.webSocket
366
+ return webSocket === undefined
367
+ ? http
368
+ : Effect.zipWith(
369
+ http,
370
+ makeWebSocket(webSocket),
371
+ (http, ws) => GraphQLProtocol.of({ execute: http.execute, subscribe: ws.subscribe })
372
+ )
373
+ }
374
+
375
+ /**
376
+ * Builds the HTTP transport as an effect, for composing transports by hand.
377
+ * Most code uses {@link layerHttp}.
378
+ *
379
+ * **Details**
380
+ *
381
+ * Takes the same options as {@link layerHttp}, including `queryMethod`.
382
+ * Without `subscriptions`, subscriptions use graphql-sse distinct mode on
383
+ * `url`. With `subscriptions.webSocket`, they go over graphql-ws instead, and
384
+ * the effect also builds the graphql-ws transport, so it needs a
385
+ * `Socket.WebSocketConstructor` and a `Scope`.
386
+ *
387
+ * @stability experimental
388
+ * @category constructors
389
+ * @since 4.0.0
390
+ */
391
+ export const makeHttp: {
392
+ /**
393
+ * Builds the HTTP transport as an effect, for composing transports by hand.
394
+ * Most code uses {@link layerHttp}.
395
+ *
396
+ * **Details**
397
+ *
398
+ * Takes the same options as {@link layerHttp}, including `queryMethod`.
399
+ * Without `subscriptions`, subscriptions use graphql-sse distinct mode on
400
+ * `url`. With `subscriptions.webSocket`, they go over graphql-ws instead, and
401
+ * the effect also builds the graphql-ws transport, so it needs a
402
+ * `Socket.WebSocketConstructor` and a `Scope`.
403
+ *
404
+ * @stability experimental
405
+ * @category constructors
406
+ * @since 4.0.0
407
+ */
408
+ <R = never>(
409
+ options: {
410
+ readonly url: string
411
+ readonly queryMethod?: "GET" | "POST" | undefined
412
+ readonly subscriptions: {
413
+ readonly webSocket: {
414
+ readonly url: string
415
+ readonly headers?: Headers.Input | undefined
416
+ readonly connectionParams?: Effect.Effect<Record<string, unknown> | undefined, unknown, R> | undefined
417
+ readonly connectionAckTimeout?: Duration.Input | undefined
418
+ readonly keepAlive?: Duration.Input | false | undefined
419
+ readonly idleTimeout?: Duration.Input | undefined
420
+ }
421
+ }
422
+ }
423
+ ): Effect.Effect<
424
+ GraphQLProtocol["Service"],
425
+ never,
426
+ HttpClient.HttpClient | Socket.WebSocketConstructor | Scope.Scope | R
427
+ >
428
+ /**
429
+ * Builds the HTTP transport as an effect, for composing transports by hand.
430
+ * Most code uses {@link layerHttp}.
431
+ *
432
+ * **Details**
433
+ *
434
+ * Takes the same options as {@link layerHttp}, including `queryMethod`.
435
+ * Without `subscriptions`, subscriptions use graphql-sse distinct mode on
436
+ * `url`. With `subscriptions.webSocket`, they go over graphql-ws instead, and
437
+ * the effect also builds the graphql-ws transport, so it needs a
438
+ * `Socket.WebSocketConstructor` and a `Scope`.
439
+ *
440
+ * @stability experimental
441
+ * @category constructors
442
+ * @since 4.0.0
443
+ */
444
+ (
445
+ options: {
446
+ readonly url: string
447
+ readonly queryMethod?: "GET" | "POST" | undefined
448
+ readonly subscriptions?: undefined
449
+ }
450
+ ): Effect.Effect<
451
+ GraphQLProtocol["Service"],
452
+ never,
453
+ HttpClient.HttpClient
454
+ >
455
+ } = makeHttpWith
456
+
457
+ /**
458
+ * The HTTP transport: queries and mutations are sent as `POST` with a JSON
459
+ * body to `url`, using the `HttpClient` in context. Subscriptions use
460
+ * graphql-sse distinct mode on the same URL, or graphql-ws when
461
+ * `subscriptions.webSocket` is set.
462
+ *
463
+ * **Details**
464
+ *
465
+ * With `queryMethod: "GET"` (default `"POST"`), queries are sent as `GET`
466
+ * with no body: `query` and `operationName` become URL params, plus
467
+ * `variables` and `extensions` as JSON when set. Mutations are always
468
+ * `POST`, and so are `GraphQLResolver.aliased` batches. A long query is not
469
+ * retried as `POST`.
470
+ *
471
+ * A request's `headers` and `urlParams`, from the call options or from
472
+ * middleware, are applied to every HTTP request. A query string already on
473
+ * `url` is kept. On `GET`, the request's `urlParams` come before the GraphQL
474
+ * params, which replace any caller param with the same name.
475
+ *
476
+ * Every response's HTTP `status` is passed to the client as
477
+ * `ExecutionResult.status`, and a call that fails with GraphQL `errors`
478
+ * reports it as `ResponseError.status`. Response headers are passed the
479
+ * same way, as `ExecutionResult.headers` and `ResponseError.headers`.
480
+ *
481
+ * A response is a GraphQL response when its `Content-Type` is
482
+ * `application/graphql-response+json`, whatever the status, or when it is
483
+ * `application/json` and the body has a list of `errors` or a `data` object
484
+ * (or `null`). Media types match case-insensitively. Anything else
485
+ * is a `TransportError` with the response `status` and `headers`, and a
486
+ * `Retry-After` header (whole seconds or an HTTP date, measured from when the
487
+ * response arrived) becomes `retryAfter`.
488
+ *
489
+ * A subscription is a `POST` with `Accept: text/event-stream`. Each `next`
490
+ * event is emitted with the response headers but no status, and the stream
491
+ * ends on a `complete` event, with or without a `data:` line. A body that
492
+ * fails, or ends without a `complete` event, is a retryable `TransportError`
493
+ * with a server `retry:` line as its `retryAfter`, so the client's
494
+ * `subscriptionRetry` resubscribes. A non-stream answer, such as a `400` with
495
+ * `errors`, is read like a query's response. Interrupting the stream aborts
496
+ * the request.
497
+ *
498
+ * With `subscriptions: { webSocket }`, subscriptions go over graphql-ws as in
499
+ * {@link layerWebSocket}, and the layer needs a `Socket.WebSocketConstructor`.
500
+ *
501
+ * **Example** (Building a client over HTTP)
502
+ *
503
+ * ```ts import.meta.vitest
504
+ * import { Effect, Layer, Schema } from "effect"
505
+ * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
506
+ * import { HttpClient, HttpClientResponse } from "effect/http"
507
+ *
508
+ * const Viewer = GraphQL.query("Viewer", {
509
+ * document: "query Viewer{viewer{login}}",
510
+ * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
511
+ * })
512
+ *
513
+ * // A stand-in for a real HttpClient layer such as FetchHttpClient.layer
514
+ * const FakeServer = Layer.succeed(
515
+ * HttpClient.HttpClient,
516
+ * HttpClient.make((request) =>
517
+ * Effect.succeed(HttpClientResponse.fromWeb(
518
+ * request,
519
+ * new Response(JSON.stringify({ data: { viewer: { login: "tim" } } }), {
520
+ * headers: { "content-type": "application/graphql-response+json" }
521
+ * })
522
+ * ))
523
+ * )
524
+ * )
525
+ *
526
+ * const program = Effect.gen(function*() {
527
+ * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer))
528
+ * return yield* client.Viewer()
529
+ * }).pipe(
530
+ * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
531
+ * Effect.provide(FakeServer)
532
+ * )
533
+ *
534
+ * await Effect.runPromise(program) // => { viewer: { login: "tim" } }
535
+ * ```
536
+ *
537
+ * **Example** (Queries over GET with a per-call URL param)
538
+ *
539
+ * ```ts import.meta.vitest
540
+ * import { Effect, Layer, Schema } from "effect"
541
+ * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
542
+ * import { HttpClient, HttpClientResponse } from "effect/http"
543
+ *
544
+ * const Viewer = GraphQL.query("Viewer", {
545
+ * document: "query Viewer{viewer{login}}",
546
+ * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
547
+ * })
548
+ *
549
+ * // A stand-in server that answers with the method and the `preview` param
550
+ * const FakeServer = Layer.succeed(
551
+ * HttpClient.HttpClient,
552
+ * HttpClient.make((request, url) =>
553
+ * Effect.succeed(HttpClientResponse.fromWeb(
554
+ * request,
555
+ * new Response(
556
+ * JSON.stringify({ data: { viewer: { login: `${request.method} ${url.searchParams.get("preview")}` } } }),
557
+ * { headers: { "content-type": "application/graphql-response+json" } }
558
+ * )
559
+ * ))
560
+ * )
561
+ * )
562
+ *
563
+ * const program = Effect.gen(function*() {
564
+ * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer))
565
+ * return yield* client.Viewer(undefined, { urlParams: { preview: "draft-token" } })
566
+ * }).pipe(
567
+ * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://cms.example.com/graphql", queryMethod: "GET" })),
568
+ * Effect.provide(FakeServer)
569
+ * )
570
+ *
571
+ * await Effect.runPromise(program) // => { viewer: { login: "GET draft-token" } }
572
+ * ```
573
+ *
574
+ * **Example** (Subscriptions over graphql-ws, everything else over POST)
575
+ *
576
+ * ```ts import.meta.vitest
577
+ * import { Effect } from "effect"
578
+ * import { GraphQLProtocol } from "effect/graphql"
579
+ *
580
+ * const token = Effect.succeed("secret")
581
+ *
582
+ * // Provide an HttpClient and a Socket.WebSocketConstructor, such as
583
+ * // FetchHttpClient.layer and NodeSocket.layerWebSocketConstructor
584
+ * export const Protocol = GraphQLProtocol.layerHttp({
585
+ * url: "https://example.com/graphql",
586
+ * subscriptions: {
587
+ * webSocket: {
588
+ * url: "wss://example.com/graphql",
589
+ * connectionParams: Effect.map(token, (token) => ({ authorization: `Bearer ${token}` }))
590
+ * }
591
+ * }
592
+ * })
593
+ * ```
594
+ *
595
+ * @stability experimental
596
+ * @category layers
597
+ * @since 4.0.0
598
+ */
599
+ export const layerHttp: {
600
+ /**
601
+ * The HTTP transport: queries and mutations are sent as `POST` with a JSON
602
+ * body to `url`, using the `HttpClient` in context. Subscriptions use
603
+ * graphql-sse distinct mode on the same URL, or graphql-ws when
604
+ * `subscriptions.webSocket` is set.
605
+ *
606
+ * **Details**
607
+ *
608
+ * With `queryMethod: "GET"` (default `"POST"`), queries are sent as `GET`
609
+ * with no body: `query` and `operationName` become URL params, plus
610
+ * `variables` and `extensions` as JSON when set. Mutations are always
611
+ * `POST`, and so are `GraphQLResolver.aliased` batches. A long query is not
612
+ * retried as `POST`.
613
+ *
614
+ * A request's `headers` and `urlParams`, from the call options or from
615
+ * middleware, are applied to every HTTP request. A query string already on
616
+ * `url` is kept. On `GET`, the request's `urlParams` come before the GraphQL
617
+ * params, which replace any caller param with the same name.
618
+ *
619
+ * Every response's HTTP `status` is passed to the client as
620
+ * `ExecutionResult.status`, and a call that fails with GraphQL `errors`
621
+ * reports it as `ResponseError.status`. Response headers are passed the
622
+ * same way, as `ExecutionResult.headers` and `ResponseError.headers`.
623
+ *
624
+ * A response is a GraphQL response when its `Content-Type` is
625
+ * `application/graphql-response+json`, whatever the status, or when it is
626
+ * `application/json` and the body has a list of `errors` or a `data` object
627
+ * (or `null`). Media types match case-insensitively. Anything else
628
+ * is a `TransportError` with the response `status` and `headers`, and a
629
+ * `Retry-After` header (whole seconds or an HTTP date, measured from when the
630
+ * response arrived) becomes `retryAfter`.
631
+ *
632
+ * A subscription is a `POST` with `Accept: text/event-stream`. Each `next`
633
+ * event is emitted with the response headers but no status, and the stream
634
+ * ends on a `complete` event, with or without a `data:` line. A body that
635
+ * fails, or ends without a `complete` event, is a retryable `TransportError`
636
+ * with a server `retry:` line as its `retryAfter`, so the client's
637
+ * `subscriptionRetry` resubscribes. A non-stream answer, such as a `400` with
638
+ * `errors`, is read like a query's response. Interrupting the stream aborts
639
+ * the request.
640
+ *
641
+ * With `subscriptions: { webSocket }`, subscriptions go over graphql-ws as in
642
+ * {@link layerWebSocket}, and the layer needs a `Socket.WebSocketConstructor`.
643
+ *
644
+ * **Example** (Building a client over HTTP)
645
+ *
646
+ * ```ts import.meta.vitest
647
+ * import { Effect, Layer, Schema } from "effect"
648
+ * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
649
+ * import { HttpClient, HttpClientResponse } from "effect/http"
650
+ *
651
+ * const Viewer = GraphQL.query("Viewer", {
652
+ * document: "query Viewer{viewer{login}}",
653
+ * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
654
+ * })
655
+ *
656
+ * // A stand-in for a real HttpClient layer such as FetchHttpClient.layer
657
+ * const FakeServer = Layer.succeed(
658
+ * HttpClient.HttpClient,
659
+ * HttpClient.make((request) =>
660
+ * Effect.succeed(HttpClientResponse.fromWeb(
661
+ * request,
662
+ * new Response(JSON.stringify({ data: { viewer: { login: "tim" } } }), {
663
+ * headers: { "content-type": "application/graphql-response+json" }
664
+ * })
665
+ * ))
666
+ * )
667
+ * )
668
+ *
669
+ * const program = Effect.gen(function*() {
670
+ * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer))
671
+ * return yield* client.Viewer()
672
+ * }).pipe(
673
+ * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
674
+ * Effect.provide(FakeServer)
675
+ * )
676
+ *
677
+ * await Effect.runPromise(program) // => { viewer: { login: "tim" } }
678
+ * ```
679
+ *
680
+ * **Example** (Queries over GET with a per-call URL param)
681
+ *
682
+ * ```ts import.meta.vitest
683
+ * import { Effect, Layer, Schema } from "effect"
684
+ * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
685
+ * import { HttpClient, HttpClientResponse } from "effect/http"
686
+ *
687
+ * const Viewer = GraphQL.query("Viewer", {
688
+ * document: "query Viewer{viewer{login}}",
689
+ * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
690
+ * })
691
+ *
692
+ * // A stand-in server that answers with the method and the `preview` param
693
+ * const FakeServer = Layer.succeed(
694
+ * HttpClient.HttpClient,
695
+ * HttpClient.make((request, url) =>
696
+ * Effect.succeed(HttpClientResponse.fromWeb(
697
+ * request,
698
+ * new Response(
699
+ * JSON.stringify({ data: { viewer: { login: `${request.method} ${url.searchParams.get("preview")}` } } }),
700
+ * { headers: { "content-type": "application/graphql-response+json" } }
701
+ * )
702
+ * ))
703
+ * )
704
+ * )
705
+ *
706
+ * const program = Effect.gen(function*() {
707
+ * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer))
708
+ * return yield* client.Viewer(undefined, { urlParams: { preview: "draft-token" } })
709
+ * }).pipe(
710
+ * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://cms.example.com/graphql", queryMethod: "GET" })),
711
+ * Effect.provide(FakeServer)
712
+ * )
713
+ *
714
+ * await Effect.runPromise(program) // => { viewer: { login: "GET draft-token" } }
715
+ * ```
716
+ *
717
+ * **Example** (Subscriptions over graphql-ws, everything else over POST)
718
+ *
719
+ * ```ts import.meta.vitest
720
+ * import { Effect } from "effect"
721
+ * import { GraphQLProtocol } from "effect/graphql"
722
+ *
723
+ * const token = Effect.succeed("secret")
724
+ *
725
+ * // Provide an HttpClient and a Socket.WebSocketConstructor, such as
726
+ * // FetchHttpClient.layer and NodeSocket.layerWebSocketConstructor
727
+ * export const Protocol = GraphQLProtocol.layerHttp({
728
+ * url: "https://example.com/graphql",
729
+ * subscriptions: {
730
+ * webSocket: {
731
+ * url: "wss://example.com/graphql",
732
+ * connectionParams: Effect.map(token, (token) => ({ authorization: `Bearer ${token}` }))
733
+ * }
734
+ * }
735
+ * })
736
+ * ```
737
+ *
738
+ * @stability experimental
739
+ * @category layers
740
+ * @since 4.0.0
741
+ */
742
+ <R = never>(
743
+ options: {
744
+ readonly url: string
745
+ readonly queryMethod?: "GET" | "POST" | undefined
746
+ readonly subscriptions: {
747
+ readonly webSocket: {
748
+ readonly url: string
749
+ readonly headers?: Headers.Input | undefined
750
+ readonly connectionParams?: Effect.Effect<Record<string, unknown> | undefined, unknown, R> | undefined
751
+ readonly connectionAckTimeout?: Duration.Input | undefined
752
+ readonly keepAlive?: Duration.Input | false | undefined
753
+ readonly idleTimeout?: Duration.Input | undefined
754
+ }
755
+ }
756
+ }
757
+ ): Layer.Layer<GraphQLProtocol, never, HttpClient.HttpClient | Socket.WebSocketConstructor | R>
758
+ /**
759
+ * The HTTP transport: queries and mutations are sent as `POST` with a JSON
760
+ * body to `url`, using the `HttpClient` in context. Subscriptions use
761
+ * graphql-sse distinct mode on the same URL, or graphql-ws when
762
+ * `subscriptions.webSocket` is set.
763
+ *
764
+ * **Details**
765
+ *
766
+ * With `queryMethod: "GET"` (default `"POST"`), queries are sent as `GET`
767
+ * with no body: `query` and `operationName` become URL params, plus
768
+ * `variables` and `extensions` as JSON when set. Mutations are always
769
+ * `POST`, and so are `GraphQLResolver.aliased` batches. A long query is not
770
+ * retried as `POST`.
771
+ *
772
+ * A request's `headers` and `urlParams`, from the call options or from
773
+ * middleware, are applied to every HTTP request. A query string already on
774
+ * `url` is kept. On `GET`, the request's `urlParams` come before the GraphQL
775
+ * params, which replace any caller param with the same name.
776
+ *
777
+ * Every response's HTTP `status` is passed to the client as
778
+ * `ExecutionResult.status`, and a call that fails with GraphQL `errors`
779
+ * reports it as `ResponseError.status`. Response headers are passed the
780
+ * same way, as `ExecutionResult.headers` and `ResponseError.headers`.
781
+ *
782
+ * A response is a GraphQL response when its `Content-Type` is
783
+ * `application/graphql-response+json`, whatever the status, or when it is
784
+ * `application/json` and the body has a list of `errors` or a `data` object
785
+ * (or `null`). Media types match case-insensitively. Anything else
786
+ * is a `TransportError` with the response `status` and `headers`, and a
787
+ * `Retry-After` header (whole seconds or an HTTP date, measured from when the
788
+ * response arrived) becomes `retryAfter`.
789
+ *
790
+ * A subscription is a `POST` with `Accept: text/event-stream`. Each `next`
791
+ * event is emitted with the response headers but no status, and the stream
792
+ * ends on a `complete` event, with or without a `data:` line. A body that
793
+ * fails, or ends without a `complete` event, is a retryable `TransportError`
794
+ * with a server `retry:` line as its `retryAfter`, so the client's
795
+ * `subscriptionRetry` resubscribes. A non-stream answer, such as a `400` with
796
+ * `errors`, is read like a query's response. Interrupting the stream aborts
797
+ * the request.
798
+ *
799
+ * With `subscriptions: { webSocket }`, subscriptions go over graphql-ws as in
800
+ * {@link layerWebSocket}, and the layer needs a `Socket.WebSocketConstructor`.
801
+ *
802
+ * **Example** (Building a client over HTTP)
803
+ *
804
+ * ```ts import.meta.vitest
805
+ * import { Effect, Layer, Schema } from "effect"
806
+ * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
807
+ * import { HttpClient, HttpClientResponse } from "effect/http"
808
+ *
809
+ * const Viewer = GraphQL.query("Viewer", {
810
+ * document: "query Viewer{viewer{login}}",
811
+ * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
812
+ * })
813
+ *
814
+ * // A stand-in for a real HttpClient layer such as FetchHttpClient.layer
815
+ * const FakeServer = Layer.succeed(
816
+ * HttpClient.HttpClient,
817
+ * HttpClient.make((request) =>
818
+ * Effect.succeed(HttpClientResponse.fromWeb(
819
+ * request,
820
+ * new Response(JSON.stringify({ data: { viewer: { login: "tim" } } }), {
821
+ * headers: { "content-type": "application/graphql-response+json" }
822
+ * })
823
+ * ))
824
+ * )
825
+ * )
826
+ *
827
+ * const program = Effect.gen(function*() {
828
+ * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer))
829
+ * return yield* client.Viewer()
830
+ * }).pipe(
831
+ * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
832
+ * Effect.provide(FakeServer)
833
+ * )
834
+ *
835
+ * await Effect.runPromise(program) // => { viewer: { login: "tim" } }
836
+ * ```
837
+ *
838
+ * **Example** (Queries over GET with a per-call URL param)
839
+ *
840
+ * ```ts import.meta.vitest
841
+ * import { Effect, Layer, Schema } from "effect"
842
+ * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql"
843
+ * import { HttpClient, HttpClientResponse } from "effect/http"
844
+ *
845
+ * const Viewer = GraphQL.query("Viewer", {
846
+ * document: "query Viewer{viewer{login}}",
847
+ * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) })
848
+ * })
849
+ *
850
+ * // A stand-in server that answers with the method and the `preview` param
851
+ * const FakeServer = Layer.succeed(
852
+ * HttpClient.HttpClient,
853
+ * HttpClient.make((request, url) =>
854
+ * Effect.succeed(HttpClientResponse.fromWeb(
855
+ * request,
856
+ * new Response(
857
+ * JSON.stringify({ data: { viewer: { login: `${request.method} ${url.searchParams.get("preview")}` } } }),
858
+ * { headers: { "content-type": "application/graphql-response+json" } }
859
+ * )
860
+ * ))
861
+ * )
862
+ * )
863
+ *
864
+ * const program = Effect.gen(function*() {
865
+ * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer))
866
+ * return yield* client.Viewer(undefined, { urlParams: { preview: "draft-token" } })
867
+ * }).pipe(
868
+ * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://cms.example.com/graphql", queryMethod: "GET" })),
869
+ * Effect.provide(FakeServer)
870
+ * )
871
+ *
872
+ * await Effect.runPromise(program) // => { viewer: { login: "GET draft-token" } }
873
+ * ```
874
+ *
875
+ * **Example** (Subscriptions over graphql-ws, everything else over POST)
876
+ *
877
+ * ```ts import.meta.vitest
878
+ * import { Effect } from "effect"
879
+ * import { GraphQLProtocol } from "effect/graphql"
880
+ *
881
+ * const token = Effect.succeed("secret")
882
+ *
883
+ * // Provide an HttpClient and a Socket.WebSocketConstructor, such as
884
+ * // FetchHttpClient.layer and NodeSocket.layerWebSocketConstructor
885
+ * export const Protocol = GraphQLProtocol.layerHttp({
886
+ * url: "https://example.com/graphql",
887
+ * subscriptions: {
888
+ * webSocket: {
889
+ * url: "wss://example.com/graphql",
890
+ * connectionParams: Effect.map(token, (token) => ({ authorization: `Bearer ${token}` }))
891
+ * }
892
+ * }
893
+ * })
894
+ * ```
895
+ *
896
+ * @stability experimental
897
+ * @category layers
898
+ * @since 4.0.0
899
+ */
900
+ (
901
+ options: {
902
+ readonly url: string
903
+ readonly queryMethod?: "GET" | "POST" | undefined
904
+ readonly subscriptions?: undefined
905
+ }
906
+ ): Layer.Layer<
907
+ GraphQLProtocol,
908
+ never,
909
+ HttpClient.HttpClient
910
+ >
911
+ } = <R>(
912
+ options: HttpOptions<R>
913
+ ): Layer.Layer<GraphQLProtocol, never, HttpClient.HttpClient | Socket.WebSocketConstructor | R> =>
914
+ Layer.effect(GraphQLProtocol, makeHttpWith(options))
915
+
916
+ interface WebSocketOptions<R = never> {
917
+ readonly url: string
918
+ readonly headers?: Headers.Input | undefined
919
+ readonly connectionParams?: Effect.Effect<Record<string, unknown> | undefined, unknown, R> | undefined
920
+ readonly connectionAckTimeout?: Duration.Input | undefined
921
+ readonly keepAlive?: Duration.Input | false | undefined
922
+ readonly idleTimeout?: Duration.Input | undefined
923
+ }
924
+
925
+ interface WsMessage {
926
+ readonly type: string
927
+ readonly id?: string | undefined
928
+ readonly payload?: unknown
929
+ }
930
+
931
+ // One WebSocket connection. `error` is set once it is lost, and the operations
932
+ // attached to it fail with it.
933
+ interface Connection {
934
+ readonly ack: Deferred.Deferred<void, TransportError>
935
+ error: TransportError | undefined
936
+ receivedFrame: boolean
937
+ fiber: Fiber.Fiber<void> | undefined
938
+ }
939
+
940
+ interface Operation {
941
+ readonly queue: Queue.Queue<{ readonly body: unknown }, TransportError | Cause.Done>
942
+ readonly connection: Connection
943
+ }
944
+
945
+ const textDecoder = new TextDecoder()
946
+
947
+ // As in graphql-ws's `validateMessage`, `error` carries at least one entry with
948
+ // a `message`.
949
+ const isFormattedErrors = (u: unknown): boolean =>
950
+ Array.isArray(u) && u.length > 0 && u.every((error) => isObject(error) && "message" in error)
951
+
952
+ const connectionError = (cause: Cause.Cause<unknown>): TransportError => {
953
+ const found = Cause.findError(cause)
954
+ if (Result.isFailure(found)) {
955
+ return new TransportError({ description: "The graphql-ws connection was closed" })
956
+ }
957
+ const error = found.success
958
+ if (error instanceof TransportError) {
959
+ return error
960
+ }
961
+ if (Socket.isSocketError(error) && error.reason._tag === "SocketCloseError") {
962
+ return new TransportError({
963
+ description: `The graphql-ws connection closed with ${error.reason.message}`,
964
+ closeCode: error.reason.code,
965
+ cause: error
966
+ })
967
+ }
968
+ return new TransportError({ description: `The graphql-ws connection failed: ${errorMessage(error)}`, cause: error })
969
+ }
970
+
971
+ /**
972
+ * Builds the graphql-ws transport as an effect, for composing transports by
973
+ * hand. Most code uses {@link layerWebSocket}, which describes the options.
974
+ *
975
+ * @stability experimental
976
+ * @category constructors
977
+ * @since 4.0.0
978
+ */
979
+ export const makeWebSocket = <R = never>(
980
+ options: {
981
+ readonly url: string
982
+ readonly headers?: Headers.Input | undefined
983
+ readonly connectionParams?: Effect.Effect<Record<string, unknown> | undefined, unknown, R> | undefined
984
+ readonly connectionAckTimeout?: Duration.Input | undefined
985
+ readonly keepAlive?: Duration.Input | false | undefined
986
+ readonly idleTimeout?: Duration.Input | undefined
987
+ }
988
+ ): Effect.Effect<GraphQLProtocol["Service"], never, Socket.WebSocketConstructor | Scope.Scope | R> =>
989
+ Effect.gen(function*() {
990
+ const socket = yield* Socket.makeWebSocket(options.url, {
991
+ protocols: "graphql-transport-ws",
992
+ headers: options.headers === undefined ? undefined : Headers.fromInput(options.headers)
993
+ })
994
+ const writer = yield* socket.writer
995
+ const scope = yield* Effect.scope
996
+ const context = yield* Effect.context<R>()
997
+ const connectionParams = options.connectionParams === undefined
998
+ ? Effect.succeed(undefined)
999
+ : options.connectionParams.pipe(
1000
+ Effect.provideContext(context),
1001
+ Effect.mapError((cause) =>
1002
+ new TransportError({ description: `graphql-ws connectionParams failed: ${errorMessage(cause)}`, cause })
1003
+ )
1004
+ )
1005
+ const ackTimeout = options.connectionAckTimeout ?? Duration.seconds(10)
1006
+ const keepAlive = options.keepAlive ?? Duration.seconds(10)
1007
+ const idleTimeout = Duration.fromInputUnsafe(options.idleTimeout ?? 0)
1008
+
1009
+ const operations = new Map<string, Operation>()
1010
+ let connection: Connection | undefined
1011
+ let nextId = 0
1012
+ let active = 0
1013
+ let idleGeneration = 0
1014
+
1015
+ const send = (message: WsMessage) =>
1016
+ writer.write(JSON.stringify(message)).pipe(
1017
+ Effect.mapError((cause) => connectionError(Cause.fail(cause)))
1018
+ )
1019
+
1020
+ const lose = (conn: Connection, error: TransportError) => {
1021
+ if (conn.error !== undefined) return
1022
+ conn.error = error
1023
+ if (connection === conn) connection = undefined
1024
+ Deferred.doneUnsafe(conn.ack, Effect.fail(error))
1025
+ for (const [id, operation] of operations) {
1026
+ if (operation.connection !== conn) continue
1027
+ operations.delete(id)
1028
+ Queue.failCauseUnsafe(operation.queue, Cause.fail(error))
1029
+ }
1030
+ }
1031
+
1032
+ // An invalid server message closes the connection with 4400, which is
1033
+ // fatal for every operation on it.
1034
+ const invalid = (reason: string, cause?: unknown) =>
1035
+ Effect.andThen(
1036
+ Effect.ignore(writer.write(new Socket.CloseEvent(4400, "Invalid message"))),
1037
+ Effect.fail(
1038
+ new TransportError({ description: `Invalid graphql-ws message: ${reason}`, closeCode: 4400, cause })
1039
+ )
1040
+ )
1041
+
1042
+ const handle = (conn: Connection, frame: string | Uint8Array): Effect.Effect<void, TransportError> => {
1043
+ // Any frame answers a keep-alive ping; an invalid one drops the connection anyway.
1044
+ conn.receivedFrame = true
1045
+ let message: WsMessage
1046
+ try {
1047
+ message = JSON.parse(typeof frame === "string" ? frame : textDecoder.decode(frame))
1048
+ } catch (cause) {
1049
+ return invalid(errorMessage(cause), cause)
1050
+ }
1051
+ if (!isObject(message)) {
1052
+ return invalid("expected an object")
1053
+ }
1054
+ switch (message.type) {
1055
+ case "connection_ack":
1056
+ case "ping":
1057
+ case "pong": {
1058
+ if (message.payload != null && !isObject(message.payload)) {
1059
+ return invalid(`${message.type} with a payload that is not an object`)
1060
+ }
1061
+ if (message.type === "connection_ack") {
1062
+ Deferred.doneUnsafe(conn.ack, Effect.void)
1063
+ }
1064
+ return message.type === "ping" ? send({ type: "pong" }) : Effect.void
1065
+ }
1066
+ case "next":
1067
+ case "error":
1068
+ case "complete": {
1069
+ const id = message.id
1070
+ if (typeof id !== "string" || id === "") {
1071
+ return invalid(`${message.type} without an id`)
1072
+ }
1073
+ if (message.type === "next" && !isObject(message.payload)) {
1074
+ return invalid("next with a payload that is not an object")
1075
+ }
1076
+ if (message.type === "error" && !isFormattedErrors(message.payload)) {
1077
+ return invalid("error with a payload that is not a list of GraphQL errors")
1078
+ }
1079
+ const operation = operations.get(id)
1080
+ // Unknown ids, including operations this client already completed, are dropped.
1081
+ if (operation === undefined || operation.connection !== conn) return Effect.void
1082
+ if (message.type === "next") {
1083
+ Queue.offerUnsafe(operation.queue, { body: message.payload })
1084
+ return Effect.void
1085
+ }
1086
+ operations.delete(id)
1087
+ if (message.type === "error") {
1088
+ // The server rejected the operation; the client fails it with a ResponseError.
1089
+ Queue.offerUnsafe(operation.queue, { body: { errors: message.payload } })
1090
+ }
1091
+ Queue.endUnsafe(operation.queue)
1092
+ return Effect.void
1093
+ }
1094
+ default: {
1095
+ return invalid(`unknown type ${JSON.stringify(message.type)}`)
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ const pinger = Effect.fnUntraced(function*(conn: Connection) {
1101
+ if (keepAlive === false) return yield* Effect.never
1102
+ while (true) {
1103
+ yield* Effect.sleep(keepAlive)
1104
+ if (!conn.receivedFrame) {
1105
+ return yield* Effect.fail(
1106
+ new TransportError({ description: "The graphql-ws server did not answer the keep-alive ping" })
1107
+ )
1108
+ }
1109
+ conn.receivedFrame = false
1110
+ yield* send({ type: "ping" })
1111
+ }
1112
+ })
1113
+
1114
+ const run = Effect.fnUntraced(
1115
+ function*(conn: Connection) {
1116
+ const { pull } = yield* socket.reader
1117
+ const init = Effect.flatMap(connectionParams, (payload) => send({ type: "connection_init", payload }))
1118
+ const read = Effect.forever(
1119
+ Effect.flatMap(pull, (frames) => Effect.forEach(frames, (frame) => handle(conn, frame), { discard: true }))
1120
+ )
1121
+ const acknowledged = Deferred.await(conn.ack).pipe(
1122
+ Effect.timeoutOrElse({
1123
+ duration: ackTimeout,
1124
+ orElse: () =>
1125
+ Effect.fail(new TransportError({ description: "The graphql-ws server did not send connection_ack" }))
1126
+ })
1127
+ )
1128
+ // Reading starts with `connectionParams`, so a close while it runs still
1129
+ // fails the waiting operations. The ack timeout starts once
1130
+ // `connection_init` is sent.
1131
+ yield* Effect.all([read, init.pipe(Effect.andThen(acknowledged), Effect.andThen(pinger(conn)))], {
1132
+ concurrency: 2,
1133
+ discard: true
1134
+ })
1135
+ },
1136
+ // Runs before the socket closes, so no write waits on a dead socket.
1137
+ (effect, conn) => Effect.onError(effect, (cause) => Effect.sync(() => lose(conn, connectionError(cause)))),
1138
+ (effect) => Effect.ignore(Effect.scoped(effect))
1139
+ )
1140
+
1141
+ const close = (conn: Connection) => {
1142
+ lose(conn, new TransportError({ description: "The graphql-ws connection was closed" }))
1143
+ return conn.fiber === undefined ? Effect.void : Fiber.interrupt(conn.fiber)
1144
+ }
1145
+
1146
+ const acquireConnection = Effect.suspend(() => {
1147
+ if (connection !== undefined) return Effect.succeed(connection)
1148
+ const conn: Connection = {
1149
+ ack: Deferred.makeUnsafe(),
1150
+ error: undefined,
1151
+ receivedFrame: true,
1152
+ fiber: undefined
1153
+ }
1154
+ connection = conn
1155
+ return Effect.forkIn(run(conn), scope).pipe(
1156
+ Effect.map((fiber) => {
1157
+ conn.fiber = fiber
1158
+ return conn
1159
+ })
1160
+ )
1161
+ })
1162
+
1163
+ const register = Effect.suspend(() => {
1164
+ // A connection forked into a closed scope is interrupted before it
1165
+ // starts, so new operations fail instead of waiting for an ack.
1166
+ if (scope.state._tag === "Closed") {
1167
+ return Effect.fail(
1168
+ new TransportError({ description: "The graphql-ws transport's scope is closed", fatal: true })
1169
+ )
1170
+ }
1171
+ active++
1172
+ idleGeneration++
1173
+ return Effect.map(acquireConnection, (conn) => [String(++nextId), conn] as const)
1174
+ })
1175
+
1176
+ const closeIfIdle = Effect.suspend(() => {
1177
+ if (--active > 0 || connection === undefined) return Effect.void
1178
+ const conn = connection
1179
+ if (Duration.isZero(idleTimeout)) return close(conn)
1180
+ const generation = ++idleGeneration
1181
+ const closeIfStillIdle = Effect.suspend(() =>
1182
+ generation === idleGeneration && connection === conn ? close(conn) : Effect.void
1183
+ )
1184
+ return Effect.asVoid(Effect.forkIn(Effect.delay(closeIfStillIdle, idleTimeout), scope))
1185
+ })
1186
+
1187
+ const release = (id: string, conn: Connection) =>
1188
+ Effect.suspend(() =>
1189
+ Effect.andThen(
1190
+ // Only a subscribed operation the server has not completed is still in `operations`.
1191
+ operations.delete(id) && conn.error === undefined
1192
+ ? Effect.ignore(send({ id, type: "complete" }))
1193
+ : Effect.void,
1194
+ closeIfIdle
1195
+ )
1196
+ )
1197
+
1198
+ const operation = (request: GraphQLRequest): Stream.Stream<{ readonly body: unknown }, TransportError> =>
1199
+ Stream.unwrap(Effect.gen(function*() {
1200
+ const [id, conn] = yield* Effect.acquireRelease(register, ([id, conn]) => release(id, conn))
1201
+ yield* Deferred.await(conn.ack)
1202
+ if (conn.error !== undefined) {
1203
+ return yield* Effect.fail(conn.error)
1204
+ }
1205
+ const queue = yield* Queue.unbounded<{ readonly body: unknown }, TransportError | Cause.Done>()
1206
+ operations.set(id, { queue, connection: conn })
1207
+ // Per-call `request.headers` and `request.urlParams` are ignored:
1208
+ // graphql-ws has no per-operation headers or URL.
1209
+ yield* send({ id, type: "subscribe", payload: requestBody(request) })
1210
+ return Stream.fromQueue(queue)
1211
+ }))
1212
+
1213
+ const execute = (request: GraphQLRequest): Effect.Effect<{ readonly body: unknown }, TransportError> =>
1214
+ Effect.flatMap(Stream.runHead(operation(request)), (result) =>
1215
+ result._tag === "Some"
1216
+ ? Effect.succeed(result.value)
1217
+ : Effect.fail(new TransportError({ description: "The graphql-ws operation completed without a result" })))
1218
+
1219
+ return GraphQLProtocol.of({ execute, subscribe: operation })
1220
+ })
1221
+
1222
+ /**
1223
+ * The graphql-ws transport: every operation goes over one WebSocket using the
1224
+ * `graphql-transport-ws` subprotocol, with the `Socket.WebSocketConstructor`
1225
+ * in context.
1226
+ *
1227
+ * **Details**
1228
+ *
1229
+ * The socket connects lazily and multiplexes operations by id. It closes after
1230
+ * `idleTimeout` (default 0) with no active operations, or with code `1000`
1231
+ * when the layer's scope closes.
1232
+ *
1233
+ * - Operations wait for `connection_ack`. `connectionAckTimeout` (default
1234
+ * 10 seconds) starts after sending `connection_init`.
1235
+ * - `connectionParams` runs on every connection, allowing token refresh.
1236
+ * Failure or a socket close during evaluation is a `TransportError`.
1237
+ * - Handshake `headers` accept `Headers.Input` and are normalized before
1238
+ * reaching the socket. They require a Node or Bun WebSocket constructor;
1239
+ * browsers cannot set them. Per-operation `headers` and `urlParams` are ignored.
1240
+ * - Results carry no HTTP `status`.
1241
+ * - `keepAlive` (default 10 seconds, or `false` to disable) sends `ping`.
1242
+ * No server frame in the following interval causes a retryable failure.
1243
+ * Server `ping` receives `pong`.
1244
+ * - Lost connections fail every active operation with a `TransportError`
1245
+ * carrying the close code. See `GraphQLClientError.isFatalCloseCode` for
1246
+ * non-retryable codes. The transport never replays operations; the client
1247
+ * retries subscriptions and the next operation reconnects.
1248
+ * - Once the layer's scope has closed, every new operation fails at once
1249
+ * with a non-retryable `TransportError` marked `fatal`.
1250
+ * - Messages follow graphql-ws validation. Invalid messages close the socket
1251
+ * with fatal code `4400`; valid messages for unknown ids are ignored.
1252
+ * - Server `error` messages become `ResponseError` values. Interrupting a
1253
+ * subscription sends `complete`.
1254
+ *
1255
+ * **Example** (A client over graphql-ws)
1256
+ *
1257
+ * ```ts import.meta.vitest
1258
+ * import { Effect, Layer } from "effect"
1259
+ * import { GraphQLProtocol } from "effect/graphql"
1260
+ * import type { Socket } from "effect/socket"
1261
+ *
1262
+ * // Provide a Socket.WebSocketConstructor, such as
1263
+ * // Socket.layerWebSocketConstructorGlobal in a browser
1264
+ * export const Protocol: Layer.Layer<GraphQLProtocol.GraphQLProtocol, never, Socket.WebSocketConstructor> =
1265
+ * GraphQLProtocol.layerWebSocket({
1266
+ * url: "wss://example.com/graphql",
1267
+ * connectionParams: Effect.succeed({ authorization: "Bearer secret" }),
1268
+ * idleTimeout: "30 seconds"
1269
+ * })
1270
+ * ```
1271
+ *
1272
+ * @stability experimental
1273
+ * @category layers
1274
+ * @since 4.0.0
1275
+ */
1276
+ export const layerWebSocket = <R = never>(
1277
+ options: {
1278
+ readonly url: string
1279
+ readonly headers?: Headers.Input | undefined
1280
+ readonly connectionParams?: Effect.Effect<Record<string, unknown> | undefined, unknown, R> | undefined
1281
+ readonly connectionAckTimeout?: Duration.Input | undefined
1282
+ readonly keepAlive?: Duration.Input | false | undefined
1283
+ readonly idleTimeout?: Duration.Input | undefined
1284
+ }
1285
+ ): Layer.Layer<GraphQLProtocol, never, Socket.WebSocketConstructor | R> =>
1286
+ Layer.effect(GraphQLProtocol, makeWebSocket(options))