@effect/platform-node 4.0.0-beta.102 → 4.0.0-beta.104

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 (61) hide show
  1. package/dist/Mime.js.map +1 -1
  2. package/dist/NodeClusterHttp.d.ts +1 -0
  3. package/dist/NodeClusterHttp.d.ts.map +1 -1
  4. package/dist/NodeClusterHttp.js +5 -1
  5. package/dist/NodeClusterHttp.js.map +1 -1
  6. package/dist/NodeClusterSocket.d.ts +1 -0
  7. package/dist/NodeClusterSocket.d.ts.map +1 -1
  8. package/dist/NodeClusterSocket.js +9 -2
  9. package/dist/NodeClusterSocket.js.map +1 -1
  10. package/dist/NodeCrypto.js.map +1 -1
  11. package/dist/NodeFileSystem.js.map +1 -1
  12. package/dist/NodeHttpClient.d.ts +18 -18
  13. package/dist/NodeHttpClient.d.ts.map +1 -1
  14. package/dist/NodeHttpClient.js +26 -20
  15. package/dist/NodeHttpClient.js.map +1 -1
  16. package/dist/NodeHttpIncomingMessage.js.map +1 -1
  17. package/dist/NodeHttpPlatform.d.ts +8 -4
  18. package/dist/NodeHttpPlatform.d.ts.map +1 -1
  19. package/dist/NodeHttpPlatform.js +38 -0
  20. package/dist/NodeHttpPlatform.js.map +1 -1
  21. package/dist/NodeHttpServer.d.ts +21 -16
  22. package/dist/NodeHttpServer.d.ts.map +1 -1
  23. package/dist/NodeHttpServer.js +23 -20
  24. package/dist/NodeHttpServer.js.map +1 -1
  25. package/dist/NodeHttpServerRequest.js.map +1 -1
  26. package/dist/NodeMultipart.d.ts.map +1 -1
  27. package/dist/NodeMultipart.js +3 -2
  28. package/dist/NodeMultipart.js.map +1 -1
  29. package/dist/NodeMultipartParser.d.ts +97 -0
  30. package/dist/NodeMultipartParser.d.ts.map +1 -0
  31. package/dist/NodeMultipartParser.js +108 -0
  32. package/dist/NodeMultipartParser.js.map +1 -0
  33. package/dist/NodePath.js.map +1 -1
  34. package/dist/NodeRedis.js.map +1 -1
  35. package/dist/NodeRuntime.js.map +1 -1
  36. package/dist/NodeServices.js.map +1 -1
  37. package/dist/NodeSocket.js.map +1 -1
  38. package/dist/NodeStdio.js.map +1 -1
  39. package/dist/NodeTerminal.js.map +1 -1
  40. package/dist/NodeWorker.js.map +1 -1
  41. package/dist/NodeWorkerRunner.d.ts.map +1 -1
  42. package/dist/NodeWorkerRunner.js +30 -18
  43. package/dist/NodeWorkerRunner.js.map +1 -1
  44. package/dist/Undici.d.ts +2 -2
  45. package/dist/Undici.js +2 -2
  46. package/dist/Undici.js.map +1 -1
  47. package/dist/index.d.ts +4 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -1
  51. package/package.json +10 -11
  52. package/src/NodeClusterHttp.ts +4 -1
  53. package/src/NodeClusterSocket.ts +6 -1
  54. package/src/NodeHttpClient.ts +27 -20
  55. package/src/NodeHttpPlatform.ts +49 -0
  56. package/src/NodeHttpServer.ts +61 -45
  57. package/src/NodeMultipart.ts +5 -4
  58. package/src/NodeMultipartParser.ts +184 -0
  59. package/src/NodeWorkerRunner.ts +39 -23
  60. package/src/Undici.ts +2 -2
  61. package/src/index.ts +5 -0
