@effect/platform-node 0.11.5 → 0.12.0

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 (63) hide show
  1. package/Http/NodeClient.d.ts +10 -0
  2. package/Http/NodeClient.d.ts.map +1 -1
  3. package/Http/NodeClient.js +14 -2
  4. package/Http/NodeClient.js.map +1 -1
  5. package/Http/Server.d.ts +34 -0
  6. package/Http/Server.d.ts.map +1 -0
  7. package/Http/Server.js +49 -0
  8. package/Http/Server.js.map +1 -0
  9. package/HttpServer.d.ts +9 -0
  10. package/HttpServer.d.ts.map +1 -0
  11. package/HttpServer.js +26 -0
  12. package/HttpServer.js.map +1 -0
  13. package/Stream.d.ts +14 -6
  14. package/Stream.d.ts.map +1 -1
  15. package/Stream.js.map +1 -1
  16. package/internal/fileSystem.js +9 -9
  17. package/internal/fileSystem.js.map +1 -1
  18. package/internal/http/formData.d.ts +5 -0
  19. package/internal/http/formData.d.ts.map +1 -0
  20. package/internal/http/formData.js +106 -0
  21. package/internal/http/formData.js.map +1 -0
  22. package/internal/http/incomingMessage.d.ts +2 -0
  23. package/internal/http/incomingMessage.d.ts.map +1 -0
  24. package/internal/http/incomingMessage.js +62 -0
  25. package/internal/http/incomingMessage.js.map +1 -0
  26. package/internal/http/nodeClient.js +28 -65
  27. package/internal/http/nodeClient.js.map +1 -1
  28. package/internal/http/server.d.ts +2 -0
  29. package/internal/http/server.d.ts.map +1 -0
  30. package/internal/http/server.js +218 -0
  31. package/internal/http/server.js.map +1 -0
  32. package/internal/stream.js +54 -35
  33. package/internal/stream.js.map +1 -1
  34. package/mjs/Http/NodeClient.mjs +10 -0
  35. package/mjs/Http/NodeClient.mjs.map +1 -1
  36. package/mjs/Http/Server.mjs +21 -0
  37. package/mjs/Http/Server.mjs.map +1 -0
  38. package/mjs/HttpServer.mjs +7 -0
  39. package/mjs/HttpServer.mjs.map +1 -0
  40. package/mjs/Stream.mjs.map +1 -1
  41. package/mjs/internal/fileSystem.mjs +9 -9
  42. package/mjs/internal/fileSystem.mjs.map +1 -1
  43. package/mjs/internal/http/formData.mjs +95 -0
  44. package/mjs/internal/http/formData.mjs.map +1 -0
  45. package/mjs/internal/http/incomingMessage.mjs +53 -0
  46. package/mjs/internal/http/incomingMessage.mjs.map +1 -0
  47. package/mjs/internal/http/nodeClient.mjs +25 -64
  48. package/mjs/internal/http/nodeClient.mjs.map +1 -1
  49. package/mjs/internal/http/server.mjs +207 -0
  50. package/mjs/internal/http/server.mjs.map +1 -0
  51. package/mjs/internal/stream.mjs +54 -35
  52. package/mjs/internal/stream.mjs.map +1 -1
  53. package/package.json +5 -2
  54. package/src/Http/NodeClient.ts +13 -0
  55. package/src/Http/Server.ts +45 -0
  56. package/src/HttpServer.ts +10 -0
  57. package/src/Stream.ts +14 -9
  58. package/src/internal/fileSystem.ts +16 -11
  59. package/src/internal/http/formData.ts +158 -0
  60. package/src/internal/http/incomingMessage.ts +78 -0
  61. package/src/internal/http/nodeClient.ts +30 -78
  62. package/src/internal/http/server.ts +323 -0
  63. package/src/internal/stream.ts +81 -46
