@effect/platform-node 0.11.3 → 0.11.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.
@@ -0,0 +1,273 @@
1
+ import * as Context from "@effect/data/Context"
2
+ import { pipe } from "@effect/data/Function"
3
+ import * as Effect from "@effect/io/Effect"
4
+ import * as Layer from "@effect/io/Layer"
5
+ import type * as Scope from "@effect/io/Scope"
6
+ import type * as NodeClient from "@effect/platform-node/Http/NodeClient"
7
+ import * as NodeSink from "@effect/platform-node/Sink"
8
+ import * as NodeStream from "@effect/platform-node/Stream"
9
+ import type * as Body from "@effect/platform/Http/Body"
10
+ import * as Client from "@effect/platform/Http/Client"
11
+ import * as Error from "@effect/platform/Http/ClientError"
12
+ import type * as ClientRequest from "@effect/platform/Http/ClientRequest"
13
+ import * as ClientResponse from "@effect/platform/Http/ClientResponse"
14
+ import * as Headers from "@effect/platform/Http/Headers"
15
+ import * as IncomingMessage from "@effect/platform/Http/IncomingMessage"
16
+ import * as UrlParams from "@effect/platform/Http/UrlParams"
17
+ import * as Stream from "@effect/stream/Stream"
18
+ import * as Http from "node:http"
19
+ import * as Https from "node:https"
20
+ import { Readable } from "node:stream"
21
+ import { pipeline } from "node:stream/promises"
22
+
23
+ /** @internal */
24
+ export const HttpAgentTypeId: NodeClient.HttpAgentTypeId = Symbol.for(
25
+ "@effect/platform-node/Http/NodeClient/HttpAgent"
26
+ ) as NodeClient.HttpAgentTypeId
27
+
28
+ /** @internal */
29
+ export const HttpAgent = Context.Tag<NodeClient.HttpAgent>("@effect/platform-node/Http/NodeClient/HttpAgent")
30
+
31
+ /** @internal */
32
+ export const makeAgent = (options?: Https.AgentOptions): Effect.Effect<Scope.Scope, never, NodeClient.HttpAgent> =>
33
+ Effect.map(
34
+ Effect.all([
35
+ Effect.acquireRelease(
36
+ Effect.sync(() => new Http.Agent(options)),
37
+ (agent) => Effect.sync(() => agent.destroy())
38
+ ),
39
+ Effect.acquireRelease(
40
+ Effect.sync(() => new Https.Agent(options)),
41
+ (agent) => Effect.sync(() => agent.destroy())
42
+ )
43
+ ]),
44
+ ([http, https]) => ({
45
+ [HttpAgentTypeId]: HttpAgentTypeId,
46
+ http,
47
+ https
48
+ })
49
+ )
50
+
51
+ /** @internal */
52
+ export const agentLayer = Layer.scoped(HttpAgent, makeAgent())
53
+
54
+ const fromAgent = (agent: NodeClient.HttpAgent): Client.Client.Default => (request) =>
55
+ Effect.flatMap(
56
+ UrlParams.makeUrl(request.url, request.urlParams, (_) =>
57
+ Error.RequestError({
58
+ request,
59
+ reason: "InvalidUrl",
60
+ error: _
61
+ })),
62
+ (url) =>
63
+ Effect.suspend(() => {
64
+ const controller = new AbortController()
65
+ const nodeRequest = url.protocol === "https:" ?
66
+ Https.request(url, {
67
+ agent: agent.https,
68
+ method: request.method,
69
+ headers: Object.fromEntries(request.headers),
70
+ signal: controller.signal
71
+ }) :
72
+ Http.request(url, {
73
+ agent: agent.http,
74
+ method: request.method,
75
+ headers: Object.fromEntries(request.headers),
76
+ signal: controller.signal
77
+ })
78
+ return pipe(
79
+ Effect.zipRight(sendBody(nodeRequest, request, request.body), waitForResponse(nodeRequest), {
80
+ concurrent: true
81
+ }),
82
+ Effect.onInterrupt(() => Effect.sync(() => controller.abort())),
83
+ Effect.map((_) => new ClientResponseImpl(request, _))
84
+ )
85
+ })
86
+ )
87
+
88
+ const sendBody = (
89
+ nodeRequest: Http.ClientRequest,
90
+ request: ClientRequest.ClientRequest,
91
+ body: Body.Body
92
+ ): Effect.Effect<never, Error.RequestError, void> =>
93
+ Effect.suspend((): Effect.Effect<never, Error.RequestError, void> => {
94
+ switch (body._tag) {
95
+ case "Empty": {
96
+ nodeRequest.end()
97
+ return waitForFinish(nodeRequest, request)
98
+ }
99
+ case "Bytes":
100
+ case "Raw": {
101
+ nodeRequest.end(body.body)
102
+ return waitForFinish(nodeRequest, request)
103
+ }
104
+ case "FormData": {
105
+ const response = new Response(body.formData)
106
+
107
+ response.headers.forEach((value, key) => {
108
+ nodeRequest.setHeader(key, value)
109
+ })
110
+
111
+ return Effect.tryPromise({
112
+ try: () => pipeline(Readable.fromWeb(response.body! as any), nodeRequest),
113
+ catch: (_) =>
114
+ Error.RequestError({
115
+ request,
116
+ reason: "Transport",
117
+ error: _
118
+ })
119
+ })
120
+ }
121
+ case "BytesEffect": {
122
+ return Effect.flatMap(
123
+ Effect.mapError(body.body, (_) =>
124
+ Error.RequestError({
125
+ request,
126
+ reason: "Encode",
127
+ error: _
128
+ })),
129
+ (bytes) => {
130
+ nodeRequest.end(bytes)
131
+ return waitForFinish(nodeRequest, request)
132
+ }
133
+ )
134
+ }
135
+ case "Stream": {
136
+ return Stream.run(
137
+ Stream.mapError(body.stream, (_) =>
138
+ Error.RequestError({
139
+ request,
140
+ reason: "Encode",
141
+ error: _
142
+ })),
143
+ NodeSink.fromWritable(() => nodeRequest, (_) =>
144
+ Error.RequestError({
145
+ request,
146
+ reason: "Transport",
147
+ error: _
148
+ }))
149
+ )
150
+ }
151
+ }
152
+ })
153
+
154
+ const waitForResponse = (nodeRequest: Http.ClientRequest) =>
155
+ Effect.async<never, never, Http.IncomingMessage>((resume) => {
156
+ nodeRequest.on("response", (response) => {
157
+ resume(Effect.succeed(response))
158
+ })
159
+ return Effect.sync(() => {
160
+ nodeRequest.removeAllListeners("response")
161
+ })
162
+ })
163
+
164
+ const waitForFinish = (nodeRequest: Http.ClientRequest, request: ClientRequest.ClientRequest) =>
165
+ Effect.async<never, Error.RequestError, void>((resume) => {
166
+ nodeRequest.on("error", (error) => {
167
+ resume(Effect.fail(Error.RequestError({
168
+ request,
169
+ reason: "Transport",
170
+ error
171
+ })))
172
+ })
173
+
174
+ nodeRequest.on("finish", () => {
175
+ resume(Effect.unit)
176
+ })
177
+
178
+ return Effect.sync(() => {
179
+ nodeRequest.removeAllListeners("error")
180
+ nodeRequest.removeAllListeners("finish")
181
+ })
182
+ })
183
+
184
+ class ClientResponseImpl implements ClientResponse.ClientResponse {
185
+ readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId = IncomingMessage.TypeId
186
+ readonly [ClientResponse.TypeId]: ClientResponse.TypeId = ClientResponse.TypeId
187
+
188
+ constructor(
189
+ readonly request: ClientRequest.ClientRequest,
190
+ readonly source: Http.IncomingMessage
191
+ ) {}
192
+
193
+ get status() {
194
+ return this.source.statusCode!
195
+ }
196
+
197
+ get headers() {
198
+ return Headers.fromInput(this.source.headers as any)
199
+ }
200
+
201
+ get text(): Effect.Effect<never, Error.ResponseError, string> {
202
+ return NodeStream.toString(() => this.source, (_) =>
203
+ Error.ResponseError({
204
+ request: this.request,
205
+ response: this,
206
+ reason: "Decode",
207
+ error: _
208
+ }))
209
+ }
210
+
211
+ get json(): Effect.Effect<never, Error.ResponseError, unknown> {
212
+ return Effect.tryMap(this.text, {
213
+ try: (_) => JSON.parse(_) as unknown,
214
+ catch: (_) =>
215
+ Error.ResponseError({
216
+ request: this.request,
217
+ response: this,
218
+ reason: "Decode",
219
+ error: _
220
+ })
221
+ })
222
+ }
223
+
224
+ get formData(): Effect.Effect<never, Error.ResponseError, FormData> {
225
+ return Effect.tryPromise({
226
+ try: () =>
227
+ new Response(Readable.toWeb(this.source) as any, {
228
+ headers: new globalThis.Headers(this.source.headers as any),
229
+ status: this.source.statusCode,
230
+ statusText: this.source.statusMessage
231
+ }).formData(),
232
+ catch: (_) =>
233
+ Error.ResponseError({
234
+ request: this.request,
235
+ response: this,
236
+ reason: "Decode",
237
+ error: _
238
+ })
239
+ })
240
+ }
241
+
242
+ get stream(): Stream.Stream<never, Error.ResponseError, Uint8Array> {
243
+ return NodeStream.fromReadable<Error.ResponseError, Uint8Array>(
244
+ () => this.source,
245
+ (_) =>
246
+ Error.ResponseError({
247
+ request: this.request,
248
+ response: this,
249
+ reason: "Decode",
250
+ error: _
251
+ })
252
+ )
253
+ }
254
+
255
+ get arrayBuffer(): Effect.Effect<never, Error.ResponseError, ArrayBuffer> {
256
+ return NodeStream.toUint8Array(() => this.source, (_) =>
257
+ Error.ResponseError({
258
+ request: this.request,
259
+ response: this,
260
+ reason: "Decode",
261
+ error: _
262
+ }))
263
+ }
264
+ }
265
+
266
+ /** @internal */
267
+ export const make = Effect.map(HttpAgent, fromAgent)
268
+
269
+ /** @internal */
270
+ export const layer = Layer.provide(
271
+ agentLayer,
272
+ Layer.effect(Client.Client, make)
273
+ )
@@ -58,3 +58,51 @@ const readChunk = <A>(
58
58
  Effect.sync(() => (size._tag === "Some" ? stream.read(Number(size)) : stream.read()) as A | null),
59
59
  Effect.flatMap((_) => (_ ? Effect.succeed(_) : Effect.fail(Option.none())))
60
60
  )
