@effect/platform-node 4.0.0-beta.7 → 4.0.0-beta.71

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 (123) hide show
  1. package/dist/Mime.d.ts +9 -3
  2. package/dist/Mime.d.ts.map +1 -1
  3. package/dist/Mime.js +9 -3
  4. package/dist/Mime.js.map +1 -1
  5. package/dist/NodeChildProcessSpawner.d.ts +1 -1
  6. package/dist/NodeChildProcessSpawner.js +1 -1
  7. package/dist/NodeClusterHttp.d.ts +49 -7
  8. package/dist/NodeClusterHttp.d.ts.map +1 -1
  9. package/dist/NodeClusterHttp.js +20 -10
  10. package/dist/NodeClusterHttp.js.map +1 -1
  11. package/dist/NodeClusterSocket.d.ts +57 -11
  12. package/dist/NodeClusterSocket.d.ts.map +1 -1
  13. package/dist/NodeClusterSocket.js +57 -11
  14. package/dist/NodeClusterSocket.js.map +1 -1
  15. package/dist/NodeCrypto.d.ts +10 -0
  16. package/dist/NodeCrypto.d.ts.map +1 -0
  17. package/dist/NodeCrypto.js +22 -0
  18. package/dist/NodeCrypto.js.map +1 -0
  19. package/dist/NodeFileSystem.d.ts +4 -2
  20. package/dist/NodeFileSystem.d.ts.map +1 -1
  21. package/dist/NodeFileSystem.js +30 -3
  22. package/dist/NodeFileSystem.js.map +1 -1
  23. package/dist/NodeHttpClient.d.ts +111 -24
  24. package/dist/NodeHttpClient.d.ts.map +1 -1
  25. package/dist/NodeHttpClient.js +133 -28
  26. package/dist/NodeHttpClient.js.map +1 -1
  27. package/dist/NodeHttpIncomingMessage.d.ts +43 -9
  28. package/dist/NodeHttpIncomingMessage.d.ts.map +1 -1
  29. package/dist/NodeHttpIncomingMessage.js +47 -8
  30. package/dist/NodeHttpIncomingMessage.js.map +1 -1
  31. package/dist/NodeHttpPlatform.d.ts +10 -4
  32. package/dist/NodeHttpPlatform.d.ts.map +1 -1
  33. package/dist/NodeHttpPlatform.js +34 -7
  34. package/dist/NodeHttpPlatform.js.map +1 -1
  35. package/dist/NodeHttpServer.d.ts +56 -16
  36. package/dist/NodeHttpServer.d.ts.map +1 -1
  37. package/dist/NodeHttpServer.js +117 -49
  38. package/dist/NodeHttpServer.js.map +1 -1
  39. package/dist/NodeHttpServerRequest.d.ts +29 -3
  40. package/dist/NodeHttpServerRequest.d.ts.map +1 -1
  41. package/dist/NodeHttpServerRequest.js +9 -2
  42. package/dist/NodeHttpServerRequest.js.map +1 -1
  43. package/dist/NodeMultipart.d.ts +47 -4
  44. package/dist/NodeMultipart.d.ts.map +1 -1
  45. package/dist/NodeMultipart.js +47 -4
  46. package/dist/NodeMultipart.js.map +1 -1
  47. package/dist/NodePath.d.ts +15 -6
  48. package/dist/NodePath.d.ts.map +1 -1
  49. package/dist/NodePath.js +43 -7
  50. package/dist/NodePath.js.map +1 -1
  51. package/dist/NodeRedis.d.ts +56 -9
  52. package/dist/NodeRedis.d.ts.map +1 -1
  53. package/dist/NodeRedis.js +59 -12
  54. package/dist/NodeRedis.js.map +1 -1
  55. package/dist/NodeRuntime.d.ts +27 -36
  56. package/dist/NodeRuntime.d.ts.map +1 -1
  57. package/dist/NodeRuntime.js +38 -13
  58. package/dist/NodeRuntime.js.map +1 -1
  59. package/dist/NodeServices.d.ts +44 -5
  60. package/dist/NodeServices.d.ts.map +1 -1
  61. package/dist/NodeServices.js +7 -3
  62. package/dist/NodeServices.js.map +1 -1
  63. package/dist/NodeSink.d.ts +2 -2
  64. package/dist/NodeSink.js +2 -2
  65. package/dist/NodeSocket.d.ts +18 -3
  66. package/dist/NodeSocket.d.ts.map +1 -1
  67. package/dist/NodeSocket.js +51 -4
  68. package/dist/NodeSocket.js.map +1 -1
  69. package/dist/NodeSocketServer.d.ts +2 -2
  70. package/dist/NodeSocketServer.js +2 -2
  71. package/dist/NodeStdio.d.ts +5 -2
  72. package/dist/NodeStdio.d.ts.map +1 -1
  73. package/dist/NodeStdio.js +30 -3
  74. package/dist/NodeStdio.js.map +1 -1
  75. package/dist/NodeStream.d.ts +2 -2
  76. package/dist/NodeStream.js +2 -2
  77. package/dist/NodeTerminal.d.ts +8 -2
  78. package/dist/NodeTerminal.d.ts.map +1 -1
  79. package/dist/NodeTerminal.js +29 -3
  80. package/dist/NodeTerminal.js.map +1 -1
  81. package/dist/NodeWorker.d.ts +9 -2
  82. package/dist/NodeWorker.d.ts.map +1 -1
  83. package/dist/NodeWorker.js +45 -6
  84. package/dist/NodeWorker.js.map +1 -1
  85. package/dist/NodeWorkerRunner.d.ts +5 -1
  86. package/dist/NodeWorkerRunner.d.ts.map +1 -1
  87. package/dist/NodeWorkerRunner.js +47 -5
  88. package/dist/NodeWorkerRunner.js.map +1 -1
  89. package/dist/Undici.d.ts +16 -3
  90. package/dist/Undici.d.ts.map +1 -1
  91. package/dist/Undici.js +16 -3
  92. package/dist/Undici.js.map +1 -1
  93. package/dist/index.d.ts +28 -26
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +28 -26
  96. package/dist/index.js.map +1 -1
  97. package/package.json +9 -9
  98. package/src/Mime.ts +10 -3
  99. package/src/NodeChildProcessSpawner.ts +1 -1
  100. package/src/NodeClusterHttp.ts +54 -11
  101. package/src/NodeClusterSocket.ts +57 -11
  102. package/src/NodeCrypto.ts +24 -0
  103. package/src/NodeFileSystem.ts +30 -3
  104. package/src/NodeHttpClient.ts +141 -33
  105. package/src/NodeHttpIncomingMessage.ts +55 -12
  106. package/src/NodeHttpPlatform.ts +35 -6
  107. package/src/NodeHttpServer.ts +139 -53
  108. package/src/NodeHttpServerRequest.ts +29 -3
  109. package/src/NodeMultipart.ts +47 -4
  110. package/src/NodePath.ts +43 -7
  111. package/src/NodeRedis.ts +61 -14
  112. package/src/NodeRuntime.ts +56 -37
  113. package/src/NodeServices.ts +46 -5
  114. package/src/NodeSink.ts +2 -2
  115. package/src/NodeSocket.ts +56 -4
  116. package/src/NodeSocketServer.ts +2 -2
  117. package/src/NodeStdio.ts +30 -3
  118. package/src/NodeStream.ts +2 -2
  119. package/src/NodeTerminal.ts +29 -3
  120. package/src/NodeWorker.ts +45 -6
  121. package/src/NodeWorkerRunner.ts +47 -5
  122. package/src/Undici.ts +16 -3
  123. package/src/index.ts +29 -26