@@ -0,0 +1,323 @@
1
+ import type { LazyArg } from "@effect/data/Function"
2
+ import * as Option from "@effect/data/Option"
3
+ import * as Config from "@effect/io/Config"
4
+ import * as Effect from "@effect/io/Effect"
5
+ import * as Fiber from "@effect/io/Fiber"
6
+ import * as Layer from "@effect/io/Layer"
7
+ import * as Runtime from "@effect/io/Runtime"
8
+ import type * as Scope from "@effect/io/Scope"
9
+ import * as internalFormData from "@effect/platform-node/internal/http/formData"
10
+ import { IncomingMessageImpl } from "@effect/platform-node/internal/http/incomingMessage"
11
+ import * as NodeSink from "@effect/platform-node/Sink"
12
+ import * as FileSystem from "@effect/platform/FileSystem"
13
+ import type * as FormData from "@effect/platform/Http/FormData"
14
+ import * as Headers from "@effect/platform/Http/Headers"
15
+ import * as IncomingMessage from "@effect/platform/Http/IncomingMessage"
16
+ import type { Method } from "@effect/platform/Http/Method"
17
+ import * as Middleware from "@effect/platform/Http/Middleware"
18
+ import * as Server from "@effect/platform/Http/Server"
19
+ import * as Error from "@effect/platform/Http/ServerError"
20
+ import * as ServerRequest from "@effect/platform/Http/ServerRequest"
21
+ import * as ServerResponse from "@effect/platform/Http/ServerResponse"
22
+ import type * as Path from "@effect/platform/Path"
23
+ import * as Stream from "@effect/stream/Stream"
24
+ import type * as Http from "node:http"
25
+ import type * as Net from "node:net"
26
+ import { Readable } from "node:stream"
27
+
28
+ /** @internal */
29
+ export const make = (
30
+ evaluate: LazyArg<Http.Server>,
31
+ options: Net.ListenOptions
32
+ ): Effect.Effect<Scope.Scope, never, Server.Server> =>
33
+ Effect.gen(function*(_) {
34
+ const server = evaluate()
35
+
36
+ const serverFiber = yield* _(
37
+ Effect.addFinalizer(() =>
38
+ Effect.async<never, never, void>((resume) => {
39
+ server.close((error) => {
40
+ if (error) {
41
+ resume(Effect.die(error))
42
+ } else {
43
+ resume(Effect.unit)
44
+ }
45
+ })
46
+ })
47
+ ),
48
+ Effect.zipRight(
49
+ Effect.async<never, Error.ServeError, never>((resume) => {
50
+ server.on("error", (error) => {
51
+ resume(Effect.fail(Error.ServeError({ error })))
52
+ })
53
+ })
54
+ ),
55
+ Effect.scoped,
56
+ Effect.forkScoped
57
+ )
58
+
59
+ yield* _(Effect.async<never, never, void>((resume) => {
60
+ server.listen(options, () => {
61
+ resume(Effect.unit)
62
+ })
63
+ }))
64
+
65
+ const address = server.address()!
66
+
67
+ return Server.make({
68
+ address: typeof address === "string" ?
69
+ {
70
+ _tag: "UnixAddress",
71
+ path: address
72
+ } :
73
+ {
74
+ _tag: "TcpAddress",
75
+ hostname: address.address,
76
+ port: address.port
77
+ },
78
+ serve: (httpApp, middleware) => {
79
+ const handledApp = middleware ? middleware(respond(httpApp)) : respond(httpApp)
80
+ return Effect.flatMap(Effect.runtime(), (runtime) => {
81
+ const runFork = Runtime.runFork(runtime as Runtime.Runtime<unknown>)
82
+ function handler(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) {
83
+ runFork(
84
+ Effect.provideService(
85
+ handledApp,
86
+ ServerRequest.ServerRequest,
87
+ new ServerRequestImpl(nodeRequest, nodeResponse)
88
+ )
89
+ )
90
+ }
91
+ return Effect.all([
92
+ Effect.acquireRelease(
93
+ Effect.sync(() => server.on("request", handler)),
94
+ () => Effect.sync(() => server.off("request", handler))
95
+ ),
96
+ Fiber.join(serverFiber)
97
+ ], { discard: true, concurrency: "unbounded" }) as Effect.Effect<never, Error.ServeError, never>
98
+ })
99
+ }
100
+ })
101
+ }).pipe(
102
+ Effect.locally(
103
+ IncomingMessage.maxBodySize,
104
+ Option.some(FileSystem.Size(1024 * 1024 * 10))
105
+ )
106
+ )
107
+
108
+ const respond = Middleware.make((httpApp) =>
109
+ Effect.flatMap(ServerRequest.ServerRequest, (request) =>
110
+ Effect.tapErrorCause(
111
+ Effect.tap(
112
+ Effect.flatMap(httpApp, ServerResponse.toNonEffectBody),
113
+ (response) => handleResponse(request, response)
114
+ ),
115
+ (_cause) =>
116
+ Effect.sync(() => {
117
+ const nodeResponse = (request as ServerRequestImpl).response
118
+ if (!nodeResponse.headersSent) {
119
+ nodeResponse.writeHead(500)
120
+ }
121
+ if (!nodeResponse.writableEnded) {
122
+ nodeResponse.end()
123
+ }
124
+ })
125
+ ))
126
+ )
127
+
128
+ class ServerRequestImpl extends IncomingMessageImpl<Error.RequestError> implements ServerRequest.ServerRequest {
129
+ readonly [ServerRequest.TypeId]: ServerRequest.TypeId
130
+
131
+ constructor(
132
+ readonly source: Http.IncomingMessage,
133
+ readonly response: Http.ServerResponse,
134
+ readonly url = source.url!,
135
+ private headersOverride?: Headers.Headers
136
+ ) {
137
+ super(source, (_) =>
138
+ Error.RequestError({
139
+ request: this,
140
+ reason: "Decode",
141
+ error: _
142
+ }))
143
+ this[ServerRequest.TypeId] = ServerRequest.TypeId
144
+ }
145
+
146
+ get originalUrl(): string {
147
+ return this.source.url!
148
+ }
149
+
150
+ get method(): Method {
151
+ return this.source.method as Method
152
+ }
153
+
154
+ get headers(): Headers.Headers {
155
+ this.headersOverride ??= Headers.fromInput(this.source.headers as any)
156
+ return this.headersOverride
157
+ }
158
+
159
+ private formDataEffect:
160
+ | Effect.Effect<
161
+ Scope.Scope | FileSystem.FileSystem | Path.Path,
162
+ FormData.FormDataError,
163
+ globalThis.FormData
164
+ >
165
+ | undefined
166
+ get formData(): Effect.Effect<
167
+ Scope.Scope | FileSystem.FileSystem | Path.Path,
168
+ FormData.FormDataError,
169
+ globalThis.FormData
170
+ > {
171
+ if (this.formDataEffect) {
172
+ return this.formDataEffect
173
+ }
174
+ this.formDataEffect = Effect.runSync(Effect.cached(
175
+ internalFormData.formData(this.source)
176
+ ))
177
+ return this.formDataEffect
178
+ }
179
+
180
+ get formDataStream(): Stream.Stream<never, FormData.FormDataError, FormData.Part> {
181
+ return internalFormData.fromRequest(this.source)
182
+ }
183
+
184
+ setUrl(url: string): ServerRequest.ServerRequest {
185
+ return new ServerRequestImpl(
186
+ this.source,
187
+ this.response,
188
+ url,
189
+ this.headersOverride
190
+ )
191
+ }
192
+
193
+ replaceHeaders(headers: Headers.Headers): ServerRequest.ServerRequest {
194
+ return new ServerRequestImpl(
195
+ this.source,
196
+ this.response,
197
+ this.url,
198
+ headers
199
+ )
200
+ }
201
+
202
+ toString(): string {
203
+ return `ServerRequest(${this.method} ${this.url})`
204
+ }
205
+
206
+ toJSON(): unknown {
207
+ return {
208
+ _tag: "ServerRequest",
209
+ method: this.method,
210
+ url: this.url,
211
+ originalUrl: this.originalUrl,
212
+ headers: Object.fromEntries(this.headers)
213
+ }
214
+ }
215
+ }
216
+
217
+ /** @internal */
218
+ export const layer = (
219
+ evaluate: LazyArg<Http.Server>,
220
+ options: Net.ListenOptions
221
+ ) => Layer.scoped(Server.Server, make(evaluate, options))
222
+
223
+ /** @internal */
224
+ export const layerConfig = (
225
+ evaluate: LazyArg<Http.Server>,
226
+ options: Config.Config.Wrap<Net.ListenOptions>
227
+ ) =>
228
+ Layer.scoped(
229
+ Server.Server,
230
+ Effect.flatMap(
231
+ Effect.config(Config.unwrap(options)),
232
+ (options) => make(evaluate, options)
233
+ )
234
+ )
235
+
236
+ const handleResponse = (
237
+ request: ServerRequest.ServerRequest,
238
+ response: ServerResponse.ServerResponse.NonEffectBody
239
+ ) =>
240
+ Effect.suspend((): Effect.Effect<never, Error.ResponseError, void> => {
241
+ const nodeResponse = (request as ServerRequestImpl).response
242
+ switch (response.body._tag) {
243
+ case "Empty": {
244
+ nodeResponse.writeHead(
245
+ response.status,
246
+ response.headers === Headers.empty ? undefined : Object.fromEntries(response.headers)
247
+ )
248
+ nodeResponse.end()
249
+ return Effect.unit
250
+ }
251
+ case "Raw": {
252
+ const headers = response.headers === Headers.empty ? {} : Object.fromEntries(response.headers)
253
+ if (response.body.contentType) {
254
+ headers["content-type"] = response.body.contentType
255
+ }
256
+ if (response.body.contentLength) {
257
+ headers["content-length"] = response.body.contentLength.toString()
258
+ }
259
+ nodeResponse.writeHead(response.status, headers)
260
+ nodeResponse.end(response.body.body)
261
+ return Effect.unit
262
+ }
263
+ case "Uint8Array": {
264
+ const headers = response.headers === Headers.empty ? {} : Object.fromEntries(response.headers)
265
+ headers["content-type"] = response.body.contentType
266
+ headers["content-length"] = response.body.contentLength.toString()
267
+ nodeResponse.writeHead(response.status, headers)
268
+ nodeResponse.end(response.body.body)
269
+ return Effect.unit
270
+ }
271
+ case "FormData": {
272
+ const body = response.body
273
+ return Effect.async<never, Error.ResponseError, void>((resume) => {
274
+ const r = new Response(body.formData)
275
+ const headers = response.headers
276
+ ? Object.fromEntries(response.headers)
277
+ : {}
278
+ headers["content-type"] = r.headers.get("content-type")!
279
+ nodeResponse.writeHead(response.status, headers)
280
+ Readable.fromWeb(r.body as any)
281
+ .pipe(nodeResponse)
282
+ .on("error", (error) => {
283
+ resume(Effect.fail(Error.ResponseError({
284
+ request,
285
+ response,
286
+ reason: "Decode",
287
+ error
288
+ })))
289
+ })
290
+ .once("finish", () => {
291
+ resume(Effect.unit)
292
+ })
293
+ })
294
+ }
295
+ case "Stream": {
296
+ const headers = response.headers === Headers.empty ? {} : Object.fromEntries(response.headers)
297
+ headers["content-type"] = response.body.contentType
298
+ if (response.body.contentLength) {
299
+ headers["content-length"] = response.body.contentLength.toString()
300
+ }
301
+ nodeResponse.writeHead(response.status, headers)
302
+ return Stream.run(
303
+ Stream.mapError(
304
+ response.body.stream,
305
+ (error) =>
306
+ Error.ResponseError({
307
+ request,
308
+ response,
309
+ reason: "Decode",
310
+ error
311
+ })
312
+ ),
313
+ NodeSink.fromWritable(() => nodeResponse, (error) =>
314
+ Error.ResponseError({
315
+ request,
316
+ response,
317
+ reason: "Decode",
318
+ error
319
+ }))
320
+ )
321
+ }
322
+ }
323
+ })
@@ -3,7 +3,7 @@ import { pipe } from "@effect/data/Function"
3
3
  import * as Option from "@effect/data/Option"
