effect 4.0.3 → 4.0.5

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