Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'
This commit is contained in:
25
repos/effect-smol/packages/platform-node/src/Mime.ts
Normal file
25
repos/effect-smol/packages/platform-node/src/Mime.ts
Normal 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
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Node.js implementation of `ChildProcessSpawner`.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeChildProcessSpawner"
|
||||
155
repos/effect-smol/packages/platform-node/src/NodeClusterHttp.ts
Normal file
155
repos/effect-smol/packages/platform-node/src/NodeClusterHttp.ts
Normal 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)
|
||||
@@ -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)
|
||||
)
|
||||
24
repos/effect-smol/packages/platform-node/src/NodeCrypto.ts
Normal file
24
repos/effect-smol/packages/platform-node/src/NodeCrypto.ts
Normal 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
|
||||
@@ -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
|
||||
664
repos/effect-smol/packages/platform-node/src/NodeHttpClient.ts
Normal file
664
repos/effect-smol/packages/platform-node/src/NodeHttpClient.ts
Normal 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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
647
repos/effect-smol/packages/platform-node/src/NodeHttpServer.ts
Normal file
647
repos/effect-smol/packages/platform-node/src/NodeHttpServer.ts
Normal 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
|
||||
)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
183
repos/effect-smol/packages/platform-node/src/NodeMultipart.ts
Normal file
183
repos/effect-smol/packages/platform-node/src/NodeMultipart.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
40
repos/effect-smol/packages/platform-node/src/NodePath.ts
Normal file
40
repos/effect-smol/packages/platform-node/src/NodePath.ts
Normal 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
|
||||
92
repos/effect-smol/packages/platform-node/src/NodeRedis.ts
Normal file
92
repos/effect-smol/packages/platform-node/src/NodeRedis.ts
Normal 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)
|
||||
)
|
||||
)
|
||||
53
repos/effect-smol/packages/platform-node/src/NodeRuntime.ts
Normal file
53
repos/effect-smol/packages/platform-node/src/NodeRuntime.ts
Normal 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
|
||||
50
repos/effect-smol/packages/platform-node/src/NodeServices.ts
Normal file
50
repos/effect-smol/packages/platform-node/src/NodeServices.ts
Normal 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
|
||||
)
|
||||
)
|
||||
8
repos/effect-smol/packages/platform-node/src/NodeSink.ts
Normal file
8
repos/effect-smol/packages/platform-node/src/NodeSink.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeSink"
|
||||
72
repos/effect-smol/packages/platform-node/src/NodeSocket.ts
Normal file
72
repos/effect-smol/packages/platform-node/src/NodeSocket.ts
Normal 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)
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeSocketServer"
|
||||
22
repos/effect-smol/packages/platform-node/src/NodeStdio.ts
Normal file
22
repos/effect-smol/packages/platform-node/src/NodeStdio.ts
Normal 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
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeStream"
|
||||
32
repos/effect-smol/packages/platform-node/src/NodeTerminal.ts
Normal file
32
repos/effect-smol/packages/platform-node/src/NodeTerminal.ts
Normal 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
|
||||
121
repos/effect-smol/packages/platform-node/src/NodeWorker.ts
Normal file
121
repos/effect-smol/packages/platform-node/src/NodeWorker.ts
Normal 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
|
||||
)
|
||||
110
repos/effect-smol/packages/platform-node/src/NodeWorkerRunner.ts
Normal file
110
repos/effect-smol/packages/platform-node/src/NodeWorkerRunner.ts
Normal 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 }
|
||||
})
|
||||
}
|
||||
})
|
||||
29
repos/effect-smol/packages/platform-node/src/Undici.ts
Normal file
29
repos/effect-smol/packages/platform-node/src/Undici.ts
Normal 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
|
||||
130
repos/effect-smol/packages/platform-node/src/index.ts
Normal file
130
repos/effect-smol/packages/platform-node/src/index.ts
Normal 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"
|
||||
Reference in New Issue
Block a user