@@ -1,12 +1,50 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Node.js implementations of the Effect `HttpClient`.
3
+ *
4
+ * This module supplies Node runtime backends for the platform-independent
5
+ * Effect HTTP client API. It re-exports the fetch-based client, defines an
6
+ * Undici-backed client, and defines a lower-level `node:http` / `node:https`
7
+ * client for integrations that need native agent configuration.
8
+ *
9
+ * **Mental model**
10
+ *
11
+ * All backends provide the same `HttpClient` service, so application code can
12
+ * depend on the Effect HTTP client interface while the layer chooses the Node
13
+ * implementation. The difference is where request execution and connection
14
+ * ownership live: fetch uses `globalThis.fetch`, Undici sends through a
15
+ * `Dispatcher`, and the `node:http` backend sends through scoped HTTP and HTTPS
16
+ * agents.
17
+ *
18
+ * **Common tasks**
19
+ *
20
+ * Use `layerFetch` when the built-in fetch implementation is enough. Use
21
+ * `layerUndici` for a scoped Undici `Agent`, or `layerUndiciNoDispatcher` when
22
+ * the caller provides the `Dispatcher` service, including the process-global
23
+ * dispatcher from `dispatcherLayerGlobal`. Use `layerNodeHttp` or
24
+ * `layerAgentOptions` when TLS, proxy, keep-alive, socket, or other native
25
+ * Node agent options must be configured directly.
26
+ *
27
+ * **Gotchas**
28
+ *
29
+ * Fetch, Undici, and `node:http` are not exact substitutes. They differ in
30
+ * dispatcher and agent hooks, request body support, abort behavior, upgrade
31
+ * support, and response body readers. Scoped layers destroy the agents or
32
+ * dispatchers they create when the layer scope ends; `dispatcherLayerGlobal`
33
+ * intentionally does not own or destroy Undici's process-global dispatcher.
34
+ * Body read failures are reported as `HttpClientError` decode or transport
35
+ * errors.
36
+ *
37
+ * @since 4.0.0
3
38
  */
