@effect/platform-node 0.44.11 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ import * as Error from "@effect/platform/Http/ServerError"
12
12
  import * as ServerRequest from "@effect/platform/Http/ServerRequest"
13
13
  import type * as ServerResponse from "@effect/platform/Http/ServerResponse"
14
14
  import type * as Path from "@effect/platform/Path"
15
+ import * as Socket from "@effect/platform/Socket"
15
16
  import * as Cause from "effect/Cause"
16
17
  import * as Config from "effect/Config"
17
18
  import * as Effect from "effect/Effect"
@@ -19,12 +20,14 @@ import { type LazyArg } from "effect/Function"
19
20
  import * as Layer from "effect/Layer"
20
21
  import * as Option from "effect/Option"
21
22
  import * as Runtime from "effect/Runtime"
22
- import type * as Scope from "effect/Scope"
23
+ import * as Scope from "effect/Scope"
23
24
  import * as Stream from "effect/Stream"
24
- import type * as Http from "node:http"
25
+ import * as Http from "node:http"
25
26
  import type * as Net from "node:net"
27
+ import type { Duplex } from "node:stream"
26
28
  import { Readable } from "node:stream"
27
29
  import { pipeline } from "node:stream/promises"
30
+ import * as WS from "ws"
28
31
  import * as NodeContext from "../../NodeContext.js"
29
32
  import * as NodeSink from "../../NodeSink.js"
30
33
  import { IncomingMessageImpl } from "./incomingMessage.js"
@@ -36,6 +39,8 @@ export const make = (
36
39
  options: Net.ListenOptions
37
40
  ): Effect.Effect<Server.Server, Error.ServeError, Scope.Scope> =>
