Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'

This commit is contained in:
-Puter
2026-07-19 03:28:54 +05:30
parent 2daf979036
commit a37e0cc3c9
2163 changed files with 668421 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
/**
* Re-exports the `mime` package through the `@effect/platform-node/Mime`
* module and the `Mime` namespace in the package barrel.
*
* @since 4.0.0
*/
/* oxlint-disable no-named-as-default */
/**
* @since 4.0.0
*/
import Mime from "mime"
/**
* @category re-exports
* @since 4.0.0
*/
export * from "mime"
/**
* @category re-exports
* @since 4.0.0
*/
export default Mime

View File

@@ -0,0 +1,6 @@
/**
* Node.js implementation of `ChildProcessSpawner`.
*
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeChildProcessSpawner"

View File

@@ -0,0 +1,155 @@
/**
* Node.js HTTP and WebSocket layers for Effect Cluster runners.
*
* The main `layer` builds a sharding layer for HTTP or WebSocket transport,
* choosing serialization, runner health checks, runner storage, message
* storage, and optional client-only mode from the supplied options.
* `layerHttpServer` provides the Node HTTP server used by cluster runners, and
* this module re-exports the Kubernetes HTTP client layer used by runner health
* checks.
*
* @since 4.0.0
*/
import type * as Config from "effect/Config"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as HttpRunner from "effect/unstable/cluster/HttpRunner"
import * as MessageStorage from "effect/unstable/cluster/MessageStorage"
import * as RunnerHealth from "effect/unstable/cluster/RunnerHealth"
import * as Runners from "effect/unstable/cluster/Runners"
import * as RunnerStorage from "effect/unstable/cluster/RunnerStorage"
import type { Sharding } from "effect/unstable/cluster/Sharding"
import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig"
import * as SqlMessageStorage from "effect/unstable/cluster/SqlMessageStorage"
import * as SqlRunnerStorage from "effect/unstable/cluster/SqlRunnerStorage"
import type * as Etag from "effect/unstable/http/Etag"
import type { HttpPlatform } from "effect/unstable/http/HttpPlatform"
import type { HttpServer } from "effect/unstable/http/HttpServer"
import type { ServeError } from "effect/unstable/http/HttpServerError"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import type { SqlClient } from "effect/unstable/sql/SqlClient"
import { createServer } from "node:http"
import { layerK8sHttpClient } from "./NodeClusterSocket.ts"
import * as NodeHttpClient from "./NodeHttpClient.ts"
import * as NodeHttpServer from "./NodeHttpServer.ts"
import type { NodeServices } from "./NodeServices.ts"
import * as NodeSocket from "./NodeSocket.ts"
export {
/**
* Provides the Kubernetes HTTP client layer used by Kubernetes runner health checks.
*
* @category re-exports
* @since 4.0.0
*/
layerK8sHttpClient
} from "./NodeClusterSocket.ts"
/**
* Builds the Node cluster HTTP/WebSocket sharding layer, configuring runner
* transport, RPC serialization, message storage, runner health checks, and
* optional client-only mode.
*
* @category layers
* @since 4.0.0
*/
export const layer = <
const ClientOnly extends boolean = false,
const Storage extends "local" | "sql" | "byo" = never
>(options: {
readonly transport: "http" | "websocket"
readonly serialization?: "msgpack" | "ndjson" | undefined
readonly clientOnly?: ClientOnly | undefined
readonly storage?: Storage | undefined
readonly runnerHealth?: "ping" | "k8s" | undefined
readonly runnerHealthK8s?: {
readonly namespace?: string | undefined
readonly labelSelector?: string | undefined
} | undefined
readonly shardingConfig?: Partial<ShardingConfig.ShardingConfig["Service"]> | undefined
}): ClientOnly extends true ? Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
Config.ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> :
Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
ServeError | Config.ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> =>
{
const layer: Layer.Layer<any, any, any> = options.clientOnly
// client only
? options.transport === "http"
? Layer.provide(HttpRunner.layerHttpClientOnly, NodeHttpClient.layerUndici)
: Layer.provide(HttpRunner.layerWebsocketClientOnly, NodeSocket.layerWebSocketConstructor)
// with server
: options.transport === "http"
? Layer.provide(HttpRunner.layerHttp, [layerHttpServer, NodeHttpClient.layerUndici])
: Layer.provide(HttpRunner.layerWebsocket, [layerHttpServer, NodeSocket.layerWebSocketConstructor])
const runnerHealth: Layer.Layer<any, any, any> = options?.clientOnly
? Layer.empty as any
: options?.runnerHealth === "k8s"
? RunnerHealth.layerK8s(options.runnerHealthK8s).pipe(
Layer.provide(layerK8sHttpClient)
)
: RunnerHealth.layerPing.pipe(
Layer.provide(Runners.layerRpc),
Layer.provide(
options.transport === "http"
? HttpRunner.layerClientProtocolHttpDefault.pipe(Layer.provide(NodeHttpClient.layerUndici))
: HttpRunner.layerClientProtocolWebsocketDefault.pipe(Layer.provide(NodeSocket.layerWebSocketConstructor))
)
)
return layer.pipe(
Layer.provide(runnerHealth),
Layer.provideMerge(
options?.storage === "local"
? MessageStorage.layerNoop
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlMessageStorage.layer)
),
Layer.provide(
options?.storage === "local"
? RunnerStorage.layerMemory
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlRunnerStorage.layer)
),
Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)),
Layer.provide(
options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack
)
) as any
}
/**
* Provides the HTTP server and Node HTTP services used by cluster runners,
* listening on `ShardingConfig.runnerListenAddress` or `runnerAddress`.
*
* @category layers
* @since 4.0.0
*/
export const layerHttpServer: Layer.Layer<
| HttpPlatform
| Etag.Generator
| NodeServices
| HttpServer,
ServeError,
ShardingConfig.ShardingConfig
> = Effect.gen(function*() {
const config = yield* ShardingConfig.ShardingConfig
const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress)
if (Option.isNone(listenAddress)) {
return yield* Effect.die("NodeClusterHttp.layerHttpServer: ShardingConfig.runnerAddress is None")
}
return NodeHttpServer.layer(createServer, listenAddress.value)
}).pipe(Layer.unwrap)

View File

@@ -0,0 +1,176 @@
/**
* Node.js socket layers for Effect Cluster runners.
*
* The main `layer` builds a sharding layer for socket transport, choosing
* serialization, runner health checks, runner storage, message storage, and
* optional client-only mode from the supplied options. This module also
* re-exports the shared socket client and server protocol layers, and provides
* Kubernetes-aware Undici dispatcher and HTTP client layers for runner health
* checks.
*
* @since 4.0.0
*/
import { layerClientProtocol, layerSocketServer } from "@effect/platform-node-shared/NodeClusterSocket"
import type { ConfigError } from "effect/Config"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as K8sHttpClient from "effect/unstable/cluster/K8sHttpClient"
import * as MessageStorage from "effect/unstable/cluster/MessageStorage"
import * as RunnerHealth from "effect/unstable/cluster/RunnerHealth"
import * as Runners from "effect/unstable/cluster/Runners"
import * as RunnerStorage from "effect/unstable/cluster/RunnerStorage"
import type { Sharding } from "effect/unstable/cluster/Sharding"
import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig"
import * as SocketRunner from "effect/unstable/cluster/SocketRunner"
import * as SqlMessageStorage from "effect/unstable/cluster/SqlMessageStorage"
import * as SqlRunnerStorage from "effect/unstable/cluster/SqlRunnerStorage"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import type * as SocketServer from "effect/unstable/socket/SocketServer"
import type { SqlClient } from "effect/unstable/sql/SqlClient"
import * as NodeFileSystem from "./NodeFileSystem.ts"
import * as NodeHttpClient from "./NodeHttpClient.ts"
import * as Undici from "./Undici.ts"
export {
/**
* Provides the cluster `RpcClientProtocol` using the shared socket client
* implementation.
*
* @category re-exports
* @since 4.0.0
*/
layerClientProtocol,
/**
* Provides the socket server used by Node cluster runners through the shared
* socket server implementation.
*
* @category re-exports
* @since 4.0.0
*/
layerSocketServer
}
/**
* Builds the Node cluster socket sharding layer, configuring RPC
* serialization, message storage, runner health checks, and optional
* client-only mode.
*
* @category layers
* @since 4.0.0
*/
export const layer = <
const ClientOnly extends boolean = false,
const Storage extends "local" | "sql" | "byo" = never
>(
options?: {
readonly serialization?: "msgpack" | "ndjson" | undefined
readonly clientOnly?: ClientOnly | undefined
readonly storage?: Storage | undefined
readonly runnerHealth?: "ping" | "k8s" | undefined
readonly runnerHealthK8s?: {
readonly namespace?: string | undefined
readonly labelSelector?: string | undefined
} | undefined
readonly shardingConfig?: Partial<ShardingConfig.ShardingConfig["Service"]> | undefined
}
): ClientOnly extends true ? Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> :
Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
SocketServer.SocketServerError | ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> =>
{
const layer: Layer.Layer<any, any, any> = options?.clientOnly
// client only
? Layer.provide(SocketRunner.layerClientOnly, layerClientProtocol)
// with server
: Layer.provide(SocketRunner.layer, [layerSocketServer, layerClientProtocol])
const runnerHealth: Layer.Layer<any, any, any> = options?.clientOnly
? Layer.empty as any
: options?.runnerHealth === "k8s"
? RunnerHealth.layerK8s(options.runnerHealthK8s).pipe(
Layer.provide(layerK8sHttpClient)
)
: RunnerHealth.layerPing.pipe(
Layer.provide(Runners.layerRpc),
Layer.provide(layerClientProtocol)
)
return layer.pipe(
Layer.provide(runnerHealth),
Layer.provideMerge(
options?.storage === "local"
? MessageStorage.layerNoop
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlMessageStorage.layer)
),
Layer.provide(
options?.storage === "local"
? RunnerStorage.layerMemory
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlRunnerStorage.layer)
),
Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)),
Layer.provide(
options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack
)
) as any
}
/**
* Provides an Undici dispatcher for Kubernetes API calls, using the service
* account CA certificate when it is available and falling back to the default
* dispatcher otherwise.
*
* @category layers
* @since 4.0.0
*/
export const layerDispatcherK8s: Layer.Layer<NodeHttpClient.Dispatcher> = Layer.effect(NodeHttpClient.Dispatcher)(
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const caCertOption = yield* fs.readFileString("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt").pipe(
Effect.option
)
if (caCertOption._tag === "Some") {
return yield* Effect.acquireRelease(
Effect.sync(() =>
new Undici.Agent({
connect: {
ca: caCertOption.value
}
})
),
(agent) => Effect.promise(() => agent.destroy())
)
}
return yield* NodeHttpClient.makeDispatcher
})
).pipe(
Layer.provide(NodeFileSystem.layer)
)
/**
* Provides a `K8sHttpClient` backed by the Undici HTTP client and the
* Kubernetes-aware dispatcher.
*
* @category layers
* @since 4.0.0
*/
export const layerK8sHttpClient: Layer.Layer<K8sHttpClient.K8sHttpClient> = K8sHttpClient.layer.pipe(
Layer.provide(Layer.fresh(NodeHttpClient.layerUndiciNoDispatcher)),
Layer.provide(layerDispatcherK8s),
Layer.provide(NodeFileSystem.layer)
)