39
+ import * as Context from "effect/Context"
4
40
  import * as Effect from "effect/Effect"
5
41
  import { flow } from "effect/Function"
6
42
  import * as Inspectable from "effect/Inspectable"
7
43
  import * as Layer from "effect/Layer"
44
+ import * as Option from "effect/Option"
45
+ import { type Pipeable, pipeArguments } from "effect/Pipeable"
46
+ import type * as Schema from "effect/Schema"
8
47
  import type * as Scope from "effect/Scope"
9
- import * as ServiceMap from "effect/ServiceMap"
10
48
  import * as Stream from "effect/Stream"
11
49
  import * as Cookies from "effect/unstable/http/Cookies"
12
50
  import * as Headers from "effect/unstable/http/Headers"
@@ -33,18 +71,24 @@ import * as Undici from "./Undici.ts"
33
71
 
34
72
  export {
35
73
  /**
36
- * @since 1.0.0
74
+ * Fetch-based HTTP client implementation for Node.js.
75
+ *
37
76
  * @category Fetch
77
+ * @since 4.0.0
38
78
  */
39
79
  Fetch,
40
80
  /**
41
- * @since 1.0.0
81
+ * Layer that provides the fetch-based HTTP client implementation.
82
+ *
42
83
  * @category Fetch
84
+ * @since 4.0.0
43
85
  */
44
86
  layer as layerFetch,
45
87
  /**
46
- * @since 1.0.0
88
+ * Request initialization options accepted by the fetch-based HTTP client.
89
+ *
47
90
  * @category Fetch
91
+ * @since 4.0.0
48
92
  */
49
93
  RequestInit
50
94
  } from "effect/unstable/http/FetchHttpClient"
