@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 +1 -1
- package/src/{internal → interfacing_with_node}/body.js +3 -3
- package/src/{internal → interfacing_with_node}/observable.js +0 -0
- package/src/{internal/nodeStreamToObservable.js → interfacing_with_node/observable_from_node_stream.js} +10 -5
- package/src/{internal → interfacing_with_node}/request_factory.js +9 -4
- package/src/{internal/populateNodeResponse.js → interfacing_with_node/write_node_response.js} +1 -1
- package/src/sse/sse_room.js +1 -1
- package/src/start_server.js +101 -74
package/package.json
CHANGED
|
@@ -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 {
|
|
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: () =>
|
|
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
|
|
64
|
+
return observableFromNodeStream(fileHandleToReadableStream(fileHandle))
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const isNodeStream = (value) => {
|
|
File without changes
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { Readable } from "node:stream"
|
|
3
3
|
import { createObservable } from "./observable.js"
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
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 ${
|
|
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
|
-
},
|
|
54
|
+
}, readableStreamLifetime)
|
|
50
55
|
observable.timeout = timeout
|
|
51
56
|
onceReadableStreamUsedOrClosed(nodeStream, () => {
|
|
52
57
|
clearTimeout(timeout)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { headersFromObject } from "
|
|
1
|
+
import { observableFromNodeStream } from "./observable_from_node_stream.js"
|
|
2
|
+
import { headersFromObject } from "../internal/headersFromObject.js"
|
|
3
3
|
|
|
4
|
-
export const fromNodeRequest = (
|
|
4
|
+
export const fromNodeRequest = (
|
|
5
|
+
nodeRequest,
|
|
6
|
+
{ serverOrigin, signal, requestBodyLifetime },
|
|
7
|
+
) => {
|
|
5
8
|
const headers = headersFromObject(nodeRequest.headers)
|
|
6
|
-
const body =
|
|
9
|
+
const body = observableFromNodeStream(nodeRequest, {
|
|
10
|
+
readableStreamLifetime: requestBodyLifetime,
|
|
11
|
+
})
|
|
7
12
|
|
|
8
13
|
let requestOrigin
|
|
9
14
|
if (nodeRequest.upgrade) {
|
package/src/{internal/populateNodeResponse.js → interfacing_with_node/write_node_response.js}
RENAMED
|
@@ -4,7 +4,7 @@ import { raceCallbacks } from "@jsenv/abort"
|
|
|
4
4
|
|
|
5
5
|
import { normalizeBodyMethods } from "./body.js"
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const writeNodeResponse = async (
|
|
8
8
|
responseStream,
|
|
9
9
|
{ status, statusText, headers, body, bodyEncoding },
|
|
10
10
|
{ signal, ignoreBody, onAbort, onError, onHeadersSent, onEnd } = {},
|
package/src/sse/sse_room.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createLogger } from "@jsenv/log"
|
|
|
3
3
|
import {
|
|
4
4
|
createObservable,
|
|
5
5
|
createCompositeProducer,
|
|
6
|
-
} from "@jsenv/server/src/
|
|
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 = ({
|
package/src/start_server.js
CHANGED
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
fromNodeRequest,
|
|
19
19
|
createPushRequest,
|
|
20
20
|
applyRedirectionToRequest,
|
|
21
|
-
} from "./
|
|
22
|
-
import {
|
|
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
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
679
|
-
|
|
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 =
|
|
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
|
|
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,
|