@@ -51,14 +51,14 @@ export {
51
51
  * Use to access or override the fetch implementation used by the Node
52
52
  * fetch-based HTTP client.
53
53
  *
54
- * @category fetch
54
+ * @category services
55
55
  * @since 4.0.0
56
56
  */
57
57
  Fetch,
58
58
  /**
59
59
  * Layer that provides the fetch-based HTTP client implementation.
60
60
  *
61
- * @category fetch
61
+ * @category layers
62
62
  * @since 4.0.0
63
63
  */
64
64
  layer as layerFetch,
@@ -69,7 +69,7 @@ export {
69
69
  *
70
70
  * Use to provide default fetch request options for Node HTTP requests.
71
71
  *
72
- * @category fetch
72
+ * @category services
73
73
  * @since 4.0.0
74
74
  */
75
75
  RequestInit
@@ -83,7 +83,7 @@ export {
83
83
  * Service tag for the Undici `Dispatcher` used by the Undici-backed HTTP
84
84
  * client.
85
85
  *
86
- * @category Dispatcher
86
+ * @category services
87
87
  * @since 4.0.0
88
88
  */
89
89
  export class Dispatcher extends Context.Service<Dispatcher, Undici.Dispatcher>()(
@@ -94,10 +94,12 @@ export class Dispatcher extends Context.Service<Dispatcher, Undici.Dispatcher>()
94
94
  * Acquires a new Undici `Agent` dispatcher and destroys it when the enclosing
95
95
  * scope is finalized.
96
96
  *
97
- * @category Dispatcher
97
+ * @category resource management
98
98
  * @since 4.0.0
99
99
  */
100
100
  export const makeDispatcher: Effect.Effect<Undici.Dispatcher, never, Scope.Scope> = Effect.acquireRelease(
101
+ // oxlint cannot resolve values re-exported through the local Undici facade.
102
+ // oxlint-disable-next-line import/namespace
101
103
  Effect.sync(() => new Undici.Agent()),
102
104
  (dispatcher) => Effect.promise(() => dispatcher.destroy())
103
105
  )
@@ -105,7 +107,7 @@ export const makeDispatcher: Effect.Effect<Undici.Dispatcher, never, Scope.Scope
105
107
  /**
106
108
  * Provides the `Dispatcher` service using a scoped Undici `Agent`.
107
109
  *
108
- * @category Dispatcher
110
+ * @category layers
109
111
  * @since 4.0.0
110
112
  */
111
113
  export const layerDispatcher: Layer.Layer<Dispatcher> = Layer.effect(Dispatcher)(makeDispatcher)
@@ -114,16 +116,18 @@ export const layerDispatcher: Layer.Layer<Dispatcher> = Layer.effect(Dispatcher)
114
116
  * Provides the `Dispatcher` service from Undici's process-global dispatcher,
115
117
  * without creating or owning a new agent.
116
118
  *
117
- * @category Dispatcher
119
+ * @category layers
118
120
  * @since 4.0.0
119
121
  */
122
+ // oxlint cannot resolve values re-exported through the local Undici facade.
123
+ // oxlint-disable-next-line import/namespace
120
124
  export const dispatcherLayerGlobal: Layer.Layer<Dispatcher> = Layer.sync(Dispatcher)(() => Undici.getGlobalDispatcher())
121
125
 
122
126
  /**
123
127
  * Fiber reference containing default Undici request options applied to requests
124
128
  * sent by `makeUndici`.
125
129
  *
126
- * @category Undici
130
+ * @category services
127
131
  * @since 4.0.0
128
132
  */
129
133
  export const UndiciOptions = Context.Reference<Partial<Undici.Dispatcher.RequestOptions>>(
@@ -136,7 +140,7 @@ export const UndiciOptions = Context.Reference<Partial<Undici.Dispatcher.Request
136
140
  * `Dispatcher`, converts Effect HTTP bodies to Undici bodies, and maps
137
141
  * transport and decode failures to `HttpClientError`.
138
142
  *
139
- * @category Undici
143
+ * @category constructors
140
144
  * @since 4.0.0
141
145
  */
142
146
  export const makeUndici = Effect.gen(function*() {
@@ -353,7 +357,7 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse, Pi
353
357
  * Provides an Undici-backed `HttpClient` using the current `Dispatcher`
354
358
  * service.
355
359
  *
356
- * @category Undici
360
+ * @category layers
357
361
  * @since 4.0.0
358
362
  */
359
363
  export const layerUndiciNoDispatcher: Layer.Layer<
@@ -366,7 +370,7 @@ export const layerUndiciNoDispatcher: Layer.Layer<
366
370
  * Provides an Undici-backed `HttpClient` together with a scoped default
367
371
  * Undici `Agent` dispatcher.
368
372
  *
369
- * @category Undici
373
+ * @category layers
370
374
  * @since 4.0.0
371
375
  */
372
376
  export const layerUndici: Layer.Layer<Client.HttpClient> = Layer.provide(layerUndiciNoDispatcher, layerDispatcher)
@@ -379,7 +383,7 @@ export const layerUndici: Layer.Layer<Client.HttpClient> = Layer.provide(layerUn
379
383
  * Service tag for the paired Node `http` and `https` agents used by the
380
384
  * node:http-backed HTTP client.
381
385
  *
382
- * @category HttpAgent
386
+ * @category services
383
387
  * @since 4.0.0
384
388
  */
385
389
  export class HttpAgent extends Context.Service<HttpAgent, {
@@ -391,7 +395,7 @@ export class HttpAgent extends Context.Service<HttpAgent, {
391
395
  * Acquires Node `http` and `https` agents with the supplied options and
392
396
  * destroys both agents when the enclosing scope is finalized.
393
397
  *
394
- * @category HttpAgent
398
+ * @category resource management
395
399
  * @since 4.0.0
396
400
  */
397
401
  export const makeAgent = (options?: Https.AgentOptions): Effect.Effect<HttpAgent["Service"], never, Scope.Scope> =>
@@ -411,7 +415,7 @@ export const makeAgent = (options?: Https.AgentOptions): Effect.Effect<HttpAgent
411
415
  * Provides the `HttpAgent` service using scoped Node `http` and `https`
412
416
  * agents configured with the supplied options.
413
417
  *
414
- * @category HttpAgent
418
+ * @category layers
415
419
  * @since 4.0.0
416
420
  */
417
421
  export const layerAgentOptions: (options?: Https.AgentOptions | undefined) => Layer.Layer<
@@ -422,7 +426,7 @@ export const layerAgentOptions: (options?: Https.AgentOptions | undefined) => La
422
426
  * Provides the `HttpAgent` service using default scoped Node `http` and
423
427
  * `https` agents.
424
428
  *
425
- * @category HttpAgent
429
+ * @category layers
426
430
  * @since 4.0.0
427
431
  */
428
432
  export const layerAgent: Layer.Layer<HttpAgent> = layerAgentOptions()
@@ -432,7 +436,7 @@ export const layerAgent: Layer.Layer<HttpAgent> = layerAgentOptions()
432
436
  * current `HttpAgent`, streaming request bodies, and wrapping Node responses
433
437
  * as `HttpClientResponse` values.
434
438
  *
435
- * @category node:http
439
+ * @category constructors
436
440
  * @since 4.0.0
437
441
  */
438
442
  export const makeNodeHttp = Effect.gen(function*() {
@@ -451,8 +455,11 @@ export const makeNodeHttp = Effect.gen(function*() {
451
455
  headers: request.headers,
452
456
  signal
453
457
  })
454
- return Effect.forkChild(sendBody(nodeRequest, request, request.body)).pipe(
455
- Effect.flatMap(() => waitForResponse(nodeRequest, request)),
458
+ return Effect.raceFirst(
459
+ waitForResponse(nodeRequest, request),
460
+ sendBody(nodeRequest, request, request.body).pipe(Effect.andThen(Effect.never))
461
+ ).pipe(
462
+ Effect.onError(() => Effect.sync(() => nodeRequest.destroy())),
456
463
  Effect.map((_) => new NodeHttpResponse(request, _))
457
464
  )
458
465
  })
@@ -645,7 +652,7 @@ class NodeHttpResponse extends NodeHttpIncomingMessage<Error.HttpClientError> im
645
652
  * Provides a node:http-backed `HttpClient` using the current `HttpAgent`
646
653
  * service.
647
654
  *
648
- * @category node:http
655
+ * @category layers
649
656
  * @since 4.0.0
650
657
  */
651
658
  export const layerNodeHttpNoAgent: Layer.Layer<
@@ -658,7 +665,7 @@ export const layerNodeHttpNoAgent: Layer.Layer<
658
665
  * Provides a node:http-backed `HttpClient` together with default scoped Node
659
666
  * `http` and `https` agents.
660
667
  *
661
- * @category node:http
668
+ * @category layers
662
669
  * @since 4.0.0
663
670
  */
664
671
  export const layerNodeHttp: Layer.Layer<Client.HttpClient> = Layer.provide(layerNodeHttpNoAgent, layerAgent)
@@ -8,16 +8,63 @@
8
8
  *
9
9
  * @since 4.0.0
10
10
  */
11
+ import * as NodeHttpCompression from "@effect/platform-node-shared/NodeHttpCompression"
12
+ import * as Effect from "effect/Effect"
11
13
  import { pipe } from "effect/Function"
12
14
  import * as Layer from "effect/Layer"
13
15
  import * as EtagImpl from "effect/unstable/http/Etag"
14
16
  import * as Headers from "effect/unstable/http/Headers"
17
+ import * as HttpBody from "effect/unstable/http/HttpBody"
15
18
  import * as Platform from "effect/unstable/http/HttpPlatform"
16
19
  import * as ServerResponse from "effect/unstable/http/HttpServerResponse"
17
20
  import * as Fs from "node:fs"
18
21
  import { Readable } from "node:stream"
19
22
  import Mime from "./Mime.ts"
20
23
  import * as NodeFileSystem from "./NodeFileSystem.ts"
24
+ import * as NodeStream from "./NodeStream.ts"
25
+
26
+ // replaces the response body while keeping every other field, dropping the
27
+ // now-stale Content-Length header
28
+ const compressedBody = (
29
+ response: ServerResponse.HttpServerResponse,
30
+ body: HttpBody.HttpBody
31
+ ): ServerResponse.HttpServerResponse =>
32
+ ServerResponse.removeHeader(ServerResponse.setBody(response, body), "content-length")
33
+
34
+ const compression = NodeHttpCompression.make({
35
+ algorithms: NodeHttpCompression.algorithms,
36
+ compressResponse(response, algorithm, options) {
37
+ const body = response.body
38
+ switch (body._tag) {
39
+ case "Stream": {
40
+ return Effect.succeed(compressedBody(
41
+ response,
42
+ HttpBody.stream(
43
+ NodeStream.pipeThroughDuplex(body.stream, {
44
+ evaluate: () => NodeHttpCompression.compressTransform(algorithm, options)
45
+ }),
46
+ body.contentType
47
+ )
48
+ ))
49
+ }
50
+ case "Raw": {
51
+ const readable = body.body instanceof Readable
52
+ ? body.body
53
+ : Readable.fromWeb(new Response(body.body as BodyInit).body as any)
54
+ const transform = NodeHttpCompression.compressTransform(algorithm, options)
55
+ readable.on("error", (cause) => transform.destroy(cause))
56
+ transform.on("error", (cause) => readable.destroy(cause))
57
+ transform.on("close", () => readable.destroy())
58
+ return Effect.succeed(
59
+ compressedBody(response, HttpBody.raw(readable.pipe(transform), { contentType: body.contentType }))
60
+ )
61
+ }
62
+ default: {
63
+ return Effect.succeed(response)
64
+ }
65
+ }
66
+ }
67
+ })
21
68
 
22
69
  /**
23
70
  * Creates the Node `HttpPlatform`, serving file responses from Node readable
@@ -27,6 +74,8 @@ import * as NodeFileSystem from "./NodeFileSystem.ts"
27
74
  * @since 4.0.0
28
75
  */
29
76
  export const make = Platform.make({
77
+ platform: "node",
78
+ compression,
30
79
  fileResponse(path, status, statusText, headers, start, end, contentLength) {
31
80
  const stream = contentLength === 0
32
81
  ? Readable.from([])
@@ -62,6 +62,26 @@ import * as NodeMultipart from "./NodeMultipart.ts"
62
62
  import * as NodeServices from "./NodeServices.ts"
63
63
  import { NodeWS } from "./NodeSocket.ts"
64
64
 
65
+ /**
66
+ * Options accepted by the Node `HttpServer` constructors and layers.
67
+ *
68
+ * @category options
69
+ * @since 4.0.0
70
+ */
71
+ export interface Options extends Net.ListenOptions {
72
+ readonly disablePreemptiveShutdown?: boolean | undefined
73
+ readonly gracefulShutdownTimeout?: Duration.Input | undefined
74
+ /**
75
+ * Options forwarded to the underlying `ws` `WebSocketServer`, minus the
76
+ * wiring options the server manages itself. Use this to enable
77
+ * `permessage-deflate` compression or tune payload limits, e.g.
78
+ * `websocket: { perMessageDeflate: true }`.
79
+ */
80
+ readonly websocket?:
81
+ | Omit<NodeWS.ServerOptions, "noServer" | "server" | "host" | "port" | "path">
82
+ | undefined
83
+ }
84
+
65
85
  /**
66
86
  * Creates a scoped `HttpServer` from a Node `http.Server`, starts listening
67
87
  * with the supplied options, registers request and upgrade handling, and closes
@@ -72,10 +92,7 @@ import { NodeWS } from "./NodeSocket.ts"
72
92
  */
73
93
  export const make = Effect.fnUntraced(function*(
74
94
  evaluate: LazyArg<Http.Server>,
75
- options: Net.ListenOptions & {
76
- readonly disablePreemptiveShutdown?: boolean | undefined
77
- readonly gracefulShutdownTimeout?: Duration.Input | undefined
78
- }
95
+ options: Options
79
96
  ) {
80
97
  const scope = yield* Effect.scope
81
98
  const server = evaluate()
@@ -116,7 +133,7 @@ export const make = Effect.fnUntraced(function*(
116
133
  const address = server.address()!
117
134
 
118
135
  const wss = yield* Effect.acquireRelease(
119
- Effect.sync(() => new NodeWS.WebSocketServer({ noServer: true })),
136
+ Effect.sync(() => new NodeWS.WebSocketServer({ ...options.websocket, noServer: true })),
120
137
  (wss) =>
121
138
  Effect.callback<void>((resume) => {
122
139
  wss.close(() => resume(Effect.void))
@@ -189,9 +206,8 @@ export const makeHandler = <
189
206
  nodeRequest: Http.IncomingMessage,
190
207
  nodeResponse: Http.ServerResponse
191
208
  ) {
192
- const map = new Map(services.mapUnsafe)
193
- map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse))
194
- const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe<any>(map))(handled), options.scope)
209
+ const context = Context.add(services, HttpServerRequest, new ServerRequestImpl(nodeRequest, nodeResponse))
210
+ const fiber = Fiber.runIn(Effect.runForkWith(context as Context.Context<any>)(handled), options.scope)
195
211
  nodeResponse.on("close", () => {
196
212
  if (!nodeResponse.writableEnded) {
197
213
  fiber.interruptUnsafe(parent.id, ClientAbort.annotation)
@@ -256,9 +272,13 @@ export const makeUpgradeHandler = <
256
272
  (ws) => Effect.sync(() => ws.close())
257
273
  )
258
274
  ))
259
- const map = new Map(services.mapUnsafe)
260
- map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect))
261
- const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe<any>(map))(handledApp), options.scope)
275
+ const context = Context.add(
276
+ services,
277
+ HttpServerRequest,
278
+ new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect)
279
+ )
280
+ const fiber = Fiber.runIn(Effect.runForkWith(context as Context.Context<any>)(handledApp), options.scope)
281
+ socket.on("error", () => {})
262
282
  socket.on("close", () => {
263
283
  if (!socket.writableEnded) {
264
284
  fiber.interruptUnsafe(parent.id, ClientAbort.annotation)
@@ -330,8 +350,9 @@ class ServerRequestImpl extends NodeHttpIncomingMessage<HttpServerError> impleme
330
350
  return this.source.url!
331
351
  }
332
352
 
353
+ private cachedMethod: HttpMethod | undefined
333
354
  get method(): HttpMethod {
334
- return this.source.method!.toUpperCase() as HttpMethod
355
+ return this.cachedMethod ??= this.source.method!.toUpperCase() as HttpMethod
335
356
  }
336
357
 
337
358
  override get headers(): Headers.Headers {
@@ -397,10 +418,7 @@ class ServerRequestImpl extends NodeHttpIncomingMessage<HttpServerError> impleme
397
418
  */
398
419
  export const layerServer: (
399
420
  evaluate: LazyArg<Http.Server<typeof Http.IncomingMessage, typeof Http.ServerResponse>>,
400
- options: Net.ListenOptions & {
401
- readonly disablePreemptiveShutdown?: boolean | undefined
402
- readonly gracefulShutdownTimeout?: Duration.Input | undefined
403
- }
421
+ options: Options
404
422
  ) => Layer.Layer<HttpServer.HttpServer, ServeError> = flow(make, Layer.effect(HttpServer.HttpServer))
405
423
 
406
424
  /**
@@ -427,10 +445,7 @@ export const layerHttpServices: Layer.Layer<
427
445
  */
428
446
  export const layer = (
429
447
  evaluate: LazyArg<Http.Server>,
430
- options: Net.ListenOptions & {
431
- readonly disablePreemptiveShutdown?: boolean | undefined
432
- readonly gracefulShutdownTimeout?: Duration.Input | undefined
433
- }
448
+ options: Options
434
449
  ): Layer.Layer<
435
450
  HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator,
436
451
  ServeError
@@ -450,12 +465,7 @@ export const layer = (
450
465
  */
451
466
  export const layerConfig = (
452
467
  evaluate: LazyArg<Http.Server>,
453
- options: Config.Wrap<
454
- Net.ListenOptions & {
455
- readonly disablePreemptiveShutdown?: boolean | undefined
456
- readonly gracefulShutdownTimeout?: Duration.Input | undefined
457
- }
458
- >
468
+ options: Config.Wrap<Options>
459
469
  ): Layer.Layer<
460
470
  HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator,
461
471
  ServeError | Config.ConfigError
@@ -517,14 +527,20 @@ const handleResponse = (
517
527
 
518
528
  if (request.method === "HEAD") {
519
529
  nodeResponse.writeHead(response.status, headers)
520
- return Effect.callback<void>((resume) => {
521
- const done = () => {
522
- nodeResponse.off("close", done)
523
- resume(Effect.void)
524
- }
525
- nodeResponse.once("close", done)
526
- nodeResponse.end(done)
527
- })
530
+ return Effect.andThen(
531
+ cancelResponseBody(response.body),
532
+ Effect.callback<void>((resume) => {
533
+ let completed = false
534
+ const done = () => {
535
+ if (completed) return
536
+ completed = true
537
+ nodeResponse.off("close", done)
538
+ resume(Effect.void)
539
+ }
540
+ nodeResponse.once("close", done)
541
+ nodeResponse.end(done)
542
+ })
543
+ )
528
544
  }
529
545
  const body = response.body
530
546
  switch (body._tag) {
@@ -608,17 +624,9 @@ const handleResponse = (
608
624
  return body.stream.pipe(
609
625
  Stream.orDie,
610
626
  Stream.runForEachArray((array) => {
611
- let needDrain = false
612
- for (let i = 0; i < array.length; i++) {
613
- const written = nodeResponse.write(array[i])
614
- if (!written && !needDrain) {
615
- needDrain = true
616
- drainLatch.closeUnsafe()
617
- } else if (written && needDrain) {
618
- needDrain = false
619
- }
620
- }
621
- if (!needDrain) return Effect.void
627
+ const chunk = array.length > 1 ? Buffer.concat(array) : array[0]
628
+ if (nodeResponse.write(chunk)) return Effect.void
629
+ drainLatch.closeUnsafe()
622
630
  return drainLatch.await
623
631
  }),
624
632
  Effect.interruptible,
@@ -631,6 +639,14 @@ const handleResponse = (
631
639
  }
632
640
  }
633
641
 
642
+ const cancelResponseBody = (body: HttpServerResponse["body"]): Effect.Effect<void> => {
643
+ const stream = body._tag === "Raw" ? body.body : undefined
644
+ if (stream instanceof Readable) {
645
+ return Effect.sync(() => stream.destroy())
646
+ }
647
+ return Effect.void
648
+ }
649
+
634
650
  const handleCause = (
635
651
  nodeResponse: Http.ServerResponse,
636
652
  originalResponse: HttpServerResponse
@@ -17,11 +17,12 @@ import type * as Path from "effect/Path"
17
17
  import type * as Scope from "effect/Scope"
18
18
  import * as Stream from "effect/Stream"
19
19
  import * as Multipart from "effect/unstable/http/Multipart"
20
- import * as MP from "effect/unstable/http/Multipasta/Node"
20
+ import * as MultipartParser from "effect/unstable/http/MultipartParser"
21
21
  import * as NFS from "node:fs"
22
22
  import type { IncomingHttpHeaders } from "node:http"
23
23
  import type { Readable } from "node:stream"
24
24
  import * as NodeStreamP from "node:stream/promises"
25
+ import * as MP from "./NodeMultipartParser.ts"
25
26
  import * as NodeStream from "./NodeStream.ts"
26
27
 
27
28
  /**
@@ -103,13 +104,13 @@ class FieldImpl extends PartBase implements Multipart.Field {
103
104
  readonly value: string
104
105
 
105
106
  constructor(
106
- info: MP.PartInfo,
107
+ info: MultipartParser.PartInfo,
107
108
  value: Uint8Array
108
109
  ) {
109
110
  super()
110
111
  this.key = info.name
111
112
  this.contentType = info.contentType
112
- this.value = MP.decodeField(info, value)
113
+ this.value = MultipartParser.decodeField(info, value)
113
114
  }
114
115
 
115
116
  toJSON(): unknown {
@@ -158,7 +159,7 @@ class FileImpl extends PartBase implements Multipart.File {
158
159
  }
159
160
  }
160
161
 
161
- function convertError(cause: MP.MultipartError): Multipart.MultipartError {
162
+ function convertError(cause: MultipartParser.MultipartError): Multipart.MultipartError {
162
163
  switch (cause._tag) {
163
164
  case "ReachedLimit": {
164
165
  switch (cause.limit) {
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Node.js streams adapter for the low-level multipart parser.
3
+ *
4
+ * @since 4.0.0
5
+ */
6
+ // oxlint-disable typescript/no-unsafe-declaration-merging
7
+ /// <reference types="node" />
8
+ import type { BaseConfig, MultipartError, Parser, PartInfo } from "effect/unstable/http/MultipartParser"
9
+ import { make as makeParser } from "effect/unstable/http/MultipartParser"
10
+ import type { IncomingHttpHeaders } from "node:http"
11
+ import { Duplex, Readable } from "node:stream"
12
+
13
+ /**
14
+ * A part emitted by the Node.js multipart parser.
15
+ *
16
+ * @category models
17
+ * @since 4.0.0
18
+ */
19
+ export type Part = Field | FileStream
20
+
21
+ /**
22
+ * A parsed multipart field.
23
+ *
24
+ * @category models
25
+ * @since 4.0.0
26
+ */
27
+ export interface Field {
28
+ readonly _tag: "Field"
29
+ readonly info: PartInfo
30
+ readonly value: Uint8Array
31
+ }
32
+
33
+ /**
34
+ * A Node.js duplex stream that emits parsed multipart parts.
35
+ *
36
+ * @category models
37
+ * @since 4.0.0
38
+ */
39
+ export interface MultipartStream extends Duplex {
40
+ [Symbol.asyncIterator](): NodeJS.AsyncIterator<Part>
41
+
42
+ on(event: "field", listener: (field: Field) => void): this
43
+ on(event: "file", listener: (file: FileStream) => void): this
44
+ on(event: "close", listener: () => void): this
45
+ on(event: "data", listener: (part: Part) => void): this
46
+ on(event: "drain", listener: () => void): this
47
+ on(event: "end", listener: () => void): this
48
+ on(event: "error", listener: (err: MultipartError) => void): this
49
+ on(event: "finish", listener: () => void): this
50
+ on(event: "pause", listener: () => void): this
51
+ on(event: "pipe", listener: (src: Readable) => void): this
52
+ on(event: "readable", listener: () => void): this
53
+ on(event: "resume", listener: () => void): this
54
+ on(event: "unpipe", listener: (src: Readable) => void): this
55
+ on(event: string | symbol, listener: (...args: Array<any>) => void): this
56
+
57
+ read(size?: number): Part | null
58
+ }
59
+
60
+ /**
61
+ * Configuration for the Node.js multipart parser.
62
+ *
63
+ * @category models
64
+ * @since 4.0.0
65
+ */
66
+ export type NodeConfig = Omit<BaseConfig, "headers"> & {
67
+ readonly headers: IncomingHttpHeaders
68
+ }
69
+
70
+ /**
71
+ * A Node.js duplex stream that parses multipart input.
72
+ *
73
+ * @category models
74
+ * @since 4.0.0
75
+ */
76
+ export class MultipartStream extends Duplex {
77
+ private _parser: Parser
78
+ _canWrite = true
79
+ private _writeCallback: (() => void) | undefined
80
+
81
+ constructor(config: NodeConfig) {
82
+ super({ readableObjectMode: true })
83
+ let currentError: MultipartError | undefined
84
+ let currentFile: FileStream | undefined
85
+ this._parser = makeParser({
86
+ ...(config as any),
87
+ onField: (info, value) => {
88
+ if (currentError !== undefined) return
89
+ const field: Field = { _tag: "Field", info, value }
90
+ this.push(field)
91
+ this.emit("field", field)
92
+ },
93
+ onFile: (info) => {
94
+ if (currentError !== undefined) return (_) => {}
95
+ const file = new FileStream(info, this)
96
+ currentFile = file
97
+ this.push(file)
98
+ this.emit("file", file)
99
+ return (chunk) => {
100
+ if (currentError !== undefined) return
101
+ this._canWrite = file.push(chunk)
102
+ if (chunk === null && !this._canWrite) {
103
+ currentFile = undefined
104
+ this._resume()
105
+ }
106
+ }
107
+ },
108
+ onError: (error) => {
109
+ this.emit("error", error)
110
+ currentFile?.emit("error", error)
111
+ currentError = error
112
+ },
113
+ onDone: () => {
114
+ this.push(null)
115
+ }
116
+ })
117
+ }
118
+
119
+ _resume() {
120
+ this._canWrite = true
121
+ if (this._writeCallback !== undefined) {
122
+ const callback = this._writeCallback
123
+ this._writeCallback = undefined
124
+ callback()
125
+ }
126
+ }
127
+
128
+ override _read(_size: number) {}
129
+
130
+ override _write(
131
+ chunk: any,
132
+ encoding: BufferEncoding,
133
+ callback: (error?: Error | null | undefined) => void
134
+ ): void {
135
+ this._parser.write(
136
+ chunk instanceof Uint8Array ? chunk : Buffer.from(chunk, encoding)
137
+ )
138
+ if (this._canWrite) {
139
+ callback()
140
+ } else {
141
+ this._writeCallback = callback
142
+ }
143
+ }
144
+
145
+ override _final(callback: (error?: Error | null | undefined) => void): void {
146
+ this._parser.end()
147
+ callback()
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Creates a Node.js multipart parser stream.
153
+ *
154
+ * @category constructors
155
+ * @since 4.0.0
156
+ */
157
+ export const make = (config: NodeConfig): MultipartStream => new MultipartStream(config)
158
+
159
+ /**
160
+ * A readable stream containing a parsed multipart file.
161
+ *
162
+ * @category models
163
+ * @since 4.0.0
164
+ */
165
+ export class FileStream extends Readable {
166
+ readonly _tag = "File"
167
+ readonly filename: string | undefined
168
+ readonly info: PartInfo
169
+ private _parent: MultipartStream
170
+ constructor(
171
+ info: PartInfo,
172
+ parent: MultipartStream
173
+ ) {
174
+ super()
175
+ this.info = info
176
+ this._parent = parent
177
+ this.filename = info.filename
178
+ }
179
+ override _read(_size: number) {
180
+ if (this._parent._canWrite === false) {
181
+ this._parent._resume()
182
+ }
183
+ }
184
+ }