View File

@@ -0,0 +1,24 @@
/**
* The `NodeCrypto` module provides the Node.js `Crypto` service layer for
* Effect programs. Provide {@link layer} at the edge of a Node application,
* CLI, script, or test to satisfy `effect/Crypto` with Node's `node:crypto`
* implementation for secure random bytes, UUID generation, random values, and
* SHA digest operations.
*
* This module is the public Node adapter around the shared Node-compatible
* implementation. Digest failures are reported as platform errors, and SHA-1
* remains available only for interoperability with existing protocols.
*
* @since 1.0.0
*/
import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
import type * as Crypto from "effect/Crypto"
import type * as Layer from "effect/Layer"
/**
* Layer that provides the Node.js Crypto service implementation.
*
* @category layers
* @since 1.0.0
*/
export const layer: Layer.Layer<Crypto.Crypto> = NodeCrypto.layer

View File

@@ -0,0 +1,21 @@
/**
* Node.js `FileSystem` layer for programs that perform real filesystem I/O.
*
* The exported layer satisfies the platform-independent `FileSystem` service
* with Node-backed operations for files, directories, metadata, permissions,
* links, temporary paths, and path watching. Effects still call the service from
* `effect/FileSystem`; this module only chooses the Node implementation.
*
* @since 4.0.0
*/
import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem"
import type { FileSystem } from "effect/FileSystem"
import type * as Layer from "effect/Layer"
/**
* Provides the `FileSystem` service backed by Node filesystem APIs.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<FileSystem> = NodeFileSystem.layer

View File

@@ -0,0 +1,664 @@
/**
* Node.js implementations of the Effect `HttpClient`.
*
* This module supplies Node runtime backends for the platform-independent
* Effect HTTP client API. It re-exports the fetch-based `Fetch`, `RequestInit`,
* and `layerFetch` APIs, defines an Undici-backed client with dispatcher
* services and request options, and defines a lower-level `node:http` /
* `node:https` client with scoped HTTP agent layers.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import { flow } from "effect/Function"
import * as Inspectable from "effect/Inspectable"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import { type Pipeable, pipeArguments } from "effect/Pipeable"
import type * as Schema from "effect/Schema"
import type * as Scope from "effect/Scope"
import * as Stream from "effect/Stream"
import * as Cookies from "effect/unstable/http/Cookies"
import * as Headers from "effect/unstable/http/Headers"
import type * as Body from "effect/unstable/http/HttpBody"
import * as Client from "effect/unstable/http/HttpClient"
import * as Error from "effect/unstable/http/HttpClientError"
import type { HttpClientRequest } from "effect/unstable/http/HttpClientRequest"
import * as Response from "effect/unstable/http/HttpClientResponse"
import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"
import * as IncomingMessage from "effect/unstable/http/HttpIncomingMessage"
import * as UrlParams from "effect/unstable/http/UrlParams"
import * as Http from "node:http"
import * as Https from "node:https"
import { Readable } from "node:stream"
import { pipeline } from "node:stream/promises"
import { NodeHttpIncomingMessage } from "./NodeHttpIncomingMessage.ts"
import * as NodeSink from "./NodeSink.ts"
import * as NodeStream from "./NodeStream.ts"
import * as Undici from "./Undici.ts"
// -----------------------------------------------------------------------------
// Fetch
// -----------------------------------------------------------------------------
export {
/**
* Provides a fetch-based HTTP client implementation for Node.js.
*
* **When to use**
*
* Use to access or override the fetch implementation used by the Node
* fetch-based HTTP client.
*
* @category fetch
* @since 4.0.0
*/
Fetch,
/**
* Layer that provides the fetch-based HTTP client implementation.
*
* @category fetch
* @since 4.0.0
*/
layer as layerFetch,
/**
* Provides request initialization options accepted by the fetch-based HTTP client.
*
* **When to use**
*
* Use to provide default fetch request options for Node HTTP requests.
*
* @category fetch
* @since 4.0.0
*/
RequestInit
} from "effect/unstable/http/FetchHttpClient"
// -----------------------------------------------------------------------------
// Undici
// -----------------------------------------------------------------------------
/**
* Service tag for the Undici `Dispatcher` used by the Undici-backed HTTP
* client.
*
* @category Dispatcher
* @since 4.0.0
*/
export class Dispatcher extends Context.Service<Dispatcher, Undici.Dispatcher>()(
"@effect/platform-node/NodeHttpClient/Dispatcher"
) {}
/**
* Acquires a new Undici `Agent` dispatcher and destroys it when the enclosing
* scope is finalized.
*
* @category Dispatcher
* @since 4.0.0
*/
export const makeDispatcher: Effect.Effect<Undici.Dispatcher, never, Scope.Scope> = Effect.acquireRelease(
Effect.sync(() => new Undici.Agent()),
(dispatcher) => Effect.promise(() => dispatcher.destroy())
)
/**
* Provides the `Dispatcher` service using a scoped Undici `Agent`.
*
* @category Dispatcher
* @since 4.0.0
*/
export const layerDispatcher: Layer.Layer<Dispatcher> = Layer.effect(Dispatcher)(makeDispatcher)
/**
* Provides the `Dispatcher` service from Undici's process-global dispatcher,
* without creating or owning a new agent.
*
* @category Dispatcher
* @since 4.0.0
*/
export const dispatcherLayerGlobal: Layer.Layer<Dispatcher> = Layer.sync(Dispatcher)(() => Undici.getGlobalDispatcher())
/**
* Fiber reference containing default Undici request options applied to requests
* sent by `makeUndici`.
*
* @category Undici
* @since 4.0.0
*/
export const UndiciOptions = Context.Reference<Partial<Undici.Dispatcher.RequestOptions>>(
"@effect/platform-node/NodeHttpClient/UndiciOptions",
{ defaultValue: () => ({}) }
)
/**
* Creates an `HttpClient` that sends requests through the current Undici
* `Dispatcher`, converts Effect HTTP bodies to Undici bodies, and maps
* transport and decode failures to `HttpClientError`.
*
* @category Undici
* @since 4.0.0
*/
export const makeUndici = Effect.gen(function*() {
const dispatcher = yield* Dispatcher
return Client.make((request, url, signal, fiber) =>
convertBody(request.body).pipe(
Effect.flatMap((body) =>
Effect.tryPromise({
try: () =>
dispatcher.request({
...fiber.getRef(UndiciOptions),
signal,
method: request.method,
headers: request.headers,
origin: url.origin,
path: url.pathname + url.search + url.hash,
body,
// leave timeouts to Effect.timeout etc
headersTimeout: 60 * 60 * 1000,
bodyTimeout: 0
}),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
})
),
Effect.map((response) => new UndiciResponse(request, response))
)
)
})
function convertBody(
body: Body.HttpBody
): Effect.Effect<Exclude<Undici.Dispatcher.DispatchOptions["body"], undefined>> {
switch (body._tag) {
case "Empty": {
return Effect.succeed(null)
}
case "Uint8Array":
case "Raw": {
return Effect.succeed(body.body as Uint8Array)
}
case "FormData": {
return Effect.succeed(body.formData as any)
}
case "Stream": {
return NodeStream.toReadable(body.stream)
}
}
}
function noopErrorHandler(_: any) {}
class UndiciResponse extends Inspectable.Class implements HttpClientResponse, Pipeable {
readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId
readonly [Response.TypeId]: typeof Response.TypeId
readonly request: HttpClientRequest
readonly source: Undici.Dispatcher.ResponseData
constructor(
request: HttpClientRequest,
source: Undici.Dispatcher.ResponseData
) {
super()
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
this[Response.TypeId] = Response.TypeId
this.request = request
this.source = source
source.body.on("error", noopErrorHandler)
}
get status() {
return this.source.statusCode!
}
get statusText() {
return undefined
}
get headers(): Headers.Headers {
return Headers.fromInput(this.source.headers)
}
cachedCookies?: Cookies.Cookies
get cookies(): Cookies.Cookies {
if (this.cachedCookies !== undefined) {
return this.cachedCookies
}
const header = this.source.headers["set-cookie"]
return this.cachedCookies = header ? Cookies.fromSetCookie(header) : Cookies.empty
}
get remoteAddress(): Option.Option<string> {
return Option.none()
}
get stream(): Stream.Stream<Uint8Array, Error.HttpClientError> {
return NodeStream.fromReadable({
evaluate: () => this.source.body,
onError: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
})
}
get json(): Effect.Effect<Schema.Json, Error.HttpClientError> {
return Effect.flatMap(this.text, (text) =>
Effect.try({
try: () => text === "" ? null : JSON.parse(text),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}))
}
private textBody?: Effect.Effect<string, Error.HttpClientError>
get text(): Effect.Effect<string, Error.HttpClientError> {
if (this.textBody) {
return this.textBody
}
this.textBody = Effect.tryPromise({
try: () => this.source.body.text(),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}).pipe(Effect.cached, Effect.runSync)
this.arrayBufferBody = Effect.map(this.textBody, (_) => new TextEncoder().encode(_).buffer)
return this.textBody
}
get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, Error.HttpClientError> {
return Effect.flatMap(this.text, (_) =>
Effect.try({
try: () => UrlParams.fromInput(new URLSearchParams(_)),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}))
}
private formDataBody?: Effect.Effect<FormData, Error.HttpClientError>
get formData(): Effect.Effect<FormData, Error.HttpClientError> {
return this.formDataBody ??= Effect.tryPromise({
try: () => this.source.body.formData() as Promise<FormData>,
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}).pipe(Effect.cached, Effect.runSync)
}
private arrayBufferBody?: Effect.Effect<ArrayBuffer, Error.HttpClientError>
get arrayBuffer(): Effect.Effect<ArrayBuffer, Error.HttpClientError> {
if (this.arrayBufferBody) {
return this.arrayBufferBody
}
this.arrayBufferBody = Effect.tryPromise({
try: () => this.source.body.arrayBuffer(),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}).pipe(Effect.cached, Effect.runSync)
this.textBody = Effect.map(this.arrayBufferBody, (_) => new TextDecoder().decode(_))
return this.arrayBufferBody
}
toJSON(): unknown {
return IncomingMessage.inspect(this, {
_id: "effect/http/HttpClientResponse",
request: this.request.toJSON(),
status: this.status
})
}
pipe() {
return pipeArguments(this, arguments)
}
}
/**
* Provides an Undici-backed `HttpClient` using the current `Dispatcher`
* service.
*
* @category Undici
* @since 4.0.0
*/
export const layerUndiciNoDispatcher: Layer.Layer<
Client.HttpClient,
never,
Dispatcher
> = Client.layerMergedContext(makeUndici)
/**
* Provides an Undici-backed `HttpClient` together with a scoped default
* Undici `Agent` dispatcher.
*
* @category Undici
* @since 4.0.0
*/
export const layerUndici: Layer.Layer<Client.HttpClient> = Layer.provide(layerUndiciNoDispatcher, layerDispatcher)
// -----------------------------------------------------------------------------
// node:http
// -----------------------------------------------------------------------------
/**
* Service tag for the paired Node `http` and `https` agents used by the
* node:http-backed HTTP client.
*
* @category HttpAgent
* @since 4.0.0
*/
export class HttpAgent extends Context.Service<HttpAgent, {
readonly http: Http.Agent
readonly https: Https.Agent
}>()("@effect/platform-node/NodeHttpClient/HttpAgent") {}
/**
* Acquires Node `http` and `https` agents with the supplied options and
* destroys both agents when the enclosing scope is finalized.
*
* @category HttpAgent
* @since 4.0.0
*/
export const makeAgent = (options?: Https.AgentOptions): Effect.Effect<HttpAgent["Service"], never, Scope.Scope> =>
Effect.zipWith(
Effect.acquireRelease(
Effect.sync(() => new Http.Agent(options)),
(agent) => Effect.sync(() => agent.destroy())
),
Effect.acquireRelease(
Effect.sync(() => new Https.Agent(options)),
(agent) => Effect.sync(() => agent.destroy())
),
(http, https) => ({ http, https })
)
/**
* Provides the `HttpAgent` service using scoped Node `http` and `https`
* agents configured with the supplied options.
*
* @category HttpAgent
* @since 4.0.0
*/
export const layerAgentOptions: (options?: Https.AgentOptions | undefined) => Layer.Layer<
HttpAgent
> = flow(makeAgent, Layer.effect(HttpAgent))
/**
* Provides the `HttpAgent` service using default scoped Node `http` and
* `https` agents.
*
* @category HttpAgent
* @since 4.0.0
*/
export const layerAgent: Layer.Layer<HttpAgent> = layerAgentOptions()
/**
* Creates an `HttpClient` backed by Node `http` and `https`, using the
* current `HttpAgent`, streaming request bodies, and wrapping Node responses
* as `HttpClientResponse` values.
*
* @category node:http
* @since 4.0.0
*/
export const makeNodeHttp = Effect.gen(function*() {
const agent = yield* HttpAgent
return Client.make((request, url, signal) => {
const nodeRequest = url.protocol === "https:" ?
Https.request(url, {
agent: agent.https,
method: request.method,
headers: request.headers,
signal
}) :
Http.request(url, {
agent: agent.http,
method: request.method,
headers: request.headers,
signal
})
return Effect.forkChild(sendBody(nodeRequest, request, request.body)).pipe(
Effect.flatMap(() => waitForResponse(nodeRequest, request)),
Effect.map((_) => new NodeHttpResponse(request, _))
)
})
})
const sendBody = (
nodeRequest: Http.ClientRequest,
request: HttpClientRequest,
body: Body.HttpBody
): Effect.Effect<void, Error.HttpClientError> =>
Effect.suspend((): Effect.Effect<void, Error.HttpClientError> => {
switch (body._tag) {
case "Empty": {
nodeRequest.end()
return waitForFinish(nodeRequest, request)
}
case "Uint8Array":
case "Raw": {
nodeRequest.end(body.body)
return waitForFinish(nodeRequest, request)
}
case "FormData": {
const response = new globalThis.Response(body.formData)
response.headers.forEach((value, key) => {
nodeRequest.setHeader(key, value)
})
return Effect.tryPromise({
try: () => pipeline(Readable.fromWeb(response.body! as any), nodeRequest),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
})
}
case "Stream": {
return Stream.run(
Stream.mapError(body.stream, (cause) =>
new Error.HttpClientError({
reason: new Error.EncodeError({
request,
cause
})
})),
NodeSink.fromWritable({
evaluate: () => nodeRequest,
onError: (cause) =>
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
})
)
}
}
})
const waitForResponse = (nodeRequest: Http.ClientRequest, request: HttpClientRequest) =>
Effect.callback<Http.IncomingMessage, Error.HttpClientError>((resume) => {
function onError(cause: Error) {
resume(Effect.fail(
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
))
}
nodeRequest.on("error", onError)
function onResponse(response: Http.IncomingMessage) {
nodeRequest.off("error", onError)
resume(Effect.succeed(response))
}
nodeRequest.on("upgrade", onResponse)
nodeRequest.on("response", onResponse)
return Effect.sync(() => {
nodeRequest.off("error", onError)
nodeRequest.off("upgrade", onResponse)
nodeRequest.off("response", onResponse)
})
})
const waitForFinish = (nodeRequest: Http.ClientRequest, request: HttpClientRequest) =>
Effect.callback<void, Error.HttpClientError>((resume) => {
function onError(cause: Error) {
resume(Effect.fail(
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
))
}
nodeRequest.once("error", onError)
function onFinish() {
nodeRequest.off("error", onError)
resume(Effect.void)
}
nodeRequest.once("finish", onFinish)
return Effect.sync(() => {
nodeRequest.off("error", onError)
nodeRequest.off("finish", onFinish)
})
})
class NodeHttpResponse extends NodeHttpIncomingMessage<Error.HttpClientError> implements HttpClientResponse, Pipeable {
readonly [Response.TypeId]: typeof Response.TypeId
readonly request: HttpClientRequest
constructor(
request: HttpClientRequest,
source: Http.IncomingMessage
) {
super(source, (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request,
response: this,
cause
})
}))
this[Response.TypeId] = Response.TypeId
this.request = request
}
get status() {
return this.source.statusCode!
}
cachedCookies?: Cookies.Cookies
get cookies(): Cookies.Cookies {
if (this.cachedCookies !== undefined) {
return this.cachedCookies
}
const header = this.source.headers["set-cookie"]
return this.cachedCookies = header ? Cookies.fromSetCookie(header) : Cookies.empty
}
get formData(): Effect.Effect<FormData, Error.HttpClientError> {
return Effect.tryPromise({
try: () => {
const init: {
headers: HeadersInit
status?: number
statusText?: string
} = {
headers: new globalThis.Headers(this.source.headers as any)
}
if (this.source.statusCode) {
init.status = this.source.statusCode
}
if (this.source.statusMessage) {
init.statusText = this.source.statusMessage
}
return new globalThis.Response(Readable.toWeb(this.source) as any, init).formData()
},
catch: this.onError
})
}
toJSON(): unknown {
return IncomingMessage.inspect(this, {
_id: "effect/http/HttpClientResponse",
request: this.request.toJSON(),
status: this.status
})
}
pipe() {
return pipeArguments(this, arguments)
}
}
/**
* Provides a node:http-backed `HttpClient` using the current `HttpAgent`
* service.
*
* @category node:http
* @since 4.0.0
*/
export const layerNodeHttpNoAgent: Layer.Layer<
Client.HttpClient,
never,
HttpAgent
> = Client.layerMergedContext(makeNodeHttp)
/**
* Provides a node:http-backed `HttpClient` together with default scoped Node
* `http` and `https` agents.
*
* @category node:http
* @since 4.0.0
*/
export const layerNodeHttp: Layer.Layer<Client.HttpClient> = Layer.provide(layerNodeHttpNoAgent, layerAgent)

