|
|
|
@@ -0,0 +1,67 @@
|
|
|
|
|
import { Config, Context, Effect, Layer, Match } from "effect"
|
|
|
|
|
import type { Server } from "node:http"
|
|
|
|
|
import type { AddressInfo } from "node:net"
|
|
|
|
|
import { ExpressApp } from "./ExpressApp"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class ExpressNodeHTTPServer extends Context.Tag("ExpressNodeHTTPServer")<ExpressNodeHTTPServer, Server>() {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const importNodeHTTP = Effect.tryPromise({
|
|
|
|
|
try: () => import("node:http"),
|
|
|
|
|
catch: cause => new Error("Could not import 'node:http'. Make sure you are using a runtime that implements Node APIs.", { cause }),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const serverListeningMessage = Match.type<AddressInfo | string | null>().pipe(
|
|
|
|
|
Match.when(Match.null, () => "HTTP server listening"),
|
|
|
|
|
Match.when(Match.string, v => `HTTP server listening on ${ v }`),
|
|
|
|
|
Match.orElse(v => `HTTP server listening on ${ v.address }:${ v.port }`),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
export const ExpressNodeHTTPServerLive = (config: {
|
|
|
|
|
readonly backlog?: Config.Config<number | undefined>
|
|
|
|
|
readonly exclusive?: Config.Config<boolean | undefined>
|
|
|
|
|
readonly host?: Config.Config<string | undefined>
|
|
|
|
|
readonly ipv6Only?: Config.Config<boolean | undefined>
|
|
|
|
|
readonly path?: Config.Config<string | undefined>
|
|
|
|
|
readonly port?: Config.Config<number | undefined>
|
|
|
|
|
readonly readableAll?: Config.Config<boolean | undefined>
|
|
|
|
|
readonly signal?: AbortSignal
|
|
|
|
|
readonly writableAll?: Config.Config<boolean | undefined>
|
|
|
|
|
}) => Layer.effect(ExpressNodeHTTPServer, Effect.acquireRelease(
|
|
|
|
|
Effect.gen(function*() {
|
|
|
|
|
const app = yield* ExpressApp
|
|
|
|
|
const http = yield* importNodeHTTP
|
|
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
|
backlog: yield* config.backlog || Config.succeed(undefined),
|
|
|
|
|
exclusive: yield* config.exclusive || Config.succeed(undefined),
|
|
|
|
|
host: yield* config.host || Config.succeed(undefined),
|
|
|
|
|
ipv6Only: yield* config.ipv6Only || Config.succeed(undefined),
|
|
|
|
|
path: yield* config.path || Config.succeed(undefined),
|
|
|
|
|
port: yield* config.port || Config.succeed(undefined),
|
|
|
|
|
readableAll: yield* config.readableAll || Config.succeed(undefined),
|
|
|
|
|
signal: config.signal,
|
|
|
|
|
writableAll: yield* config.writableAll || Config.succeed(undefined),
|
|
|
|
|
} as const
|
|
|
|
|
|
|
|
|
|
return yield* Effect.async<Server>(resume => {
|
|
|
|
|
const server = http.createServer(app).listen(options,
|
|
|
|
|
() => resume(
|
|
|
|
|
Effect.succeed(server).pipe(
|
|
|
|
|
Effect.tap(Effect.logInfo(
|
|
|
|
|
serverListeningMessage(server.address())
|
|
|
|
|
))
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
server => Effect.gen(function*() {
|
|
|
|
|
yield* Effect.logInfo("HTTP server is stopping. Waiting for existing connections to end...")
|
|
|
|
|
yield* Effect.async(resume => {
|
|
|
|
|
server.close(() => resume(Effect.logInfo("HTTP server closed")))
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
))
|