@jsenv/server 14.1.4 → 14.1.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "14.1.4",
3
+ "version": "14.1.5",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -2,7 +2,7 @@ import { Stream, Writable, Readable } from "node:stream"
2
2
  import { createReadStream } from "node:fs"
3
3
 
4
4
  import { isObservable, observableFromValue } from "./observable.js"
5
- import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
5
+ import { observableFromNodeStream } from "./observable_from_node_stream.js"
6
6
 
7
7
  export const normalizeBodyMethods = (body) => {
8
8
  if (isObservable(body)) {
@@ -23,7 +23,7 @@ export const normalizeBodyMethods = (body) => {
23
23
 
24
24
  if (isNodeStream(body)) {
25
25
  return {
26
- asObservable: () => nodeStreamToObservable(body),
26
+ asObservable: () => observableFromNodeStream(body),
27
27
  destroy: () => {
28
28
  body.destroy()
29
29
  },
@@ -61,7 +61,7 @@ export const fileHandleToReadableStream = (fileHandle) => {
61
61
  }
62
62
 
63
63
  const fileHandleToObservable = (fileHandle) => {
64
- return nodeStreamToObservable(fileHandleToReadableStream(fileHandle))
64
+ return observableFromNodeStream(fileHandleToReadableStream(fileHandle))
65
65
  }
66
66
 
67
67
  const isNodeStream = (value) => {
@@ -2,9 +2,12 @@
2
2
  import { Readable } from "node:stream"
3
3
  import { createObservable } from "./observable.js"
4
4
 
5
- const readableStreamLifetimeInSeconds = 120
6
-
7
- export const nodeStreamToObservable = (nodeStream) => {
5
+ export const observableFromNodeStream = (
6
+ nodeStream,
7
+ {
8
+ readableStreamLifetime = 120_000, // 2s
9
+ } = {},
10
+ ) => {
8
11
  const observable = createObservable(({ next, error, complete }) => {
9
12
  if (nodeStream.isPaused()) {
10
13
  nodeStream.resume()
@@ -38,7 +41,9 @@ export const nodeStreamToObservable = (nodeStream) => {
38
41
  // used in the next ${readableStreamLifetimeInSeconds} otherwise destroys it
39
42
  const timeout = setTimeout(() => {
40
43
  process.emitWarning(
41
- `Readable stream not used after ${readableStreamLifetimeInSeconds} seconds. It will be destroyed to release resources`,
44
+ `Readable stream not used after ${
45
+ readableStreamLifetime / 1000
46
+ } seconds. It will be destroyed to release resources`,
42
47
  {
43
48
  CODE: "READABLE_STREAM_TIMEOUT",
44
49
  // url is for http client request
@@ -46,7 +51,7 @@ export const nodeStreamToObservable = (nodeStream) => {
46
51
  },
47
52
  )
48
53
  nodeStream.destroy()
49
- }, readableStreamLifetimeInSeconds * 1000)
54
+ }, readableStreamLifetime)
50
55
  observable.timeout = timeout
51
56
  onceReadableStreamUsedOrClosed(nodeStream, () => {
52
57
  clearTimeout(timeout)
@@ -1,9 +1,14 @@
1
- import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
2
- import { headersFromObject } from "./headersFromObject.js"
1
+ import { observableFromNodeStream } from "./observable_from_node_stream.js"
2
+ import { headersFromObject } from "../internal/headersFromObject.js"
3
3
 
4
- export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
4
+ export const fromNodeRequest = (
5
+ nodeRequest,
6
+ { serverOrigin, signal, requestBodyLifetime },
7
+ ) => {
5
8
  const headers = headersFromObject(nodeRequest.headers)
6
- const body = nodeStreamToObservable(nodeRequest)
9
+ const body = observableFromNodeStream(nodeRequest, {
10
+ readableStreamLifetime: requestBodyLifetime,
11
+ })
7
12
 
8
13
  let requestOrigin
9
14
  if (nodeRequest.upgrade) {
@@ -4,7 +4,7 @@ import { raceCallbacks } from "@jsenv/abort"
4
4
 
5
5
  import { normalizeBodyMethods } from "./body.js"
6
6
 
7
- export const populateNodeResponse = async (
7
+ export const writeNodeResponse = async (
8
8
  responseStream,
9
9
  { status, statusText, headers, body, bodyEncoding },
10
10
  { signal, ignoreBody, onAbort, onError, onHeadersSent, onEnd } = {},
@@ -3,7 +3,7 @@ import { createLogger } from "@jsenv/log"
3
3
  import {
4
4
  createObservable,
5
5
  createCompositeProducer,
6
- } from "@jsenv/server/src/internal/observable.js"
6
+ } from "@jsenv/server/src/interfacing_with_node/observable.js"
7
7
 
8
8
  // https://www.html5rocks.com/en/tutorials/eventsource/basics/
9
9
  export const createSSERoom = ({
@@ -18,8 +18,8 @@ import {
18
18
  fromNodeRequest,
19
19
  createPushRequest,
20
20
  applyRedirectionToRequest,
21
- } from "./internal/request_factory.js"
22
- import { populateNodeResponse } from "./internal/populateNodeResponse.js"
21
+ } from "./interfacing_with_node/request_factory.js"
22
+ import { writeNodeResponse } from "./interfacing_with_node/write_node_response.js"
23
23
  import {
24
24
  statusToType,
25
25
  colorizeResponseStatus,
@@ -87,6 +87,13 @@ export const startServer = async ({
87
87
  ),
88
88
  )
89
89
  },
90
+ // timeAllocated to start responding to a request
91
+ // after this delay the server will respond with 504
92
+ responseTimeout = 60_000 * 10, // 10s
93
+ // time allocated to server code to start reading the request body
94
+ // after this delay the underlying stream is destroyed, attempting to read it would throw
95
+ // if used the stream stays opened, it's only if the stream is not read at all that it gets destroyed
96
+ requestBodyLifetime = 60_000 * 2, // 2s
90
97
  } = {}) => {
91
98
  const logger = createLogger({ logLevel })
92
99
 
@@ -535,7 +542,8 @@ export const startServer = async ({
535
542
  abortController.abort()
536
543
  } else if (!http2Stream.pushAllowed) {
537
544
  abortController.abort()
538
- } else if (responseProperties !== ABORTED_RESPONSE_PROPERTIES) {
545
+ } else if (responseProperties.requestAborted) {
546
+ } else {
539
547
  const responseLength =
540
548
  responseProperties.headers["content-length"] || 0
541
549
  const { effectiveRecvDataLength, remoteWindowSize } =
@@ -609,75 +617,95 @@ export const startServer = async ({
609
617
  let handleRequestReturnValue
610
618
  let errorWhileHandlingRequest = null
611
619
  let handleRequestTimings = serverTiming ? {} : null
612
- try {
613
- handleRequestReturnValue =
614
- await serviceController.callAsyncHooksUntil(
615
- "handleRequest",
616
- request,
617
- {
618
- timing: handleRequestTimings,
619
- warn,
620
- pushResponse: async ({ path, method }) => {
621
- if (typeof path !== "string" || path[0] !== "/") {
622
- addRequestLog(requestNode, {
623
- type: "warn",
624
- value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
625
- })
626
- return
627
- }
628
- if (!request.http2) {
629
- addRequestLog(requestNode, {
630
- type: "warn",
631
- value: `response push ignored because request is not http2`,
632
- })
633
- return
634
- }
635
- const canPushStream = testCanPushStream(nodeResponse.stream)
636
- if (!canPushStream.can) {
637
- addRequestLog(requestNode, {
638
- type: "debug",
639
- value: `response push ignored because ${canPushStream.reason}`,
640
- })
641
- return
642
- }
643
-
644
- let preventedByService = null
645
- const prevent = () => {
646
- preventedByService = serviceController.getCurrentService()
647
- }
648
- serviceController.callHooksUntil(
649
- "onResponsePush",
650
- { path, method },
651
- {
652
- request,
653
- warn,
654
- prevent,
655
- },
656
- () => preventedByService,
657
- )
658
- if (preventedByService) {
659
- addRequestLog(requestNode, {
660
- type: "debug",
661
- value: `response push prevented by "${preventedByService.name}" service`,
662
- })
663
- return
664
- }
665
-
666
- const requestChildNode = { logs: [], children: [] }
667
- requestNode.children.push(requestChildNode)
668
- await pushResponse(
669
- { path, method },
670
- {
671
- requestNode: requestChildNode,
672
- parentHttp2Stream: nodeResponse.stream,
673
- },
674
- )
620
+
621
+ let timeout
622
+ const timeoutPromise = new Promise((resolve) => {
623
+ timeout = setTimeout(() => {
624
+ resolve({
625
+ // the correct status code should be 500 because it's
626
+ // we don't really know what takes time
627
+ // in practice it's often because server is trying to reach an other server
628
+ // that is not responding so 504 is more correct
629
+ status: 504,
630
+ statusText: `server timeout after ${
631
+ responseTimeout / 1000
632
+ }s waiting to handle request`,
633
+ })
634
+ }, responseTimeout)
635
+ })
636
+ const handleRequestPromise = serviceController.callAsyncHooksUntil(
637
+ "handleRequest",
638
+ request,
639
+ {
640
+ timing: handleRequestTimings,
641
+ warn,
642
+ pushResponse: async ({ path, method }) => {
643
+ if (typeof path !== "string" || path[0] !== "/") {
644
+ addRequestLog(requestNode, {
645
+ type: "warn",
646
+ value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
647
+ })
648
+ return
649
+ }
650
+ if (!request.http2) {
651
+ addRequestLog(requestNode, {
652
+ type: "warn",
653
+ value: `response push ignored because request is not http2`,
654
+ })
655
+ return
656
+ }
657
+ const canPushStream = testCanPushStream(nodeResponse.stream)
658
+ if (!canPushStream.can) {
659
+ addRequestLog(requestNode, {
660
+ type: "debug",
661
+ value: `response push ignored because ${canPushStream.reason}`,
662
+ })
663
+ return
664
+ }
665
+
666
+ let preventedByService = null
667
+ const prevent = () => {
668
+ preventedByService = serviceController.getCurrentService()
669
+ }
670
+ serviceController.callHooksUntil(
671
+ "onResponsePush",
672
+ { path, method },
673
+ {
674
+ request,
675
+ warn,
676
+ prevent,
675
677
  },
676
- },
677
- )
678
- } catch (error) {
679
- errorWhileHandlingRequest = error
678
+ () => preventedByService,
679
+ )
680
+ if (preventedByService) {
681
+ addRequestLog(requestNode, {
682
+ type: "debug",
683
+ value: `response push prevented by "${preventedByService.name}" service`,
684
+ })
685
+ return
686
+ }
687
+
688
+ const requestChildNode = { logs: [], children: [] }
689
+ requestNode.children.push(requestChildNode)
690
+ await pushResponse(
691
+ { path, method },
692
+ {
693
+ requestNode: requestChildNode,
694
+ parentHttp2Stream: nodeResponse.stream,
695
+ },
696
+ )
697
+ },
698
+ },
699
+ )
700
+ try {
701
+ handleRequestReturnValue = await Promise.race([
702
+ timeoutPromise,
703
+ handleRequestPromise,
704
+ ])
705
+ } catch (e) {
706
+ errorWhileHandlingRequest = e
680
707
  }
708
+ clearTimeout(timeout)
681
709
 
682
710
  let responseProperties
683
711
  if (errorWhileHandlingRequest) {
@@ -685,7 +713,7 @@ export const startServer = async ({
685
713
  errorWhileHandlingRequest.name === "AbortError" &&
686
714
  request.signal.aborted
687
715
  ) {
688
- responseProperties = ABORTED_RESPONSE_PROPERTIES
716
+ responseProperties = { requestAborted: true }
689
717
  } else {
690
718
  // internal error, create 500 response
691
719
  if (
@@ -818,7 +846,7 @@ export const startServer = async ({
818
846
  await new Promise((resolve) => setTimeout(resolve))
819
847
  }
820
848
 
821
- await populateNodeResponse(responseStream, responseProperties, {
849
+ await writeNodeResponse(responseStream, responseProperties, {
822
850
  signal,
823
851
  ignoreBody,
824
852
  onAbort: () => {
@@ -927,6 +955,7 @@ export const startServer = async ({
927
955
  const request = fromNodeRequest(nodeRequest, {
928
956
  serverOrigin: websocketOrigin,
929
957
  signal: new AbortController().signal,
958
+ requestBodyLifetime,
930
959
  })
931
960
  serviceController.callAsyncHooksUntil(
932
961
  "handleWebsocket",
@@ -1039,8 +1068,6 @@ const testCanPushStream = (http2Stream) => {
1039
1068
  }
1040
1069
  }
1041
1070
 
1042
- const ABORTED_RESPONSE_PROPERTIES = {}
1043
-
1044
1071
  const PROCESS_TEARDOWN_EVENTS_MAP = {
1045
1072
  SIGHUP: STOP_REASON_PROCESS_SIGHUP,
1046
1073
  SIGTERM: STOP_REASON_PROCESS_SIGTERM,