View File

@@ -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
}
}

View File

@@ -0,0 +1,70 @@
/**
* Node.js implementation of the Effect HTTP platform service.
*
* This module connects the portable `HttpPlatform` file response helpers to
* Node runtime primitives. It serves local files through Node readable streams,
* supports byte ranges, converts Web `File` values to readable streams, and
* fills in content type and content length headers when needed.
*
* @since 4.0.0
*/
import { pipe } from "effect/Function"
import * as Layer from "effect/Layer"
import * as EtagImpl from "effect/unstable/http/Etag"
import * as Headers from "effect/unstable/http/Headers"
import * as Platform from "effect/unstable/http/HttpPlatform"
import * as ServerResponse from "effect/unstable/http/HttpServerResponse"
import * as Fs from "node:fs"
import { Readable } from "node:stream"
import Mime from "./Mime.ts"
import * as NodeFileSystem from "./NodeFileSystem.ts"
/**
* Creates the Node `HttpPlatform`, serving file responses from Node readable
* streams and adding MIME type and content-length headers when needed.
*
* @category constructors
* @since 4.0.0
*/
export const make = Platform.make({
fileResponse(path, status, statusText, headers, start, end, contentLength) {
const stream = contentLength === 0
? Readable.from([])
: Fs.createReadStream(path, { start, end: end === undefined ? undefined : end - 1 })
return ServerResponse.raw(stream, {
headers: {
...headers,
"content-type": headers["content-type"] ?? Mime.getType(path) ?? "application/octet-stream",
"content-length": contentLength.toString()
},
status,
statusText
})
},
fileWebResponse(file, status, statusText, headers, _options) {
return ServerResponse.raw(Readable.fromWeb(file.stream() as any), {
headers: Headers.merge(
headers,
Headers.fromRecordUnsafe({
"content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream",
"content-length": file.size.toString()
})
),
status,
statusText
})
}
})
/**
* Provides the Node `HttpPlatform` together with the filesystem and ETag
* services it needs for file responses.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Platform.HttpPlatform> = pipe(
Layer.effect(Platform.HttpPlatform)(make),
Layer.provide(NodeFileSystem.layer),
Layer.provide(EtagImpl.layer)
)

View File

@@ -0,0 +1,647 @@
/**
* Node.js implementation of the Effect `HttpServer`.
*
* This module adapts a supplied Node `http.Server` into Effect's
* platform-independent HTTP server service. It starts the server with Node
* `listen` options, converts `request` events into `HttpServerRequest` values,
* writes `HttpServerResponse` bodies through Node's `ServerResponse`, and
* handles `upgrade` events by exposing the upgraded socket through
* `HttpServerRequest.upgrade`. It also exports request and upgrade handler
* constructors plus layers for the server alone, HTTP support services, the
* combined server, configurable options, and tests.
*
* @since 4.0.0
*/
import * as Cause from "effect/Cause"
import * as Config from "effect/Config"
import * as Context from "effect/Context"
import * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import type * as FileSystem from "effect/FileSystem"
import { flow, type LazyArg } from "effect/Function"
import * as Latch from "effect/Latch"
import * as Layer from "effect/Layer"
import type * as Option from "effect/Option"
import type * as Path from "effect/Path"
import type * as Record from "effect/Record"
import * as Scope from "effect/Scope"
import * as Stream from "effect/Stream"
import * as Cookies from "effect/unstable/http/Cookies"
import * as Etag from "effect/unstable/http/Etag"
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
import type * as Headers from "effect/unstable/http/Headers"
import type { HttpClient } from "effect/unstable/http/HttpClient"
import * as HttpEffect from "effect/unstable/http/HttpEffect"
import * as HttpIncomingMessage from "effect/unstable/http/HttpIncomingMessage"
import type { HttpMethod } from "effect/unstable/http/HttpMethod"
import type * as Middleware from "effect/unstable/http/HttpMiddleware"
import type * as HttpPlatform from "effect/unstable/http/HttpPlatform"
import * as HttpServer from "effect/unstable/http/HttpServer"
import {
causeResponse,
ClientAbort,
HttpServerError,
RequestParseError,
ResponseError,
ServeError
} from "effect/unstable/http/HttpServerError"
import * as Request from "effect/unstable/http/HttpServerRequest"
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"
import type { HttpServerResponse } from "effect/unstable/http/HttpServerResponse"
import type * as Multipart from "effect/unstable/http/Multipart"
import * as Socket from "effect/unstable/socket/Socket"
import * as Http from "node:http"
import type * as Net from "node:net"
import type { Duplex } from "node:stream"
import { Readable } from "node:stream"
import { pipeline } from "node:stream/promises"
import { NodeHttpIncomingMessage } from "./NodeHttpIncomingMessage.ts"
import * as NodeHttpPlatform from "./NodeHttpPlatform.ts"
import * as NodeMultipart from "./NodeMultipart.ts"
import * as NodeServices from "./NodeServices.ts"
import { NodeWS } from "./NodeSocket.ts"
/**
* Creates a scoped `HttpServer` from a Node `http.Server`, starts listening
* with the supplied options, registers request and upgrade handling, and closes
* the server during scope finalization with optional graceful-shutdown control.
*
* @category constructors
* @since 4.0.0
*/
export const make = Effect.fnUntraced(function*(
evaluate: LazyArg<Http.Server>,
options: Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
) {
const scope = yield* Effect.scope
const server = evaluate()
const shutdown = yield* Effect.callback<void>((resume) => {
if (!server.listening) {
return resume(Effect.void)
}
server.close((error) => {
if (error) {
resume(Effect.die(error))
} else {
resume(Effect.void)
}
})
}).pipe(Effect.cached)
const preemptiveShutdown = options.disablePreemptiveShutdown ?
Effect.void :
Effect.timeoutOrElse(shutdown, {
duration: options.gracefulShutdownTimeout ?? Duration.seconds(20),
orElse: () => Effect.void
})
yield* Scope.addFinalizer(scope, shutdown)
yield* Effect.callback<void, ServeError>((resume) => {
function onError(cause: Error) {
resume(Effect.fail(new ServeError({ cause })))
}
server.on("error", onError)
server.listen(options, () => {
server.off("error", onError)
resume(Effect.void)
})
})
const address = server.address()!
const wss = yield* Effect.acquireRelease(
Effect.sync(() => new NodeWS.WebSocketServer({ noServer: true })),
(wss) =>
Effect.callback<void>((resume) => {
wss.close(() => resume(Effect.void))
})
).pipe(
Scope.provide(scope),
Effect.cached
)
return HttpServer.make({
address: typeof address === "string" ?
{
_tag: "UnixAddress",
path: address
} :
{
_tag: "TcpAddress",
hostname: address.address === "::" ? "0.0.0.0" : address.address,
port: address.port
},
serve: Effect.fnUntraced(function*(httpApp, middleware) {
const serveScope = yield* Effect.scope
const scope = Scope.forkUnsafe(serveScope, "parallel")
const handler = yield* (makeHandler(httpApp, {
middleware: middleware as any,
scope
}) as Effect.Effect<(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void>)
const upgradeHandler = yield* makeUpgradeHandler(wss, httpApp, {
middleware: middleware as any,
scope
})
yield* Scope.addFinalizerExit(serveScope, () => {
server.off("request", handler)
server.off("upgrade", upgradeHandler)
return preemptiveShutdown
})
server.on("request", handler)
server.on("upgrade", upgradeHandler)
})
})
})
/**
* Creates a Node `request` event handler for an Effect HTTP application,
* injecting a `HttpServerRequest` and interrupting the request fiber if the
* client closes the response before it finishes.
*
* @category handlers
* @since 4.0.0
*/
export const makeHandler = <
R,
E,
App extends Effect.Effect<HttpServerResponse, any, any> = Effect.Effect<HttpServerResponse, E, R>
>(
httpEffect: Effect.Effect<HttpServerResponse, E, R>,
options: {
readonly scope: Scope.Scope
readonly middleware?: Middleware.HttpMiddleware.Applied<App, E, R> | undefined
}
): Effect.Effect<
(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void,
never,
Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
> => {
const handled = HttpEffect.toHandled(httpEffect, handleResponse, options.middleware as any)
return Effect.withFiber((parent) => {
const services = parent.context
return Effect.succeed(function handler(
nodeRequest: Http.IncomingMessage,
nodeResponse: Http.ServerResponse
) {
const map = new Map(services.mapUnsafe)
map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse))
const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe<any>(map))(handled), options.scope)
nodeResponse.on("close", () => {
if (!nodeResponse.writableEnded) {
fiber.interruptUnsafe(parent.id, ClientAbort.annotation)
}
})
})
})
}
/**
* Creates a Node `upgrade` event handler for an Effect HTTP application,
* exposing the upgraded WebSocket as the request's `upgrade` effect and
* interrupting the request fiber when the socket closes early.
*
* @category handlers
* @since 4.0.0
*/
export const makeUpgradeHandler = <
R,
E,
App extends Effect.Effect<HttpServerResponse, any, any> = Effect.Effect<HttpServerResponse, E, R>
>(
lazyWss: Effect.Effect<NodeWS.WebSocketServer>,
httpEffect: Effect.Effect<HttpServerResponse, E, R>,
options: {
readonly scope: Scope.Scope
readonly middleware?: Middleware.HttpMiddleware.Applied<App, E, R> | undefined
}
): Effect.Effect<
(nodeRequest: Http.IncomingMessage, socket: Duplex, head: Buffer) => void,
never,
Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
> => {
const handledApp = HttpEffect.toHandled(httpEffect, handleResponse, options.middleware as any)
return Effect.withFiber((parent) => {
const services = parent.context
return Effect.succeed(function handler(
nodeRequest: Http.IncomingMessage,
socket: Duplex,
head: Buffer
) {
let nodeResponse_: Http.ServerResponse | undefined = undefined
const nodeResponse = () => {
if (nodeResponse_ === undefined) {
nodeResponse_ = new Http.ServerResponse(nodeRequest)
nodeResponse_.assignSocket(socket as any)
nodeResponse_.on("finish", () => {
socket.end()
})
}
return nodeResponse_
}
const upgradeEffect = Socket.fromWebSocket(Effect.flatMap(
lazyWss,
(wss) =>
Effect.acquireRelease(
Effect.callback<globalThis.WebSocket>((resume) =>
wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
resume(Effect.succeed(ws as any))
})
),
(ws) => Effect.sync(() => ws.close())
)
))
const map = new Map(services.mapUnsafe)
map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect))
const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe<any>(map))(handledApp), options.scope)
socket.on("close", () => {
if (!socket.writableEnded) {
fiber.interruptUnsafe(parent.id, ClientAbort.annotation)
}
})
})
})
}
class ServerRequestImpl extends NodeHttpIncomingMessage<HttpServerError> implements HttpServerRequest {
readonly [Request.TypeId]: typeof Request.TypeId
readonly response: Http.ServerResponse | LazyArg<Http.ServerResponse>
private upgradeEffect?: Effect.Effect<Socket.Socket, HttpServerError> | undefined
readonly url: string
private headersOverride?: Headers.Headers | undefined
constructor(
source: Http.IncomingMessage,
response: Http.ServerResponse | LazyArg<Http.ServerResponse>,
upgradeEffect?: Effect.Effect<Socket.Socket, HttpServerError>,
url = source.url!,
headersOverride?: Headers.Headers,
remoteAddressOverride?: Option.Option<string>
) {
super(source, (cause) =>
new HttpServerError({
reason: new RequestParseError({
request: this,
cause
})
}), remoteAddressOverride)
this[Request.TypeId] = Request.TypeId
this.response = response
this.upgradeEffect = upgradeEffect
this.url = url
this.headersOverride = headersOverride
}
private cachedCookies: Record.ReadonlyRecord<string, string> | undefined
get cookies() {
if (this.cachedCookies) {
return this.cachedCookies
}
return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
}
get resolvedResponse(): Http.ServerResponse {
return typeof this.response === "function" ? this.response() : this.response
}
modify(
options: {
readonly url?: string | undefined
readonly headers?: Headers.Headers | undefined
readonly remoteAddress?: Option.Option<string> | undefined
}
) {
return new ServerRequestImpl(
this.source,
this.response,
this.upgradeEffect,
options.url ?? this.url,
options.headers ?? this.headersOverride,
"remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
)
}
get originalUrl(): string {
return this.source.url!
}
get method(): HttpMethod {
return this.source.method!.toUpperCase() as HttpMethod
}
override get headers(): Headers.Headers {
this.headersOverride ??= this.source.headers as Headers.Headers
return this.headersOverride
}
private multipartEffect:
| Effect.Effect<
Multipart.Persisted,
Multipart.MultipartError,
Scope.Scope | FileSystem.FileSystem | Path.Path
>
| undefined
get multipart(): Effect.Effect<
Multipart.Persisted,
Multipart.MultipartError,
Scope.Scope | FileSystem.FileSystem | Path.Path
> {
if (this.multipartEffect) {
return this.multipartEffect
}
this.multipartEffect = Effect.runSync(Effect.cached(
NodeMultipart.persisted(this.source, this.source.headers)
))
return this.multipartEffect
}
get multipartStream(): Stream.Stream<Multipart.Part, Multipart.MultipartError> {
return NodeMultipart.stream(this.source, this.source.headers)
}
get upgrade(): Effect.Effect<Socket.Socket, HttpServerError> {
return this.upgradeEffect ?? Effect.fail(
new HttpServerError({
reason: new RequestParseError({
request: this,
description: "not an upgradeable ServerRequest"
})
})
)
}
override toString(): string {
return `ServerRequest(${this.method} ${this.url})`
}
toJSON(): unknown {
return HttpIncomingMessage.inspect(this, {
_id: "HttpServerRequest",
method: this.method,
url: this.originalUrl
})
}
}
/**
* Provides an `HttpServer` by creating and managing a scoped Node
* `http.Server` with the supplied listen and shutdown options.
*
* @category layers
* @since 4.0.0
*/
export const layerServer: (
evaluate: LazyArg<Http.Server<typeof Http.IncomingMessage, typeof Http.ServerResponse>>,
options: Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
) => Layer.Layer<HttpServer.HttpServer, ServeError> = flow(make, Layer.effect(HttpServer.HttpServer))
/**
* Provides the Node HTTP support services used by `NodeHttpServer`, including
* the HTTP platform, ETag generator, and core Node platform services.
*
* @category layers
* @since 4.0.0
*/
export const layerHttpServices: Layer.Layer<
NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator
> = Layer.mergeAll(
NodeHttpPlatform.layer,
Etag.layerWeak,
NodeServices.layer
)
/**
* Provides a Node `HttpServer` together with the Node HTTP platform, ETag, and
* core platform services required to serve requests.
*
* @category layers
* @since 4.0.0
*/
export const layer = (
evaluate: LazyArg<Http.Server>,
options: Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
): Layer.Layer<
HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator,
ServeError
> =>
Layer.mergeAll(
layerServer(evaluate, options),
layerHttpServices
)
/**
* Provides a Node `HttpServer` together with the Node HTTP platform, ETag,
* and core Node platform services, reading the listen and shutdown options from
* a `Config` value.
*
* @category layers
* @since 4.0.0
*/
export const layerConfig = (
evaluate: LazyArg<Http.Server>,
options: Config.Wrap<
Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
>
): Layer.Layer<
HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator,
ServeError | Config.ConfigError
> =>
Layer.mergeAll(
Layer.effect(HttpServer.HttpServer)(
Effect.flatMap(Config.unwrap(options), (options) => make(evaluate, options))
),
layerHttpServices
)
/**
* Provides a test HTTP server listening on an ephemeral port together with a
* Fetch-backed `HttpClient` configured for server integration tests.
*
* @category testing
* @since 4.0.0
*/
export const layerTest: Layer.Layer<
| HttpServer.HttpServer
| FileSystem.FileSystem
| Path.Path
| HttpPlatform.HttpPlatform
| Etag.Generator
| HttpClient,
ServeError,
never
> = HttpServer.layerTestClient.pipe(
Layer.provide(
Layer.fresh(FetchHttpClient.layer).pipe(
Layer.provide(Layer.succeed(FetchHttpClient.RequestInit)({ keepalive: false }))
)
),
Layer.provideMerge(layer(Http.createServer, { port: 0 }))
)
// -----------------------------------------------------------------------------
// Internal
// -----------------------------------------------------------------------------
const handleResponse = (
request: HttpServerRequest,
response: HttpServerResponse
): Effect.Effect<void, HttpServerError> => {
const nodeResponse = (request as ServerRequestImpl).resolvedResponse
if (nodeResponse.writableEnded) {
return Effect.void
}
let headers: Record<string, string | Array<string>> = response.headers
if (!Cookies.isEmpty(response.cookies)) {
headers = { ...headers }
const toSet = Cookies.toSetCookieHeaders(response.cookies)
if (headers["set-cookie"] !== undefined) {
toSet.push(headers["set-cookie"] as string)
}
headers["set-cookie"] = toSet
}
if (request.method === "HEAD") {
nodeResponse.writeHead(response.status, headers)
return Effect.callback<void>((resume) => {
nodeResponse.end(() => resume(Effect.void))
})
}
const body = response.body
switch (body._tag) {
case "Empty": {
nodeResponse.writeHead(response.status, headers)
nodeResponse.end()
return Effect.void
}
case "Raw": {
nodeResponse.writeHead(response.status, headers)
if (
typeof body.body === "object" && body.body !== null && "pipe" in body.body &&
typeof body.body.pipe === "function"
) {
return Effect.tryPromise({
try: (signal) => pipeline(body.body as any, nodeResponse, { signal, end: true }),
catch: (cause) =>
new HttpServerError({
reason: new ResponseError({
request,
response,
description: "Error writing raw response",
cause
})
})
}).pipe(
Effect.interruptible,
Effect.tapCause(handleCause(nodeResponse, response))
)
}
return Effect.callback<void>((resume) => {
nodeResponse.end(body.body, () => resume(Effect.void))
})
}
case "Uint8Array": {
nodeResponse.writeHead(response.status, headers)
// If the body is less than 1MB, we skip the callback
if (body.body.length < 1024 * 1024) {
nodeResponse.end(body.body)
return Effect.void
}
return Effect.callback<void>((resume) => {
nodeResponse.end(body.body, () => resume(Effect.void))
})
}
case "FormData": {
return Effect.suspend(() => {
const r = new globalThis.Response(body.formData)
nodeResponse.writeHead(response.status, {
...headers,
...Object.fromEntries(r.headers)
})
return Effect.callback<void, HttpServerError>((resume, signal) => {
Readable.fromWeb(r.body as any, { signal })
.pipe(nodeResponse)
.on("error", (cause) => {
resume(Effect.fail(
new HttpServerError({
reason: new ResponseError({
request,
response,
description: "Error writing FormData response",
cause
})
})
))
})
.once("finish", () => {
resume(Effect.void)
})
}).pipe(
Effect.interruptible,
Effect.tapCause(handleCause(nodeResponse, response))
)
})
}
case "Stream": {
nodeResponse.writeHead(response.status, headers)
const drainLatch = Latch.makeUnsafe()
nodeResponse.on("drain", () => drainLatch.openUnsafe())
return body.stream.pipe(
Stream.orDie,
Stream.runForEachArray((array) => {
let needDrain = false
for (let i = 0; i < array.length; i++) {
const written = nodeResponse.write(array[i])
if (!written && !needDrain) {
needDrain = true
drainLatch.closeUnsafe()
} else if (written && needDrain) {
needDrain = false
}
}
if (!needDrain) return Effect.void
return drainLatch.await
}),
Effect.interruptible,
Effect.matchCauseEffect({
onSuccess: () => Effect.sync(() => nodeResponse.end()),
onFailure: handleCause(nodeResponse, response)
})
)
}
}
}
const handleCause = (
nodeResponse: Http.ServerResponse,
originalResponse: HttpServerResponse
) =>
<E>(originalCause: Cause.Cause<E>) =>
Effect.flatMap(causeResponse(originalCause), ([response, cause]) => {
const headersSent = nodeResponse.headersSent
if (!headersSent) {
nodeResponse.writeHead(response.status)
}
if (!nodeResponse.writableEnded) {
nodeResponse.end()
}
return Effect.failCause(
headersSent
? Cause.combine(originalCause, Cause.die(originalResponse))
: cause
)
})