61
+
62
+ /** @internal */
63
+ export const toString = <E>(
64
+ evaluate: LazyArg<Readable>,
65
+ onError: (error: unknown) => E,
66
+ encoding: BufferEncoding = "utf-8"
67
+ ): Effect.Effect<never, E, string> =>
68
+ Effect.async<never, E, string>((resume) => {
69
+ const stream = evaluate()
70
+ let string = ""
71
+ stream.setEncoding(encoding)
72
+ stream.once("error", (err) => {
73
+ resume(Effect.fail(onError(err)))
74
+ })
75
+ stream.once("end", () => {
76
+ resume(Effect.succeed(string))
77
+ })
78
+ stream.on("data", (chunk) => {
79
+ string += chunk
80
+ })
81
+ return Effect.sync(() => {
82
+ stream.removeAllListeners()
83
+ stream.destroy()
84
+ })
85
+ })
86
+
87
+ /** @internal */
88
+ export const toUint8Array = <E>(
89
+ evaluate: LazyArg<Readable>,
90
+ onError: (error: unknown) => E
91
+ ): Effect.Effect<never, E, Uint8Array> =>
92
+ Effect.async<never, E, Uint8Array>((resume) => {
93
+ const stream = evaluate()
94
+ let buffer = Buffer.alloc(0)
95
+ stream.once("error", (err) => {
96
+ resume(Effect.fail(onError(err)))
97
+ })
98
+ stream.once("end", () => {
99
+ resume(Effect.succeed(buffer))
100
+ })
101
+ stream.on("data", (chunk) => {
102
+ buffer = Buffer.concat([buffer, chunk])
103
+ })
104
+ return Effect.sync(() => {
105
+ stream.removeAllListeners()
106
+ stream.destroy()
107
+ })
108
+ })