Skip to content

Commit 7870557

Browse files
authored
Add Deno socket server support (#6724)
1 parent 20b9660 commit 7870557

5 files changed

Lines changed: 469 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/platform-deno": patch
3+
---
4+
5+
Add native Deno TCP, Unix, and TLS socket server adapters.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Native Deno TCP and Unix socket servers for Effect's unstable socket server
3+
* API.
4+
*
5+
* The plain `make` and `layer` names intentionally differ from
6+
* `DenoSocket.makeTcp` and `DenoSocket.layerTcp` because this module has only
7+
* one server kind. Deno has no standalone WebSocket server, so WebSocket
8+
* constructors are omitted, and there is no `IncomingMessage` analogue.
9+
* Connections made before `run` starts remain in the kernel backlog instead of
10+
* being accepted and buffered as they are by the Node implementation. A second
11+
* concurrent `run` waits for the first because listener access is guarded by a
12+
* semaphore.
13+
*
14+
* Deno-native TLS server constructors are included even though the Node socket
15+
* server has no corresponding dedicated constructors. TLS is deliberately not
16+
* tested here because the repository has no certificate fixture.
17+
*
18+
* @since 4.0.0
19+
*/
20+
import * as Effect from "effect/Effect"
21+
import * as Function from "effect/Function"
22+
import * as Layer from "effect/Layer"
23+
import type * as Scope from "effect/Scope"
24+
import * as SocketServer from "effect/unstable/socket/SocketServer"
25+
import { closeListener, fromListener } from "./internal/denoSocketServer.ts"
26+
27+
/**
28+
* Native Deno options for listening on a TCP or Unix socket.
29+
*
30+
* @category models
31+
* @since 4.0.0
32+
*/
33+
export type ListenOptions =
34+
| (Deno.TcpListenOptions & { transport?: "tcp" })
35+
| (Deno.UnixListenOptions & { transport: "unix" })
36+
37+
/**
38+
* Native Deno options and certified key material for listening with TLS.
39+
*
40+
* @category models
41+
* @since 4.0.0
42+
*/
43+
export type TlsListenOptions = Deno.ListenTlsOptions & Deno.TlsCertifiedKeyPem
44+
45+
/**
46+
* Creates a scoped socket server using a native Deno TCP or Unix listener.
47+
*
48+
* @category constructors
49+
* @since 4.0.0
50+
*/
51+
export const make: (
52+
options: ListenOptions
53+
) => Effect.Effect<
54+
SocketServer.SocketServer["Service"],
55+
SocketServer.SocketServerError,
56+
Scope.Scope
57+
> = Effect.fnUntraced(function*(options) {
58+
const listener = yield* Effect.acquireRelease(
59+
Effect.try({
60+
try: () => options.transport === "unix" ? Deno.listen(options) : Deno.listen(options),
61+
catch: openError
62+
}),
63+
closeListener
64+
)
65+
return fromListener(listener)
66+
})
67+
68+
/**
69+
* Provides a socket server using a scoped native Deno TCP or Unix listener.
70+
*
71+
* @category layers
72+
* @since 4.0.0
73+
*/
74+
export const layer: (
75+
options: ListenOptions
76+
) => Layer.Layer<SocketServer.SocketServer, SocketServer.SocketServerError> = Function.flow(
77+
make,
78+
Layer.effect(SocketServer.SocketServer)
79+
)
80+
81+
/**
82+
* Creates a scoped TLS socket server using a native Deno TLS listener.
83+
*
84+
* @category constructors
85+
* @since 4.0.0
86+
*/
87+
export const makeTls: (
88+
options: TlsListenOptions
89+
) => Effect.Effect<
90+
SocketServer.SocketServer["Service"],
91+
SocketServer.SocketServerError,
92+
Scope.Scope
93+
> = Effect.fnUntraced(function*(options) {
94+
const listener = yield* Effect.acquireRelease(
95+
Effect.try({
96+
try: () => Deno.listenTls(options),
97+
catch: openError
98+
}),
99+
closeListener
100+
)
101+
return fromListener(listener)
102+
})
103+
104+
/**
105+
* Provides a TLS socket server using a scoped native Deno TLS listener.
106+
*
107+
* @category layers
108+
* @since 4.0.0
109+
*/
110+
export const layerTls: (
111+
options: TlsListenOptions
112+
) => Layer.Layer<SocketServer.SocketServer, SocketServer.SocketServerError> = Function.flow(
113+
makeTls,
114+
Layer.effect(SocketServer.SocketServer)
115+
)
116+
117+
const openError = (cause: unknown) =>
118+
new SocketServer.SocketServerError({
119+
reason: new SocketServer.SocketServerOpenError({ cause })
120+
})

packages/platform-deno/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export * as DenoServices from "./DenoServices.ts"
5959
*/
6060
export * as DenoSocket from "./DenoSocket.ts"
6161

62+
/**
63+
* @since 4.0.0
64+
*/
65+
export * as DenoSocketServer from "./DenoSocketServer.ts"
66+
6267
/**
6368
* @since 4.0.0
6469
*/
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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

Comments
 (0)