View File

@@ -0,0 +1,33 @@
/**
* Accessors for the Node.js objects behind an Effect HTTP server request.
*
* `toIncomingMessage` returns the underlying Node `http.IncomingMessage`.
* `toServerResponse` returns the underlying Node `http.ServerResponse`,
* evaluating the stored response thunk when the response was created lazily.
*
* @since 4.0.0
*/
import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"
import type * as Http from "node:http"
/**
* Returns the underlying Node `IncomingMessage` for a platform Node
* `HttpServerRequest`.
*
* @category accessors
* @since 4.0.0
*/
export const toIncomingMessage = (self: HttpServerRequest): Http.IncomingMessage => self.source as any
/**
* Returns the underlying Node `ServerResponse` for a platform Node
* `HttpServerRequest`, evaluating the stored response thunk when the response
* was created lazily.
*
* @category accessors
* @since 4.0.0
*/
export const toServerResponse = (self: HttpServerRequest): Http.ServerResponse => {
const res = (self as any).response
return typeof res === "function" ? res() : res
}

View File

@@ -0,0 +1,183 @@
/**
* Node.js multipart parsing for HTTP `multipart/form-data` request bodies.
*
* `NodeMultipart` adapts a Node `Readable` plus incoming HTTP headers into
* Effect's shared multipart model. It can expose form parts as a stream or
* collect a complete persisted form by writing file uploads to scoped temporary
* files through the current `FileSystem` and `Path` services. `fileToReadable`
* returns the underlying Node readable stream for file parts produced by this
* parser.
*
* @since 4.0.0
*/
import * as Effect from "effect/Effect"
import type * as FileSystem from "effect/FileSystem"
import * as Inspectable from "effect/Inspectable"
import type * as Path from "effect/Path"
import type * as Scope from "effect/Scope"
import * as Stream from "effect/Stream"
import * as Multipart from "effect/unstable/http/Multipart"
import * as MP from "effect/unstable/http/Multipasta/Node"
import * as NFS from "node:fs"
import type { IncomingHttpHeaders } from "node:http"
import type { Readable } from "node:stream"
import * as NodeStreamP from "node:stream/promises"
import * as NodeStream from "./NodeStream.ts"
/**
* Parses multipart data from a Node readable request body and headers into a
* stream of `Multipart.Part` values, converting parser failures to
* `MultipartError`.
*
* @category constructors
* @since 4.0.0
*/
export const stream = (
source: Readable,
headers: IncomingHttpHeaders
): Stream.Stream<Multipart.Part, Multipart.MultipartError> =>
Multipart.makeConfig(headers as any).pipe(
Effect.map((config) =>
NodeStream.fromReadable<MP.Part, Multipart.MultipartError>({
evaluate() {
const parser = MP.make(config)
source.pipe(parser)
return parser as any
},
onError: (error) => convertError(error as any)
})
),
Stream.unwrap,
Stream.map(convertPart)
)
/**
* Parses multipart data from a Node readable request body and persists file
* parts using the current `FileSystem`, `Path`, and `Scope` services.
*
* @category constructors
* @since 4.0.0
*/
export const persisted = (
source: Readable,
headers: IncomingHttpHeaders
): Effect.Effect<
Multipart.Persisted,
Multipart.MultipartError,
Scope.Scope | FileSystem.FileSystem | Path.Path
> =>
Multipart.toPersisted(stream(source, headers), (path, file) =>
Effect.tryPromise({
try: (signal) => NodeStreamP.pipeline((file as FileImpl).file, NFS.createWriteStream(path), { signal }),
catch: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
}))
/**
* Returns the underlying Node readable stream for a multipart file produced by
* the Node multipart parser.
*
* @category converting
* @since 4.0.0
*/
export const fileToReadable = (file: Multipart.File): Readable => (file as FileImpl).file
// -----------------------------------------------------------------------------
// Internal
// -----------------------------------------------------------------------------
const convertPart = (part: MP.Part): Multipart.Part =>
part._tag === "Field" ? new FieldImpl(part.info, part.value) : new FileImpl(part)
abstract class PartBase extends Inspectable.Class {
readonly [Multipart.TypeId]: typeof Multipart.TypeId
constructor() {
super()
this[Multipart.TypeId] = Multipart.TypeId
}
}
class FieldImpl extends PartBase implements Multipart.Field {
readonly _tag = "Field"
readonly key: string
readonly contentType: string
readonly value: string
constructor(
info: MP.PartInfo,
value: Uint8Array
) {
super()
this.key = info.name
this.contentType = info.contentType
this.value = MP.decodeField(info, value)
}
toJSON(): unknown {
return {
_id: "@effect/platform/Multipart/Part",
_tag: "Field",
key: this.key,
value: this.value,
contentType: this.contentType
}
}
}
class FileImpl extends PartBase implements Multipart.File {
readonly _tag = "File"
readonly key: string
readonly name: string
readonly contentType: string
readonly content: Stream.Stream<Uint8Array, Multipart.MultipartError>
readonly contentEffect: Effect.Effect<Uint8Array, Multipart.MultipartError>
readonly file: MP.FileStream
constructor(file: MP.FileStream) {
super()
this.file = file
this.key = file.info.name
this.name = file.filename ?? file.info.name
this.contentType = file.info.contentType
this.content = NodeStream.fromReadable({
evaluate: () => file,
onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
})
this.contentEffect = NodeStream.toUint8Array(() => file, {
onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
})
}
toJSON(): unknown {
return {
_id: "@effect/platform/Multipart/Part",
_tag: "File",
key: this.key,
name: this.name,
contentType: this.contentType
}
}
}
function convertError(cause: MP.MultipartError): Multipart.MultipartError {
switch (cause._tag) {
case "ReachedLimit": {
switch (cause.limit) {
case "MaxParts": {
return Multipart.MultipartError.fromReason("TooManyParts", cause)
}
case "MaxFieldSize": {
return Multipart.MultipartError.fromReason("FieldTooLarge", cause)
}
case "MaxPartSize": {
return Multipart.MultipartError.fromReason("FileTooLarge", cause)
}
case "MaxTotalSize": {
return Multipart.MultipartError.fromReason("BodyTooLarge", cause)
}
}
}
default: {
return Multipart.MultipartError.fromReason("Parse", cause)
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* Node.js layers for Effect's `Path` service.
*
* This module provides the default, POSIX, and Windows variants of the
* platform-independent `Path` service by reusing the shared Node path
* implementation. The provided path services include Node file URL conversion
* behavior.
*
* @since 4.0.0
*/
import * as NodePath from "@effect/platform-node-shared/NodePath"
import type * as Layer from "effect/Layer"
import type { Path } from "effect/Path"
/**
* Provides the default Node `Path` service using the platform's `node:path`
* implementation.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Path> = NodePath.layer
/**
* Provides the `Path` service using Node's POSIX path implementation,
* regardless of the host platform.
*
* @category layers
* @since 4.0.0
*/
export const layerPosix: Layer.Layer<Path> = NodePath.layerPosix
/**
* Provides the `Path` service using Node's Windows path implementation,
* regardless of the host platform.
*
* @category layers
* @since 4.0.0
*/
export const layerWin32: Layer.Layer<Path> = NodePath.layerWin32

View File

@@ -0,0 +1,92 @@
/**
* Node.js Redis integration backed by `ioredis`.
*
* This module creates a scoped `ioredis` client and exposes it in two forms:
* the generic `Redis` service and the {@link NodeRedis} service for direct
* access to the underlying client. `layer` accepts ioredis options directly,
* while `layerConfig` reads them from Effect config. Both layers close the
* client when the layer scope ends.
*
* @since 4.0.0
*/
import * as Config from "effect/Config"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Fn from "effect/Function"
import * as Layer from "effect/Layer"
import * as Scope from "effect/Scope"
import * as Redis from "effect/unstable/persistence/Redis"
import * as IoRedis from "ioredis"
/**
* Service tag for the Node Redis integration, exposing the underlying
* `ioredis` client and a `use` helper that maps client failures to
* `RedisError`.
*
* @category services
* @since 4.0.0
*/
export class NodeRedis extends Context.Service<NodeRedis, {
readonly client: IoRedis.Redis
readonly use: <A>(f: (client: IoRedis.Redis) => Promise<A>) => Effect.Effect<A, Redis.RedisError>
}>()("@effect/platform-node/NodeRedis") {}
const make = Effect.fnUntraced(function*(
options?: IoRedis.RedisOptions
) {
const scope = yield* Effect.scope
yield* Scope.addFinalizer(scope, Effect.promise(() => client.quit()))
const client = new IoRedis.Redis(options ?? {})
const use = <A>(f: (client: IoRedis.Redis) => Promise<A>) =>
Effect.tryPromise({
try: () => f(client),
catch: (cause) => new Redis.RedisError({ cause })
})
const redis = yield* Redis.make({
send: <A = unknown>(command: string, ...args: ReadonlyArray<string>) =>
Effect.tryPromise({
try: () => client.call(command, ...args) as Promise<A>,
catch: (cause) => new Redis.RedisError({ cause })
})
})
const nodeRedis = Fn.identity<NodeRedis["Service"]>({
client,
use
})
return Context.make(NodeRedis, nodeRedis).pipe(
Context.add(Redis.Redis, redis)
)
})
/**
* Provides `Redis` and `NodeRedis` services backed by an `ioredis` client
* created with the supplied options and closed when the layer scope ends.
*
* @category layers
* @since 4.0.0
*/
export const layer = (
options?: IoRedis.RedisOptions | undefined
): Layer.Layer<Redis.Redis | NodeRedis> => Layer.effectContext(make(options))
/**
* Provides `Redis` and `NodeRedis` services from `Config`-backed ioredis
* options, closing the client when the layer scope ends.
*
* @category layers
* @since 4.0.0
*/
export const layerConfig: (
options: Config.Wrap<IoRedis.RedisOptions>
) => Layer.Layer<Redis.Redis | NodeRedis, Config.ConfigError> = (
options: Config.Wrap<IoRedis.RedisOptions>
): Layer.Layer<Redis.Redis | NodeRedis, Config.ConfigError> =>
Layer.effectContext(
Config.unwrap(options).pipe(
Effect.flatMap(make)
)
)

View File

@@ -0,0 +1,53 @@
/**
* Node.js process runner for Effect programs.
*
* This module exports `runMain`, which runs one Effect as the main process
* fiber in Node.js. It reuses the shared Node runtime runner, including its
* error reporting, `SIGINT` / `SIGTERM` interruption, and optional teardown
* behavior.
*
* @since 4.0.0
*/
import * as NodeRuntime from "@effect/platform-node-shared/NodeRuntime"
import type { Effect } from "effect/Effect"
import type * as Runtime from "effect/Runtime"
/**
* Helps you run a main effect with built-in error handling, logging, and signal management.
*
* **When to use**
*
* Use to run a Node.js application's main Effect with structured error
* handling, log management, interrupt support, or advanced teardown
* capabilities.
*
* **Details**
*
* This function launches an Effect as the main entry point, setting exit codes
* based on success or failure, handling interrupts (e.g., Ctrl+C), and optionally
* logging errors. By default, it logs errors and uses a "pretty" format, but both
* behaviors can be turned off. You can also provide custom teardown logic to
* finalize resources or produce different exit codes.
*
* The optional configuration object can include:
* - `disableErrorReporting`: Turn off automatic error logging.
* - `teardown`: Provide custom finalization logic.
*
* @category running
* @since 4.0.0
*/
export const runMain: {
(
options?: {
readonly disableErrorReporting?: boolean | undefined
readonly teardown?: Runtime.Teardown | undefined
}
): <E, A>(effect: Effect<A, E>) => void
<E, A>(
effect: Effect<A, E>,
options?: {
readonly disableErrorReporting?: boolean | undefined
readonly teardown?: Runtime.Teardown | undefined
}
): void
} = NodeRuntime.runMain

View File

@@ -0,0 +1,50 @@
/**
* Aggregate Node.js platform services layer.
*
* This module defines the `NodeServices` union and a single `layer` that
* provides Node-backed child process spawning, crypto, filesystem, path, stdio,
* and terminal services. Use the layer when a Node program wants the standard
* platform services from one place.
*
* @since 4.0.0
*/
import type { Crypto } from "effect/Crypto"
import type { FileSystem } from "effect/FileSystem"
import * as Layer from "effect/Layer"
import type { Path } from "effect/Path"
import type { Stdio } from "effect/Stdio"
import type { Terminal } from "effect/Terminal"
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts"
import * as NodeCrypto from "./NodeCrypto.ts"
import * as NodeFileSystem from "./NodeFileSystem.ts"
import * as NodePath from "./NodePath.ts"
import * as NodeStdio from "./NodeStdio.ts"
import * as NodeTerminal from "./NodeTerminal.ts"
/**
* The union of core services provided by the Node platform layer, including
* child process spawning, filesystem, path, stdio, and terminal services.
*
* @category models
* @since 4.0.0
*/
export type NodeServices = ChildProcessSpawner | Crypto | FileSystem | Path | Stdio | Terminal
/**
* Provides the default Node implementations for child process spawning,
* filesystem, path, stdio, and terminal services.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<NodeServices> = Layer.provideMerge(
NodeChildProcessSpawner.layer,
Layer.mergeAll(
NodeFileSystem.layer,
NodeCrypto.layer,
NodePath.layer,
NodeStdio.layer,
NodeTerminal.layer
)
)

View File

@@ -0,0 +1,8 @@
/**
* @since 4.0.0
*/
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeSink"

View File

@@ -0,0 +1,72 @@
/**
* Node.js socket constructors and layers for Effect sockets.
*
* This module re-exports the shared Node socket support for TCP connections,
* Unix domain socket connections, and Node `Duplex` streams. It also provides
* WebSocket constructor layers: one that uses `globalThis.WebSocket` when
* present and falls back to `ws`, one that always uses `ws`, and one that
* creates a `Socket.Socket` layer for a WebSocket URL.
*
* @since 4.0.0
*/
import { NodeWS as WS } from "@effect/platform-node-shared/NodeSocket"
import type * as Duration from "effect/Duration"
import type * as Effect from "effect/Effect"
import { flow } from "effect/Function"
import * as Layer from "effect/Layer"
import * as Socket from "effect/unstable/socket/Socket"
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeSocket"
/**
* Provides a `Socket.WebSocketConstructor`, using `globalThis.WebSocket` when
* available and falling back to the `ws` package otherwise.
*
* @category layers
* @since 4.0.0
*/
export const layerWebSocketConstructor: Layer.Layer<
Socket.WebSocketConstructor
> = Layer.sync(Socket.WebSocketConstructor)(() => {
if ("WebSocket" in globalThis) {
return (url, protocols) => new globalThis.WebSocket(url, protocols)
}
return (url, protocols) => new WS.WebSocket(url, protocols) as unknown as globalThis.WebSocket
})
/**
* Provides a `Socket.WebSocketConstructor` backed explicitly by the `ws`
* package.
*
* @category layers
* @since 4.0.0
*/
export const layerWebSocketConstructorWS: Layer.Layer<
Socket.WebSocketConstructor
> = Layer.succeed(Socket.WebSocketConstructor)(
(url, protocols) => new WS.WebSocket(url, protocols) as unknown as globalThis.WebSocket
)
/**
* Creates a `Socket.Socket` layer for a WebSocket URL using the Node WebSocket
* constructor layer, honoring protocol, open-timeout, and close-code error
* options.
*
* @category layers
* @since 4.0.0
*/
export const layerWebSocket: (
url: string | Effect.Effect<string>,
options?: {
readonly closeCodeIsError?: ((code: number) => boolean) | undefined
readonly openTimeout?: Duration.Input | undefined
readonly protocols?: string | Array<string> | undefined
} | undefined
) => Layer.Layer<Socket.Socket, never, never> = flow(
Socket.makeWebSocket,
Layer.effect(Socket.Socket),
Layer.provide(layerWebSocketConstructor)
)

View File

@@ -0,0 +1,8 @@
/**
* @since 4.0.0
*/
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeSocketServer"

View File

@@ -0,0 +1,22 @@
/**
* Node.js `Stdio` layer for the current process.
*
* The exported layer reuses the shared Node stdio implementation. It satisfies
* the platform-independent `Stdio` service by reading command-line arguments
* from `process.argv`, consuming input from `process.stdin`, and writing output
* streams to `process.stdout` and `process.stderr`.
*
* @since 4.0.0
*/
import * as NodeStdio from "@effect/platform-node-shared/NodeStdio"
import type * as Layer from "effect/Layer"
import type { Stdio } from "effect/Stdio"
/**
* Provides the `Stdio` service backed by the current process arguments,
* stdin, stdout, and stderr streams.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Stdio> = NodeStdio.layer

View File

@@ -0,0 +1,8 @@
/**
* @since 4.0.0
*/
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeStream"

View File

@@ -0,0 +1,32 @@
/**
* Node.js implementation of the Effect `Terminal` service.
*
* This module reuses the shared Node terminal implementation. `make` creates a
* scoped process-backed `Terminal` service, and `layer` provides the default
* service with the standard quit behavior for key input.
*
* @since 4.0.0
*/
import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal"
import type { Effect } from "effect/Effect"
import type { Layer } from "effect/Layer"
import type { Scope } from "effect/Scope"
import type { Terminal, UserInput } from "effect/Terminal"
/**
* Creates a scoped `Terminal` service backed by process stdin/stdout, using the
* optional predicate to decide when key input should end the input stream.
*
* @category constructors
* @since 4.0.0
*/
export const make: (shouldQuit?: (input: UserInput) => boolean) => Effect<Terminal, never, Scope> = NodeTerminal.make
/**
* Provides the default process-backed `Terminal` service, ending key input on
* the default quit keys.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer<Terminal> = NodeTerminal.layer

View File

@@ -0,0 +1,121 @@
/**
* Parent-side Node.js support for Effect workers.
*
* `layerPlatform` installs the `WorkerPlatform` used by a Node program that
* owns workers. It supports both `node:worker_threads` workers and IPC-enabled
* child processes, routing messages through Effect's worker protocol. `layer`
* combines that platform with a `Spawner` callback, and the platform asks
* workers to close on scope finalization before forcefully terminating them on
* timeout.
*
* @since 4.0.0
*/
import * as Deferred from "effect/Deferred"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Layer from "effect/Layer"
import * as Scope from "effect/Scope"
import * as Worker from "effect/unstable/workers/Worker"
import { WorkerError, WorkerReceiveError } from "effect/unstable/workers/WorkerError"
import type * as ChildProcess from "node:child_process"
import type * as WorkerThreads from "node:worker_threads"
/**
* Provides the Node `WorkerPlatform` for `worker_threads` workers and child
* process workers, wiring messages, errors, and exits into Effect workers and
* terminating the worker if graceful shutdown times out.
*
* @category layers
* @since 4.0.0
*/
export const layerPlatform: Layer.Layer<Worker.WorkerPlatform> = Layer.succeed(Worker.WorkerPlatform)(
Worker.makePlatform<WorkerThreads.Worker | ChildProcess.ChildProcess>()({
setup({ scope, worker }) {
const exitDeferred = Deferred.makeUnsafe<void, WorkerError>()
const thing = "postMessage" in worker ?
{
postMessage(msg: any, t?: any) {
worker.postMessage(msg, t)
},
kill: () => worker.terminate(),
worker
} :
{
postMessage(msg: any, _?: any) {
worker.send(msg)
},
kill: () => worker.kill("SIGKILL"),
worker
}
worker.on("exit", () => {
Deferred.doneUnsafe(exitDeferred, Exit.void)
})
return Effect.as(
Scope.addFinalizer(
scope,
Effect.suspend(() => {
thing.postMessage([1])
return Deferred.await(exitDeferred)
}).pipe(
Effect.timeout(5000),
Effect.catchCause(() => Effect.sync(() => thing.kill()))
)
),
thing
)
},
listen({ deferred, emit, port }) {
port.worker.on("message", (message) => {
emit(message)
})
port.worker.on("messageerror", (cause) => {
Deferred.doneUnsafe(
deferred,
new WorkerError({
reason: new WorkerReceiveError({
message: "An messageerror event was emitted",
cause
})
})
)
})
port.worker.on("error", (cause) => {
Deferred.doneUnsafe(
deferred,
new WorkerError({
reason: new WorkerReceiveError({
message: "An error event was emitted",
cause
})
})
)
})
port.worker.on("exit", (code) => {
Deferred.doneUnsafe(
deferred,
new WorkerError({
reason: new WorkerReceiveError({
message: "The worker has exited with code: " + code
})
})
)
})
return Effect.void
}
})
)
/**
* Provides the Node `WorkerPlatform` together with a `Worker.Spawner` created
* from the supplied worker or child-process spawning function.
*
* @category layers
* @since 4.0.0
*/
export const layer = (
spawn: (id: number) => WorkerThreads.Worker | ChildProcess.ChildProcess
): Layer.Layer<Worker.WorkerPlatform | Worker.Spawner> =>
Layer.merge(
Worker.layerSpawner(spawn),
layerPlatform
)

View File

@@ -0,0 +1,110 @@
/**
* Node.js runtime support for workers that serve Effect worker requests.
*
* `NodeWorkerRunner` supplies the Node implementation of the Effect worker
* runner platform. The exported `layer` runs inside a `node:worker_threads`
* worker through `parentPort`, or inside a child process through
* `process.send`. It listens for parent messages, runs handlers registered with
* `WorkerRunner`, sends replies over the same channel, and closes when the
* parent sends the close message.
*
* @since 4.0.0
*/
import * as Cause from "effect/Cause"
import * as Deferred from "effect/Deferred"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Fiber from "effect/Fiber"
import * as Layer from "effect/Layer"
import { WorkerError, WorkerReceiveError, WorkerSpawnError } from "effect/unstable/workers/WorkerError"
import * as WorkerRunner from "effect/unstable/workers/WorkerRunner"
import * as WorkerThreads from "node:worker_threads"
/**
* Provides the `WorkerRunnerPlatform` for code running inside a Node worker
* thread or child process, routing parent messages to the registered handler
* and sending responses back through the parent channel.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<WorkerRunner.WorkerRunnerPlatform> = Layer.succeed(WorkerRunner.WorkerRunnerPlatform)({
start<O = unknown, I = unknown>() {
return Effect.gen(function*() {
if (!WorkerThreads.parentPort && !process.send) {
return yield* new WorkerError({
reason: new WorkerSpawnError({ message: "not in a worker" })
})
}
const sendUnsafe = WorkerThreads.parentPort
? (_portId: number, message: any, transfers?: any) => WorkerThreads.parentPort!.postMessage(message, transfers)
: (_portId: number, message: any, _transfers?: any) => process.send!(message)
const send = (_portId: number, message: O, transfers?: ReadonlyArray<unknown>) =>
Effect.sync(() => sendUnsafe(_portId, [1, message], transfers as any))
const run = <A, E, R>(
handler: (portId: number, message: I) => Effect.Effect<A, E, R> | void
): Effect.Effect<void, WorkerError, R> =>
Effect.scopedWith(Effect.fnUntraced(function*(scope) {
const closeLatch = Deferred.makeUnsafe<void, WorkerError>()
const trackFiber = Fiber.runIn(scope)
const services = yield* Effect.context<R>()
const runFork = Effect.runForkWith(services)
const onExit = (exit: Exit.Exit<any, E>) => {
if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) {
runFork(Effect.logError("unhandled error in worker", exit.cause))
}
}
;(WorkerThreads.parentPort ?? process).on("message", (message: WorkerRunner.PlatformMessage<I>) => {
if (message[0] === 0) {
const result = handler(0, message[1])
if (Effect.isEffect(result)) {
const fiber = runFork(result)
fiber.addObserver(onExit)
trackFiber(fiber)
}
} else {
if (WorkerThreads.parentPort) {
WorkerThreads.parentPort.close()
} else {
process.channel?.unref()
}
Deferred.doneUnsafe(closeLatch, Exit.void)
}
})
if (WorkerThreads.parentPort) {
WorkerThreads.parentPort.on("messageerror", (cause) => {
Deferred.doneUnsafe(
closeLatch,
new WorkerError({
reason: new WorkerReceiveError({
message: "received messageerror event",
cause
})
})
)
})
WorkerThreads.parentPort.on("error", (cause) => {
Deferred.doneUnsafe(
closeLatch,
new WorkerError({
reason: new WorkerReceiveError({
message: "received messageerror event",
cause
})
})
)
})
}
sendUnsafe(0, [0])
return yield* Deferred.await(closeLatch)
}))
return { run, send, sendUnsafe }
})
}
})

View File

@@ -0,0 +1,29 @@
/**
* Re-export of the Undici HTTP client package used by the Node platform.
*
* This module gives Effect applications a package-local import for Undici
* primitives while working with `@effect/platform-node`. Import named Undici
* APIs from here when configuring Node HTTP client dispatchers, creating agents
* or mock agents, setting the process-global dispatcher, or sharing the same
* Undici types with integrations that use the platform HTTP client.
*
* The module does not wrap or reinterpret Undici behavior. It forwards the
* installed `undici` named exports and default export, so connection pooling,
* dispatcher lifetimes, mocking, aborts, and request options follow Undici's
* own semantics.
*
* @since 4.0.0
*/
import Undici from "undici"
/**
* @category Undici
* @since 4.0.0
*/
export * from "undici"
/**
* @category Undici
* @since 4.0.0
*/
export default Undici

View File

@@ -0,0 +1,130 @@
/**
* @since 4.0.0
*/
// @barrel: Auto-generated exports. Do not edit manually.
/**
* @since 4.0.0
*/
export * as Mime from "./Mime.ts"
/**
* @since 4.0.0
*/
export * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts"
/**
* @since 4.0.0
*/
export * as NodeClusterHttp from "./NodeClusterHttp.ts"
/**
* @since 4.0.0
*/
export * as NodeClusterSocket from "./NodeClusterSocket.ts"
/**
* @since 1.0.0
*/
export * as NodeCrypto from "./NodeCrypto.ts"
/**
* @since 4.0.0
*/
export * as NodeFileSystem from "./NodeFileSystem.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpClient from "./NodeHttpClient.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpIncomingMessage from "./NodeHttpIncomingMessage.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpPlatform from "./NodeHttpPlatform.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpServer from "./NodeHttpServer.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpServerRequest from "./NodeHttpServerRequest.ts"
/**
* @since 4.0.0
*/
export * as NodeMultipart from "./NodeMultipart.ts"
/**
* @since 4.0.0
*/
export * as NodePath from "./NodePath.ts"
/**
* @since 4.0.0
*/
export * as NodeRedis from "./NodeRedis.ts"
/**
* @since 4.0.0
*/
export * as NodeRuntime from "./NodeRuntime.ts"
/**
* @since 4.0.0
*/
export * as NodeServices from "./NodeServices.ts"
/**
* @since 4.0.0
*/
export * as NodeSink from "./NodeSink.ts"
/**
* @since 4.0.0
*/
export * as NodeSocket from "./NodeSocket.ts"
/**
* @since 4.0.0
*/
export * as NodeSocketServer from "./NodeSocketServer.ts"
/**
* @since 4.0.0
*/
export * as NodeStdio from "./NodeStdio.ts"
/**
* @since 4.0.0
*/
export * as NodeStream from "./NodeStream.ts"
/**
* @since 4.0.0
*/
export * as NodeTerminal from "./NodeTerminal.ts"
/**
* @since 4.0.0
*/
export * as NodeWorker from "./NodeWorker.ts"
/**
* @since 4.0.0
*/
export * as NodeWorkerRunner from "./NodeWorkerRunner.ts"
/**
* @since 4.0.0
*/
export * as Undici from "./Undici.ts"