4
4
  import * as Effect from "@effect/io/Effect"
5
5
  import type { FromReadableOptions } from "@effect/platform-node/Stream"
6
- import type { Size } from "@effect/platform/FileSystem"
6
+ import type { SizeInput } from "@effect/platform/FileSystem"
7
7
  import * as Stream from "@effect/stream/Stream"
8
8
  import type { Readable } from "node:stream"
9
9
 
@@ -11,7 +11,7 @@ import type { Readable } from "node:stream"
11
11
  export const fromReadable = <E, A>(
12
12
  evaluate: LazyArg<Readable>,
13
13
  onError: (error: unknown) => E,
14
- { chunkSize = Option.none() }: FromReadableOptions = {}
14
+ { chunkSize }: FromReadableOptions = {}
15
15
  ): Stream.Stream<never, E, A> =>
16
16
  pipe(
17
17
  Effect.acquireRelease(Effect.sync(evaluate), (stream) =>
@@ -52,57 +52,92 @@ export const fromReadable = <E, A>(
52
52
 
53
53
  const readChunk = <A>(
54
54
  stream: Readable,
55
- size: Option.Option<Size>
55
+ size: SizeInput | undefined
56
56
  ): Effect.Effect<never, Option.Option<never>, A> =>
57
57
  pipe(
58
- Effect.sync(() => (size._tag === "Some" ? stream.read(Number(size)) : stream.read()) as A | null),
58
+ Effect.sync(() => (size ? stream.read(Number(size)) : stream.read()) as A | null),
59
59
  Effect.flatMap((_) => (_ ? Effect.succeed(_) : Effect.fail(Option.none())))
60
60
  )
61
61
 
62
62
  /** @internal */
63
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
- })
64
+ options: {
65
+ readable: LazyArg<Readable>
66
+ onFailure: (error: unknown) => E
67
+ encoding?: BufferEncoding
68
+ maxBytes?: SizeInput
69
+ }
70
+ ): Effect.Effect<never, E, string> => {
71
+ const maxBytesNumber = options.maxBytes ? Number(options.maxBytes) : undefined
72
+ return Effect.acquireUseRelease(
73
+ Effect.sync(() => {
74
+ const stream = options.readable()
75
+ stream.setEncoding(options.encoding ?? "utf8")
76
+ return stream
77
+ }),
78
+ (stream) =>
79
+ Effect.async((resume) => {
80
+ let string = ""
81
+ let bytes = 0
82
+ stream.once("error", (err) => {
83
+ resume(Effect.fail(options.onFailure(err)))
84
+ })
85
+ stream.once("end", () => {
86
+ resume(Effect.succeed(string))
87
+ })
88
+ stream.on("data", (chunk) => {
89
+ string += chunk
90
+ bytes += Buffer.byteLength(chunk)
91
+ if (maxBytesNumber && bytes > maxBytesNumber) {
92
+ resume(Effect.fail(options.onFailure(new Error("maxBytes exceeded"))))
93
+ }
94
+ })
95
+ }),
96
+ (stream) =>
97
+ Effect.sync(() => {
98
+ stream.removeAllListeners()
99
+ if (!stream.closed) {
100
+ stream.destroy()
101
+ }
102
+ })
103
+ )
104
+ }
86
105
 