@@ -54,16 +98,22 @@ export {
54
98
  // -----------------------------------------------------------------------------
55
99
 
56
100
  /**
57
- * @since 1.0.0
101
+ * Service tag for the Undici `Dispatcher` used by the Undici-backed HTTP
102
+ * client.
103
+ *
58
104
  * @category Dispatcher
105
+ * @since 4.0.0
59
106
  */
60
- export class Dispatcher extends ServiceMap.Service<Dispatcher, Undici.Dispatcher>()(
107
+ export class Dispatcher extends Context.Service<Dispatcher, Undici.Dispatcher>()(
61
108
  "@effect/platform-node/NodeHttpClient/Dispatcher"
62
109
  ) {}
63
110
 
64
111
  /**
65
- * @since 1.0.0
112
+ * Acquires a new Undici `Agent` dispatcher and destroys it when the enclosing
113
+ * scope is finalized.
114
+ *
66
115
  * @category Dispatcher
116
+ * @since 4.0.0
67
117
  */
68
118
  export const makeDispatcher: Effect.Effect<Undici.Dispatcher, never, Scope.Scope> = Effect.acquireRelease(
69
119
  Effect.sync(() => new Undici.Agent()),
@@ -71,29 +121,41 @@ export const makeDispatcher: Effect.Effect<Undici.Dispatcher, never, Scope.Scope
71
121
  )
72
122
 
73
123
  /**
74
- * @since 1.0.0
124
+ * Provides the `Dispatcher` service using a scoped Undici `Agent`.
125
+ *
75
126
  * @category Dispatcher
127
+ * @since 4.0.0
76
128
  */
77
129
  export const layerDispatcher: Layer.Layer<Dispatcher> = Layer.effect(Dispatcher)(makeDispatcher)
78
130
 
79
131
  /**
80
- * @since 1.0.0
132
+ * Provides the `Dispatcher` service from Undici's process-global dispatcher,
133
+ * without creating or owning a new agent.
134
+ *
81
135
  * @category Dispatcher
136
+ * @since 4.0.0
82
137
  */
83
138
  export const dispatcherLayerGlobal: Layer.Layer<Dispatcher> = Layer.sync(Dispatcher)(() => Undici.getGlobalDispatcher())
84
139
 
85
140
  /**
86
- * @since 1.0.0
141
+ * Fiber reference containing default Undici request options applied to requests
142
+ * sent by `makeUndici`.
143
+ *
87
144
  * @category undici
145
+ * @since 4.0.0
88
146
  */
89
- export const UndiciOptions = ServiceMap.Reference<Partial<Undici.Dispatcher.RequestOptions>>(
147
+ export const UndiciOptions = Context.Reference<Partial<Undici.Dispatcher.RequestOptions>>(
90
148
  "@effect/platform-node/NodeHttpClient/UndiciOptions",
91
149
  { defaultValue: () => ({}) }
92
150
  )
93
151
 
94
152
  /**
95
- * @since 1.0.0
153
+ * Creates an `HttpClient` that sends requests through the current Undici
154
+ * `Dispatcher`, converts Effect HTTP bodies to Undici bodies, and maps
155
+ * transport and decode failures to `HttpClientError`.
156
+ *
96
157
  * @category undici
158
+ * @since 4.0.0
97
159
  */
98
160
  export const makeUndici = Effect.gen(function*() {
99
161
  const dispatcher = yield* Dispatcher
@@ -150,7 +212,7 @@ function convertBody(
150
212
 
151
213
  function noopErrorHandler(_: any) {}
152
214
 
153
- class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
215
+ class UndiciResponse extends Inspectable.Class implements HttpClientResponse, Pipeable {
154
216
  readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId
155
217
  readonly [Response.TypeId]: typeof Response.TypeId
156
218
  readonly request: HttpClientRequest
@@ -189,8 +251,8 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
189
251
  return this.cachedCookies = header ? Cookies.fromSetCookie(header) : Cookies.empty
190
252
  }
191
253
 
192
- get remoteAddress(): string | undefined {
193
- return undefined
254
+ get remoteAddress(): Option.Option<string> {
255
+ return Option.none()
194
256
  }
195
257
 
196
258
  get stream(): Stream.Stream<Uint8Array, Error.HttpClientError> {
@@ -207,10 +269,10 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
207
269
  })
208
270
  }
209
271
 
210
- get json(): Effect.Effect<unknown, Error.HttpClientError> {
272
+ get json(): Effect.Effect<Schema.Json, Error.HttpClientError> {
211
273
  return Effect.flatMap(this.text, (text) =>
212
274
  Effect.try({
213
- try: () => text === "" ? null : JSON.parse(text) as unknown,
275
+ try: () => text === "" ? null : JSON.parse(text),
214
276
  catch: (cause) =>
215
277
  new Error.HttpClientError({
216
278
  reason: new Error.DecodeError({
@@ -224,7 +286,10 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
224
286
 
225
287
  private textBody?: Effect.Effect<string, Error.HttpClientError>
226
288
  get text(): Effect.Effect<string, Error.HttpClientError> {
227
- return this.textBody ??= Effect.tryPromise({
289
+ if (this.textBody) {
290
+ return this.textBody
291
+ }
292
+ this.textBody = Effect.tryPromise({
228
293
  try: () => this.source.body.text(),
229
294
  catch: (cause) =>
230
295
  new Error.HttpClientError({
@@ -235,6 +300,8 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
235
300
  })
236
301
  })
237
302
  }).pipe(Effect.cached, Effect.runSync)
303
+ this.arrayBufferBody = Effect.map(this.textBody, (_) => new TextEncoder().encode(_).buffer)
304
+ return this.textBody
238
305
  }
239
306
 
240
307
  get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, Error.HttpClientError> {
@@ -269,7 +336,10 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
269
336
 
270
337
  private arrayBufferBody?: Effect.Effect<ArrayBuffer, Error.HttpClientError>
271
338
  get arrayBuffer(): Effect.Effect<ArrayBuffer, Error.HttpClientError> {
272
- return this.arrayBufferBody ??= Effect.tryPromise({
339
+ if (this.arrayBufferBody) {
340
+ return this.arrayBufferBody
341
+ }
342
+ this.arrayBufferBody = Effect.tryPromise({
273
343
  try: () => this.source.body.arrayBuffer(),
274
344
  catch: (cause) =>
275
345
  new Error.HttpClientError({
@@ -280,6 +350,8 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
280
350
  })
281
351
  })
282
352
  }).pipe(Effect.cached, Effect.runSync)
353
+ this.textBody = Effect.map(this.arrayBufferBody, (_) => new TextDecoder().decode(_))
354
+ return this.arrayBufferBody
283
355
  }
284
356
 
285
357
  toJSON(): unknown {
@@ -289,21 +361,31 @@ class UndiciResponse extends Inspectable.Class implements HttpClientResponse {
289
361
  status: this.status
290
362
  })
291
363
  }
364
+
365
+ pipe() {
366
+ return pipeArguments(this, arguments)
367
+ }
292
368
  }
293
369
 
294
370
  /**
295
- * @since 1.0.0
371
+ * Provides an Undici-backed `HttpClient` using the current `Dispatcher`
372
+ * service.
373
+ *
296
374
  * @category Undici
375
+ * @since 4.0.0
297
376
  */
298
377
  export const layerUndiciNoDispatcher: Layer.Layer<
299
378
  Client.HttpClient,
300
379
  never,
301
380
  Dispatcher
302
- > = Client.layerMergedServices(makeUndici)
381
+ > = Client.layerMergedContext(makeUndici)
303
382
 
304
383
  /**
305
- * @since 1.0.0
384
+ * Provides an Undici-backed `HttpClient` together with a scoped default
385
+ * Undici `Agent` dispatcher.
386
+ *
306
387
  * @category Undici
388
+ * @since 4.0.0
307
389
  */
308
390
  export const layerUndici: Layer.Layer<Client.HttpClient> = Layer.provide(layerUndiciNoDispatcher, layerDispatcher)
309
391
 
@@ -312,17 +394,23 @@ export const layerUndici: Layer.Layer<Client.HttpClient> = Layer.provide(layerUn
312
394
  // -----------------------------------------------------------------------------
313
395
 
314
396
  /**
315
- * @since 1.0.0
397
+ * Service tag for the paired Node `http` and `https` agents used by the
398
+ * node:http-backed HTTP client.
399
+ *
316
400
  * @category HttpAgent
401
+ * @since 4.0.0
317
402
  */
318
- export class HttpAgent extends ServiceMap.Service<HttpAgent, {
403
+ export class HttpAgent extends Context.Service<HttpAgent, {
319
404
  readonly http: Http.Agent
320
405
  readonly https: Https.Agent
321
406
  }>()("@effect/platform-node/NodeHttpClient/HttpAgent") {}
322
407
 
323
408
  /**
324
- * @since 1.0.0
409
+ * Acquires Node `http` and `https` agents with the supplied options and
410
+ * destroys both agents when the enclosing scope is finalized.
411
+ *
325
412
  * @category HttpAgent
413
+ * @since 4.0.0
326
414
  */
327
415
  export const makeAgent = (options?: Https.AgentOptions): Effect.Effect<HttpAgent["Service"], never, Scope.Scope> =>
328
416
  Effect.zipWith(
@@ -338,22 +426,32 @@ export const makeAgent = (options?: Https.AgentOptions): Effect.Effect<HttpAgent
338
426
  )
339
427
 
340
428
  /**
341
- * @since 1.0.0
429
+ * Provides the `HttpAgent` service using scoped Node `http` and `https`
430
+ * agents configured with the supplied options.
431
+ *
342
432
  * @category HttpAgent
433
+ * @since 4.0.0
343
434
  */
344
435
  export const layerAgentOptions: (options?: Https.AgentOptions | undefined) => Layer.Layer<
345
436
  HttpAgent
346
437
  > = flow(makeAgent, Layer.effect(HttpAgent))
347
438
 
348
439
  /**
349
- * @since 1.0.0
440
+ * Provides the `HttpAgent` service using default scoped Node `http` and
441
+ * `https` agents.
442
+ *
350
443
  * @category HttpAgent
444
+ * @since 4.0.0
351
445
  */
352
446
  export const layerAgent: Layer.Layer<HttpAgent> = layerAgentOptions()
353
447
 
354
448
  /**
355
- * @since 1.0.0
449
+ * Creates an `HttpClient` backed by Node `http` and `https`, using the
450
+ * current `HttpAgent`, streaming request bodies, and wrapping Node responses
451
+ * as `HttpClientResponse` values.
452
+ *
356
453
  * @category node:http
454
+ * @since 4.0.0
357
455
  */
358
456
  export const makeNodeHttp = Effect.gen(function*() {
359
457
  const agent = yield* HttpAgent
@@ -490,7 +588,7 @@ const waitForFinish = (nodeRequest: Http.ClientRequest, request: HttpClientReque
490
588
  })
491
589
  })
492
590
 
493
- class NodeHttpResponse extends NodeHttpIncomingMessage<Error.HttpClientError> implements HttpClientResponse {
591
+ class NodeHttpResponse extends NodeHttpIncomingMessage<Error.HttpClientError> implements HttpClientResponse, Pipeable {
494
592
  readonly [Response.TypeId]: typeof Response.TypeId
495
593
  readonly request: HttpClientRequest
496
594
 
@@ -555,20 +653,30 @@ class NodeHttpResponse extends NodeHttpIncomingMessage<Error.HttpClientError> im
555
653
  status: this.status
556
654
  })
557
655
  }
656
+
657
+ pipe() {
658
+ return pipeArguments(this, arguments)
659
+ }
558
660
  }
559
661
 
560
662
  /**
561
- * @since 1.0.0
663
+ * Provides a node:http-backed `HttpClient` using the current `HttpAgent`
664
+ * service.
665
+ *
562
666
  * @category node:http
667
+ * @since 4.0.0
563
668
  */
564
669
  export const layerNodeHttpNoAgent: Layer.Layer<
565
670
  Client.HttpClient,
566
671
  never,
567
672
  HttpAgent
568
- > = Client.layerMergedServices(makeNodeHttp)
673
+ > = Client.layerMergedContext(makeNodeHttp)
569
674
 
570
675
  /**
571
- * @since 1.0.0
676
+ * Provides a node:http-backed `HttpClient` together with default scoped Node
677
+ * `http` and `https` agents.
678
+ *
572
679
  * @category node:http
680
+ * @since 4.0.0
573
681
  */
574
682
  export const layerNodeHttp: Layer.Layer<Client.HttpClient> = Layer.provide(layerNodeHttpNoAgent, layerAgent)
@@ -1,8 +1,35 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Adapter base for exposing Node `http.IncomingMessage` values as Effect HTTP
3
+ * incoming messages.
4
+ *
5
+ * Server requests and Node client responses both arrive as Node readable
6
+ * streams with raw header objects, socket metadata, and one-shot body
7
+ * consumption. This module's `NodeHttpIncomingMessage` class keeps the original
8
+ * Node message available while presenting Effect's `HttpIncomingMessage` shape:
9
+ * typed headers, remote address lookup, stream access, and text, JSON,
10
+ * URL-encoded, and array-buffer body readers.
11
+ *
12
+ * **Mental model**
13
+ *
14
+ * The Node message remains the source of truth. The adapter translates headers
15
+ * and remote address on demand, delegates raw streaming to `NodeStream`, and
16
+ * lets subclasses choose how unknown Node errors are mapped into their HTTP
17
+ * error type.
18
+ *
19
+ * **Gotchas**
20
+ *
21
+ * Node request and response bodies are one-shot streams. The `text` and
22
+ * `arrayBuffer` readers are cached and share decoded values with each other,
23
+ * but direct `stream` access is not cached and can consume the underlying Node
24
+ * stream before a decoder reads it. Body readers honor
25
+ * `HttpIncomingMessage.MaxBodySize`.
26
+ *
27
+ * @since 4.0.0
3
28
  */
4
29
  import * as Effect from "effect/Effect"
5
30
  import * as Inspectable from "effect/Inspectable"
31
+ import * as Option from "effect/Option"
32
+ import type * as Schema from "effect/Schema"
6
33
  import type * as Stream from "effect/Stream"
7
34
  import * as Headers from "effect/unstable/http/Headers"
8
35
  import * as IncomingMessage from "effect/unstable/http/HttpIncomingMessage"
@@ -11,24 +38,30 @@ import type * as Http from "node:http"
11
38
  import * as NodeStream from "./NodeStream.ts"
12
39
 
13
40
  /**
14
- * @since 1.0.0
15
- * @category Constructors
41
+ * Base adapter from Node `IncomingMessage` to Effect HTTP incoming messages,
42
+ * exposing headers, remote address, stream access, and cached text, JSON, URL
43
+ * parameter, and array-buffer body decoders with caller-provided error mapping.
44
+ *
45
+ * @category constructors
46
+ * @since 4.0.0
16
47
  */
17
48
  export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
18
49
  implements IncomingMessage.HttpIncomingMessage<E>
19
50
  {
20
51
  /**
21
- * @since 1.0.0
52
+ * Marks this value as an HTTP incoming message for runtime guards.
53
+ *
54
+ * @since 4.0.0
22
55
  */
23
56
  readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId
24
57
  readonly source: Http.IncomingMessage
25
58
  readonly onError: (error: unknown) => E
26
- readonly remoteAddressOverride?: string | undefined
59
+ readonly remoteAddressOverride?: Option.Option<string> | undefined
27
60
 
28
61
  constructor(
29
62
  source: Http.IncomingMessage,
30
63
  onError: (error: unknown) => E,
31
- remoteAddressOverride?: string
64
+ remoteAddressOverride?: Option.Option<string>
32
65
  ) {
33
66
  super()
34
67
  this[IncomingMessage.TypeId] = IncomingMessage.TypeId
@@ -42,7 +75,7 @@ export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
42
75
  }
43
76
 
44
77
  get remoteAddress() {
45
- return this.remoteAddressOverride ?? this.source.socket.remoteAddress
78
+ return this.remoteAddressOverride ?? Option.fromNullishOr(this.source.socket.remoteAddress)
46
79
  }
47
80
 
48
81
  private textEffect: Effect.Effect<string, E> | undefined
@@ -52,7 +85,7 @@ export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
52
85
  }
53
86
  this.textEffect = Effect.runSync(Effect.cached(
54
87
  Effect.flatMap(
55
- IncomingMessage.MaxBodySize.asEffect(),
88
+ IncomingMessage.MaxBodySize,
56
89
  (maxBodySize) =>
57
90
  NodeStream.toString(() => this.source, {
58
91
  onError: this.onError,
@@ -60,6 +93,7 @@ export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
60
93
  })
61
94
  )
62
95
  ))
96
+ this.arrayBufferEffect = Effect.map(this.textEffect, (_) => new TextEncoder().encode(_).buffer)
63
97
  return this.textEffect
64
98
  }
65
99
 
@@ -67,15 +101,15 @@ export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
67
101
  return Effect.runSync(this.text)
68
102
  }
69
103
 
70
- get json(): Effect.Effect<unknown, E> {
104
+ get json(): Effect.Effect<Schema.Json, E> {
71
105
  return Effect.flatMap(this.text, (text) =>
72
106
  Effect.try({
73
- try: () => text === "" ? null : JSON.parse(text) as unknown,
107
+ try: () => text === "" ? null : JSON.parse(text),
74
108
  catch: this.onError
75
109
  }))
76
110
  }
77
111
 
78
- get jsonUnsafe(): unknown {
112
+ get jsonUnsafe(): Schema.Json {
79
113
  return Effect.runSync(this.json)
80
114
  }
81
115
 
@@ -94,12 +128,21 @@ export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
94
128
  })
95
129
  }
96
130
 
131
+ private arrayBufferEffect: Effect.Effect<ArrayBuffer, E> | undefined
97
132
  get arrayBuffer(): Effect.Effect<ArrayBuffer, E> {
98
- return Effect.withFiber((fiber) =>
133
+ if (this.arrayBufferEffect) {
134
+ return this.arrayBufferEffect
135
+ }
136
+ this.arrayBufferEffect = Effect.withFiber((fiber) =>
99
137
  NodeStream.toArrayBuffer(() => this.source, {
100
138
  onError: this.onError,
101
139
  maxBytes: fiber.getRef(IncomingMessage.MaxBodySize)
102
140
  })
141
+ ).pipe(
142
+ Effect.cached,
143
+ Effect.runSync
103
144
  )
145
+ this.textEffect = Effect.map(this.arrayBufferEffect, (_) => new TextDecoder().decode(_))
146
+ return this.arrayBufferEffect
104
147
  }
105
148
  }
@@ -1,5 +1,26 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Node.js implementation of the Effect HTTP platform service.
3
+ *
4
+ * This module connects the portable `HttpPlatform` file response helpers to
5
+ * Node runtime primitives. It is used by Node HTTP servers and static file
6
+ * handlers when returning local files, public assets, downloads, byte ranges,
7
+ * or Web `File` values as `HttpServerResponse` bodies.
8
+ *
9
+ * Path-based responses are served with `node:fs.createReadStream`; Web `File`
10
+ * responses are bridged with `Readable.fromWeb`. The implementation fills in
11
+ * `content-type` from `Mime`, falls back to `application/octet-stream`, and
12
+ * writes the `content-length` for the selected range or whole file. Node's
13
+ * stream `end` option is inclusive, so the platform converts Effect's half-open
14
+ * range before reading. Empty bodies use an empty readable stream.
15
+ *
16
+ * Provide `layer` at the Node runtime edge when file responses, static serving,
17
+ * or response bodies created from files need real filesystem and ETag support.
18
+ * These responses are raw Node streams, so they are intended for the Node HTTP
19
+ * server adapter; keep files available until the response body has been
20
+ * consumed and prefer the portable `HttpServerResponse` constructors when a
21
+ * response does not depend on Node file or stream behavior.
22
+ *
23
+ * @since 4.0.0
3
24
  */
4
25
  import { pipe } from "effect/Function"
5
26
  import * as Layer from "effect/Layer"
@@ -13,12 +34,17 @@ import Mime from "./Mime.ts"
13
34
  import * as NodeFileSystem from "./NodeFileSystem.ts"
14
35
 
15
36
  /**
16
- * @since 1.0.0
17
- * @category Constructors
37
+ * Creates the Node `HttpPlatform`, serving file responses from Node readable
38
+ * streams and adding MIME type and content-length headers when needed.
39
+ *
40
+ * @category constructors
41
+ * @since 4.0.0
18
42
  */
19
43
  export const make = Platform.make({
20
44
  fileResponse(path, status, statusText, headers, start, end, contentLength) {
21
- const stream = Fs.createReadStream(path, { start, end })
45
+ const stream = contentLength === 0
46
+ ? Readable.from([])
47
+ : Fs.createReadStream(path, { start, end: end === undefined ? undefined : end - 1 })
22
48
  return ServerResponse.raw(stream, {
23
49
  headers: {
24
50
  ...headers,
@@ -45,8 +71,11 @@ export const make = Platform.make({
45
71
  })
46
72
 
47
73
  /**
48
- * @since 1.0.0
49
- * @category Layers
74
+ * Provides the Node `HttpPlatform` together with the filesystem and ETag
75
+ * services it needs for file responses.
76
+ *
77
+ * @category layers
78
+ * @since 4.0.0
50
79
  */
51
80
  export const layer: Layer.Layer<Platform.HttpPlatform> = pipe(
52
81
  Layer.effect(Platform.HttpPlatform)(make),