|
| 1 | +import * as Cause from "effect/Cause" |
| 2 | +import * as Context from "effect/Context" |
| 3 | +import * as Effect from "effect/Effect" |
| 4 | +import * as Exit from "effect/Exit" |
| 5 | +import * as Fiber from "effect/Fiber" |
| 6 | +import { pipe } from "effect/Function" |
| 7 | +import * as References from "effect/References" |
| 8 | +import * as Scope from "effect/Scope" |
| 9 | +import * as Semaphore from "effect/Semaphore" |
| 10 | +import type * as Socket from "effect/unstable/socket/Socket" |
| 11 | +import * as SocketServer from "effect/unstable/socket/SocketServer" |
| 12 | +import * as DenoSocket from "../DenoSocket.ts" |
| 13 | + |
| 14 | +const initialBackoffMillis = 10 |
| 15 | +const maximumBackoffMillis = 1_000 |
| 16 | + |
| 17 | +type Listener = Deno.Listener<Deno.Conn, Deno.NetAddr | Deno.UnixAddr> |
| 18 | + |
| 19 | +export const fromListener = (listener: Listener): SocketServer.SocketServer["Service"] => { |
| 20 | + const semaphore = Semaphore.makeUnsafe(1) |
| 21 | + |
| 22 | + const run = <R, E, _>(handler: (socket: Socket.Socket) => Effect.Effect<_, E, R>) => |
| 23 | + semaphore.withPermit(Effect.gen(function*() { |
| 24 | + const scope = yield* Scope.make() |
| 25 | + const services = Context.omit(Scope.Scope)(yield* Effect.context<R>()) as Context.Context<R> |
| 26 | + const trackFiber = Fiber.runIn(scope) |
| 27 | + let failures = 0 |
| 28 | + |
| 29 | + const loop: Effect.Effect<never> = Effect.suspend(() => |
| 30 | + Effect.tryPromise(() => listener.accept()).pipe( |
| 31 | + Effect.matchEffect({ |
| 32 | + onFailure: (error) => { |
| 33 | + const cause = error.cause |
| 34 | + if (isTeardownError(cause)) return Effect.never |
| 35 | + const backoff = Math.min(initialBackoffMillis * 2 ** failures++, maximumBackoffMillis) |
| 36 | + return reportUnhandledError(Cause.fail(cause)).pipe( |
| 37 | + Effect.andThen(Effect.sleep(backoff)), |
| 38 | + Effect.andThen(loop) |
| 39 | + ) |
| 40 | + }, |
| 41 | + onSuccess: (conn) => { |
| 42 | + failures = 0 |
| 43 | + pipe( |
| 44 | + DenoSocket.fromConn(Effect.acquireRelease(Effect.succeed(conn), closeConn)), |
| 45 | + Effect.flatMap(handler), |
| 46 | + Effect.ensuring(closeConn(conn)), |
| 47 | + Effect.catchCause(reportUnhandledError), |
| 48 | + Effect.runForkWith(Context.add(services, DenoSocket.Conn, conn)), |
| 49 | + trackFiber |
| 50 | + ) |
| 51 | + return loop |
| 52 | + } |
| 53 | + }) |
| 54 | + ) |
| 55 | + ) |
| 56 | + |
| 57 | + return yield* loop.pipe(Effect.ensuring(Scope.close(scope, Exit.void))) |
| 58 | + })) |
| 59 | + |
| 60 | + const address = listener.addr |
| 61 | + return SocketServer.SocketServer.of({ |
| 62 | + address: "path" in address |
| 63 | + ? { _tag: "UnixAddress", path: address.path } |
| 64 | + : { _tag: "TcpAddress", hostname: address.hostname, port: address.port }, |
| 65 | + run |
| 66 | + }) |
| 67 | +} |
| 68 | + |
| 69 | +export const closeListener = (listener: Listener): Effect.Effect<void> => |
| 70 | + Effect.try(() => listener.close()).pipe( |
| 71 | + Effect.catch(({ cause }) => isTeardownError(cause) ? Effect.void : Effect.die(cause)) |
| 72 | + ) |
| 73 | + |
| 74 | +const closeConn = (conn: Deno.Conn): Effect.Effect<void> => |
| 75 | + Effect.try(() => conn.close()).pipe( |
| 76 | + Effect.catch(({ cause }) => isTeardownError(cause) ? Effect.void : Effect.die(cause)) |
| 77 | + ) |
| 78 | + |
| 79 | +const isTeardownError = (cause: unknown): boolean => |
| 80 | + cause instanceof Deno.errors.BadResource || cause instanceof Deno.errors.Interrupted |
| 81 | + |
| 82 | +const reportUnhandledError = <E>(cause: Cause.Cause<E>) => |
| 83 | + Effect.withFiber<void>((fiber) => { |
| 84 | + const unhandledLogLevel = fiber.getRef(References.UnhandledLogLevel) |
| 85 | + if (unhandledLogLevel) { |
| 86 | + return Effect.logWithLevel(unhandledLogLevel)(cause, "Unhandled error in SocketServer") |
| 87 | + } |
| 88 | + return Effect.void |
| 89 | + }) |
0 commit comments