38
41
  Effect.gen(function*(_) {
42
+ const scope = yield* _(Effect.scope)
43
+
39
44
  const server = yield* _(Effect.acquireRelease(
40
45
  Effect.sync(evaluate),
41
46
  (server) =>
@@ -52,7 +57,7 @@ export const make = (
52
57
 
53
58
  yield* _(Effect.async<void, Error.ServeError>((resume) => {
54
59
  server.on("error", (error) => {
55
- resume(Effect.fail(Error.ServeError({ error })))
60
+ resume(Effect.fail(new Error.ServeError({ error })))
56
61
  })
57
62
  server.listen(options, () => {
58
63
  resume(Effect.unit)
@@ -61,6 +66,18 @@ export const make = (
61
66
 
62
67
  const address = server.address()!
63
68
 
69
+ const wss = yield* _(
70
+ Effect.acquireRelease(
71
+ Effect.sync(() => new WS.WebSocketServer({ noServer: true })),
72
+ (wss) =>
73
+ Effect.async<void>((resume) => {
74
+ wss.close(() => resume(Effect.unit))
75
+ })
76
+ ),
77
+ Scope.extend(scope),
78
+ Effect.cached
79
+ )
80
+
64
81
  return Server.make({
65
82
  address: typeof address === "string" ?
66
83
  {
@@ -75,12 +92,15 @@ export const make = (
75
92
  serve: (httpApp, middleware) =>
76
93
  Effect.gen(function*(_) {
77
94
  const handler = yield* _(makeHandler(httpApp, middleware!))
95
+ const upgradeHandler = yield* _(makeUpgradeHandler(wss, httpApp, middleware!))
78
96
  yield* _(Effect.addFinalizer(() =>
79
97
  Effect.sync(() => {
80
98
  server.off("request", handler)
99
+ server.off("upgrade", upgradeHandler)
81
100
  })
82
101
  ))
83
102
  server.on("request", handler)
103
+ server.on("upgrade", upgradeHandler)
84
104
  })
85
105
  })
86
106
  }).pipe(
@@ -113,7 +133,10 @@ export const makeHandler: {
113
133
  )
114
134
  return Effect.map(Effect.runtime<R>(), (runtime) => {
115
135
  const runFork = Runtime.runFork(runtime)
116
- return function handler(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) {
136
+ return function handler(
137
+ nodeRequest: Http.IncomingMessage,
138
+ nodeResponse: Http.ServerResponse
139
+ ) {
117
140
  const fiber = runFork(
118
141
  Effect.provideService(
119
142
  handledApp,
@@ -134,6 +157,65 @@ export const makeHandler: {
134
157
  })
135
158
  }
136
159
 
160
+ /** @internal */
161
+ export const makeUpgradeHandler = <R, E>(
162
+ lazyWss: Effect.Effect<WS.WebSocketServer>,
163
+ httpApp: App.Default<R, E>,
164
+ middleware?: Middleware.Middleware
165
+ ) => {
166
+ const handledApp = Effect.scoped(
167
+ middleware
168
+ ? middleware(App.withDefaultMiddleware(respond(httpApp)))
169
+ : App.withDefaultMiddleware(respond(httpApp))
170
+ )
171
+ return Effect.map(Effect.runtime<R>(), (runtime) => {
172
+ const runFork = Runtime.runFork(runtime)
173
+ return function handler(
174
+ nodeRequest: Http.IncomingMessage,
175
+ socket: Duplex,
176
+ head: Buffer
177
+ ) {
178
+ let nodeResponse_: Http.ServerResponse | undefined = undefined
179
+ const nodeResponse = () => {
180
+ if (nodeResponse_ === undefined) {
181
+ nodeResponse_ = new Http.ServerResponse(nodeRequest)
182
+ nodeResponse_.assignSocket(socket as any)
183
+ }
184
+ return nodeResponse_
185
+ }
186
+ const upgradeEffect = Socket.fromWebSocket(Effect.flatMap(
187
+ lazyWss,
188
+ (wss) =>
189
+ Effect.acquireRelease(
190
+ Effect.async<globalThis.WebSocket>((resume) =>
191
+ wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
192
+ resume(Effect.succeed(ws as any))
193
+ })
194
+ ),
195
+ (ws) => Effect.sync(() => ws.close())
196
+ )
197
+ ))
198
+ const fiber = runFork(
199
+ Effect.provideService(
200
+ handledApp,
201
+ ServerRequest.ServerRequest,
202
+ new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect)
203
+ )
204
+ )
205
+ socket.on("close", () => {
206
+ const res = nodeResponse()
207
+ if (!socket.writableEnded) {
208
+ if (!res.headersSent) {
209
+ res.writeHead(499)
210
+ }
211
+ res.end()
212
+ runFork(fiber.interruptAsFork(Error.clientAbortFiberId))
213
+ }
214
+ })
215
+ }
216
+ })
217
+ }
218
+
137
219
  const respond = Middleware.make((httpApp) =>
138
220
  Effect.uninterruptibleMask((restore) =>
139
221
  Effect.flatMap(ServerRequest.ServerRequest, (request) =>
@@ -149,7 +231,7 @@ const respond = Middleware.make((httpApp) =>
149
231
  ),
150
232
  (cause) =>
151
233
  Effect.sync(() => {
152
- const nodeResponse = (request as ServerRequestImpl).response
234
+ const nodeResponse = (request as ServerRequestImpl).resolvedResponse
153
235
  if (!nodeResponse.headersSent) {
154
236
  nodeResponse.writeHead(Cause.isInterruptedOnly(cause) ? 503 : 500)
155
237
  }
@@ -166,13 +248,14 @@ class ServerRequestImpl extends IncomingMessageImpl<Error.RequestError> implemen
166
248
 
167
249
  constructor(
168
250
  readonly source: Http.IncomingMessage,
169
- readonly response: Http.ServerResponse,
251
+ readonly response: Http.ServerResponse | LazyArg<Http.ServerResponse>,
252
+ private upgradeEffect?: Effect.Effect<Socket.Socket, Error.RequestError>,
170
253
  readonly url = source.url!,
171
254
  private headersOverride?: Headers.Headers,
172
255
  remoteAddressOverride?: string
173
256
  ) {
174
257
  super(source, (_) =>
175
- Error.RequestError({
258
+ new Error.RequestError({
176
259
  request: this,
177
260
  reason: "Decode",
178
261
  error: _
@@ -180,6 +263,10 @@ class ServerRequestImpl extends IncomingMessageImpl<Error.RequestError> implemen
180
263
  this[ServerRequest.TypeId] = ServerRequest.TypeId
181
264
  }
182
265
 
266
+ get resolvedResponse(): Http.ServerResponse {
267
+ return typeof this.response === "function" ? this.response() : this.response
268
+ }
269
+
183
270
  modify(
184
271
  options: {
185
272
  readonly url?: string | undefined
@@ -190,6 +277,7 @@ class ServerRequestImpl extends IncomingMessageImpl<Error.RequestError> implemen
190
277
  return new ServerRequestImpl(
191
278
  this.source,
192
279
  this.response,
280
+ this.upgradeEffect,
193
281
  options.url ?? this.url,
194
282
  options.headers ?? this.headersOverride,
195
283
  options.remoteAddress ?? this.remoteAddressOverride
@@ -234,6 +322,16 @@ class ServerRequestImpl extends IncomingMessageImpl<Error.RequestError> implemen
234
322
  return MultipartNode.stream(this.source, this.source.headers)
235
323
  }
236
324
 
325
+ get upgrade(): Effect.Effect<Socket.Socket, Error.RequestError> {
326
+ return this.upgradeEffect ?? Effect.fail(
327
+ new Error.RequestError({
328
+ request: this,
329
+ reason: "Decode",
330
+ error: "not an upgradeable ServerRequest"
331
+ })
332
+ )
333
+ }
334
+
237
335
  toString(): string {
238
336
  return `ServerRequest(${this.method} ${this.url})`
239
337
  }
@@ -284,8 +382,10 @@ export const layerConfig = (
284
382
 
285
383
  const handleResponse = (request: ServerRequest.ServerRequest, response: ServerResponse.ServerResponse) =>
286
384
  Effect.suspend((): Effect.Effect<void, Error.ResponseError> => {
287
- const nodeResponse = (request as ServerRequestImpl).response
288
- if (request.method === "HEAD") {
385
+ const nodeResponse = (request as ServerRequestImpl).resolvedResponse
386
+ if (nodeResponse.writableEnded) {
387
+ return Effect.unit
388
+ } else if (request.method === "HEAD") {
289
389
  nodeResponse.writeHead(response.status, response.headers)
290
390
  nodeResponse.end()
291
391
  return Effect.unit
@@ -306,7 +406,7 @@ const handleResponse = (request: ServerRequest.ServerRequest, response: ServerRe
306
406
  return Effect.tryPromise({
307
407
  try: (signal) => pipeline(body.body as any, nodeResponse, { signal, end: true }),
308
408
  catch: (error) =>
309
- Error.ResponseError({
409
+ new Error.ResponseError({
310
410
  request,
311
411
  response,
312
412
  reason: "Decode",
@@ -333,12 +433,14 @@ const handleResponse = (request: ServerRequest.ServerRequest, response: ServerRe
333
433
  Readable.fromWeb(r.body as any)
334
434
  .pipe(nodeResponse)
335
435
  .on("error", (error) => {
336
- resume(Effect.fail(Error.ResponseError({
337
- request,
338
- response,
339
- reason: "Decode",
340
- error
341
- })))
436
+ resume(Effect.fail(
437
+ new Error.ResponseError({
438
+ request,
439
+ response,
440
+ reason: "Decode",
441
+ error
442
+ })
443
+ ))
342
444
  })
343
445
  .once("finish", () => {
344
446
  resume(Effect.unit)
@@ -351,7 +453,7 @@ const handleResponse = (request: ServerRequest.ServerRequest, response: ServerRe
351
453
  Stream.mapError(
352
454
  body.stream,
353
455
  (error) =>
354
- Error.ResponseError({
456
+ new Error.ResponseError({
355
457
  request,
356
458
  response,
357
459
  reason: "Decode",
@@ -359,7 +461,7 @@ const handleResponse = (request: ServerRequest.ServerRequest, response: ServerRe
359
461
  })
360
462
  ),
361
463
  NodeSink.fromWritable(() => nodeResponse, (error) =>
362
- Error.ResponseError({
464
+ new Error.ResponseError({
363
465
  request,
364
466
  response,
365
467
  reason: "Decode",