87
106
  /** @internal */
88
107
  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
- })
108
+ options: {
109
+ readable: LazyArg<Readable>
110
+ onFailure: (error: unknown) => E
111
+ maxBytes?: SizeInput
112
+ }
113
+ ): Effect.Effect<never, E, Uint8Array> => {
114
+ const maxBytesNumber = options.maxBytes ? Number(options.maxBytes) : undefined
115
+ return Effect.acquireUseRelease(
116
+ Effect.sync(options.readable),
117
+ (stream) =>
118
+ Effect.async((resume) => {
119
+ let buffer = Buffer.alloc(0)
120
+ let bytes = 0
121
+ stream.once("error", (err) => {
122
+ resume(Effect.fail(options.onFailure(err)))
123
+ })
124
+ stream.once("end", () => {
125
+ resume(Effect.succeed(buffer))
126
+ })
127
+ stream.on("data", (chunk) => {
128
+ buffer = Buffer.concat([buffer, chunk])
129
+ bytes += chunk.length
130
+ if (maxBytesNumber && bytes > maxBytesNumber) {
131
+ resume(Effect.fail(options.onFailure(new Error("maxBytes exceeded"))))
132
+ }
133
+ })
134
+ }),
135
+ (stream) =>
136
+ Effect.sync(() => {
137
+ stream.removeAllListeners()
138
+ if (!stream.closed) {
139
+ stream.destroy()
140
+ }
141
+ })
142
+ )
143
+ }