@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.
- package/Http/NodeClient.d.ts +57 -0
- package/Http/NodeClient.d.ts.map +1 -0
- package/Http/NodeClient.js +46 -0
- package/Http/NodeClient.js.map +1 -0
- package/HttpClient.d.ts +4 -0
- package/HttpClient.d.ts.map +1 -1
- package/HttpClient.js +3 -1
- package/HttpClient.js.map +1 -1
- package/Stream.d.ts +14 -2
- package/Stream.d.ts.map +1 -1
- package/Stream.js +14 -2
- package/Stream.js.map +1 -1
- package/internal/http/nodeClient.d.ts +2 -0
- package/internal/http/nodeClient.d.ts.map +1 -0
- package/internal/http/nodeClient.js +211 -0
- package/internal/http/nodeClient.js.map +1 -0
- package/internal/stream.js +40 -1
- package/internal/stream.js.map +1 -1
- package/mjs/Http/NodeClient.mjs +32 -0
- package/mjs/Http/NodeClient.mjs.map +1 -0
- package/mjs/HttpClient.mjs +2 -0
- package/mjs/HttpClient.mjs.map +1 -1
- package/mjs/Stream.mjs +11 -1
- package/mjs/Stream.mjs.map +1 -1
- package/mjs/internal/http/nodeClient.mjs +197 -0
- package/mjs/internal/http/nodeClient.mjs.map +1 -0
- package/mjs/internal/stream.mjs +37 -0
- package/mjs/internal/stream.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Http/NodeClient.ts +64 -0
- package/src/HttpClient.ts +4 -0
- package/src/Stream.ts +22 -2
- package/src/internal/http/nodeClient.ts +273 -0
- package/src/internal/stream.ts +48 -0
|
@@ -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
|
+
)
|
package/src/internal/stream.ts
CHANGED
|
@@ -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
|
+
})
|