Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Adapter base for exposing Node `http.IncomingMessage` values as Effect HTTP
|
||||
* incoming messages.
|
||||
*
|
||||
* Server requests and Node client responses both arrive as Node readable
|
||||
* streams with raw header objects, socket metadata, and one-shot body
|
||||
* consumption. This module's `NodeHttpIncomingMessage` class keeps the original
|
||||
* Node message available while presenting Effect's `HttpIncomingMessage` shape:
|
||||
* typed headers, remote address lookup, stream access, and text, JSON,
|
||||
* URL-encoded, and array-buffer body readers.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Inspectable from "effect/Inspectable"
|
||||
import * as Option from "effect/Option"
|
||||
import type * as Schema from "effect/Schema"
|
||||
import type * as Stream from "effect/Stream"
|
||||
import * as Headers from "effect/unstable/http/Headers"
|
||||
import * as IncomingMessage from "effect/unstable/http/HttpIncomingMessage"
|
||||
import * as UrlParams from "effect/unstable/http/UrlParams"
|
||||
import type * as Http from "node:http"
|
||||
import * as NodeStream from "./NodeStream.ts"
|
||||
|
||||
/**
|
||||
* Adapts a Node `IncomingMessage` to Effect HTTP incoming messages.
|
||||
*
|
||||
* **When to use**
|
||||
*
|
||||
* Use to implement Node HTTP request or response adapters that expose the
|
||||
* Effect HTTP incoming-message interface.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* The adapter exposes headers, remote address, stream access, and cached body
|
||||
* decoders. Subclasses provide the error mapping for unknown Node errors.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
|
||||
implements IncomingMessage.HttpIncomingMessage<E>
|
||||
{
|
||||
/**
|
||||
* Marks this value as an HTTP incoming message for runtime guards.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId
|
||||
readonly source: Http.IncomingMessage
|
||||
readonly onError: (error: unknown) => E
|
||||
readonly remoteAddressOverride?: Option.Option<string> | undefined
|
||||
|
||||
constructor(
|
||||
source: Http.IncomingMessage,
|
||||
onError: (error: unknown) => E,
|
||||
remoteAddressOverride?: Option.Option<string>
|
||||
) {
|
||||
super()
|
||||
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
|
||||
this.source = source
|
||||
this.onError = onError
|
||||
this.remoteAddressOverride = remoteAddressOverride
|
||||
}
|
||||
|
||||
get headers() {
|
||||
return Headers.fromInput(this.source.headers as any)
|
||||
}
|
||||
|
||||
get remoteAddress() {
|
||||
return this.remoteAddressOverride ?? Option.fromNullishOr(this.source.socket.remoteAddress)
|
||||
}
|
||||
|
||||
private textEffect: Effect.Effect<string, E> | undefined
|
||||
get text(): Effect.Effect<string, E> {
|
||||
if (this.textEffect) {
|
||||
return this.textEffect
|
||||
}
|
||||
this.textEffect = Effect.runSync(Effect.cached(
|
||||
Effect.flatMap(
|
||||
IncomingMessage.MaxBodySize,
|
||||
(maxBodySize) =>
|
||||
NodeStream.toString(() => this.source, {
|
||||
onError: this.onError,
|
||||
maxBytes: maxBodySize
|
||||
})
|
||||
)
|
||||
))
|
||||
this.arrayBufferEffect = Effect.map(this.textEffect, (_) => new TextEncoder().encode(_).buffer)
|
||||
return this.textEffect
|
||||
}
|
||||
|
||||
get textUnsafe(): string {
|
||||
return Effect.runSync(this.text)
|
||||
}
|
||||
|
||||
get json(): Effect.Effect<Schema.Json, E> {
|
||||
return Effect.flatMap(this.text, (text) =>
|
||||
Effect.try({
|
||||
try: () => text === "" ? null : JSON.parse(text),
|
||||
catch: this.onError
|
||||
}))
|
||||
}
|
||||
|
||||
get jsonUnsafe(): Schema.Json {
|
||||
return Effect.runSync(this.json)
|
||||
}
|
||||
|
||||
get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, E> {
|
||||
return Effect.flatMap(this.text, (_) =>
|
||||
Effect.try({
|
||||
try: () => UrlParams.fromInput(new URLSearchParams(_)),
|
||||
catch: this.onError
|
||||
}))
|
||||
}
|
||||
|
||||
get stream(): Stream.Stream<Uint8Array, E> {
|
||||
return NodeStream.fromReadable({
|
||||
evaluate: () => this.source,
|
||||
onError: this.onError
|
||||
})
|
||||
}
|
||||
|
||||
private arrayBufferEffect: Effect.Effect<ArrayBuffer, E> | undefined
|
||||
get arrayBuffer(): Effect.Effect<ArrayBuffer, E> {
|
||||
if (this.arrayBufferEffect) {
|
||||
return this.arrayBufferEffect
|
||||
}
|
||||
this.arrayBufferEffect = Effect.withFiber((fiber) =>
|
||||
NodeStream.toArrayBuffer(() => this.source, {
|
||||
onError: this.onError,
|
||||
maxBytes: fiber.getRef(IncomingMessage.MaxBodySize)
|
||||
})
|
||||
).pipe(
|
||||
Effect.cached,
|
||||
Effect.runSync
|
||||
)
|
||||
this.textEffect = Effect.map(this.arrayBufferEffect, (_) => new TextDecoder().decode(_))
|
||||
return this.arrayBufferEffect
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user