@jsenv/server 10.2.1 → 12.2.0
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/README.md +6 -3
- package/main.js +3 -5
- package/package.json +17 -20
- package/src/internal/request_factory.js +78 -124
- package/src/internal/server-polyglot.js +10 -10
- package/src/plugin_request_waiting_check.js +0 -2
- package/src/ressource_aliases/plugin_ressource_aliases.js +55 -0
- package/src/service_composition/routing.js +35 -0
- package/src/service_composition/service_composition.js +7 -41
- package/src/sse/createSSERoom.js +16 -0
- package/src/startServer.js +76 -52
package/README.md
CHANGED
|
@@ -70,7 +70,10 @@ const notFoundService = (request) => {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const server = await startServer({
|
|
73
|
-
requestToResponse: composeServices(
|
|
73
|
+
requestToResponse: composeServices({
|
|
74
|
+
index: indexService,
|
|
75
|
+
otherwise: notFoundService,
|
|
76
|
+
}),
|
|
74
77
|
})
|
|
75
78
|
|
|
76
79
|
const fetch = await import("node-fetch")
|
|
@@ -89,8 +92,8 @@ import { startServer } from "@jsenv/server"
|
|
|
89
92
|
|
|
90
93
|
await startServer({
|
|
91
94
|
protocol: "https",
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
certificate: readFileSyncAsString("./server.crt"),
|
|
96
|
+
privateKey: readFileSyncAsString("./server.key"),
|
|
94
97
|
allowHttpRequestOnHttps: true,
|
|
95
98
|
requestToResponse: (request) => {
|
|
96
99
|
const clientUsesHttp = request.origin.startsWith("http:")
|
package/main.js
CHANGED
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { startServer } from "./src/startServer.js"
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
composeServicesWithTiming,
|
|
11
|
-
redirectRequest,
|
|
12
|
-
} from "./src/service_composition/service_composition.js"
|
|
8
|
+
export { composeServices } from "./src/service_composition/service_composition.js"
|
|
9
|
+
export { setupRoutes } from "./src/service_composition/routing.js"
|
|
13
10
|
export { jsenvServerInternalErrorToResponse } from "./src/jsenvServerInternalErrorToResponse.js"
|
|
14
11
|
export { readRequestBody } from "./src/readRequestBody.js"
|
|
15
12
|
export { fetchFileSystem } from "./src/fetch_filesystem.js"
|
|
@@ -46,6 +43,7 @@ export { pluginContentNegotiationCheck } from "./src/content_negotiation/plugin_
|
|
|
46
43
|
// others
|
|
47
44
|
export { fetchUrl } from "./src/fetch_url.js"
|
|
48
45
|
export { composeTwoResponses } from "./src/internal/response_composition.js"
|
|
46
|
+
export { pluginRessourceAliases } from "./src/ressource_aliases/plugin_ressource_aliases.js"
|
|
49
47
|
export { pluginRequestWaitingCheck } from "./src/plugin_request_waiting_check.js"
|
|
50
48
|
export { findFreePort } from "./src/internal/listen.js"
|
|
51
49
|
export { urlToContentType } from "./src/urlToContentType.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "Write your Node.js server using pure functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/jsenv/server"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": ">=16.12.0"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
},
|
|
26
26
|
"./*": "./*"
|
|
27
27
|
},
|
|
28
|
+
"main": "./main.js",
|
|
28
29
|
"files": [
|
|
29
30
|
"/src/",
|
|
30
31
|
"/main.js"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
|
-
"eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
|
|
34
34
|
"importmap-generate": "node ./script/importmap/generate_importmap.mjs",
|
|
35
35
|
"measure-performances": "node --expose-gc ./script/performance/generate_performance_report.mjs --log",
|
|
36
|
-
"test": "node
|
|
36
|
+
"test": "node ./script/test/test.mjs",
|
|
37
37
|
"test-with-coverage": "npm run test -- --coverage",
|
|
38
38
|
"generate-html-snapshots": "node ./test/internal_error/generate_html_snapshot_files.mjs",
|
|
39
|
-
"
|
|
40
|
-
"prettier-
|
|
41
|
-
"prettier-
|
|
39
|
+
"eslint-check": "node ./node_modules/eslint/bin/eslint.js . --ext=.js,.mjs",
|
|
40
|
+
"prettier-check": "prettier --check .",
|
|
41
|
+
"prettier-write": "prettier --write .",
|
|
42
42
|
"certificate-authority-install": "node ./script/certificate/certificate_authority_install.mjs",
|
|
43
43
|
"playwright-install": "npx playwright install-deps && npx playwright install",
|
|
44
44
|
"prepublishOnly": "node ./script/publish/remove_postinstall.mjs",
|
|
@@ -48,28 +48,25 @@
|
|
|
48
48
|
"@jsenv/abort": "4.1.1",
|
|
49
49
|
"@jsenv/logger": "4.0.1",
|
|
50
50
|
"@jsenv/url-meta": "6.0.1",
|
|
51
|
-
"node-fetch": "2.6.2"
|
|
51
|
+
"node-fetch": "2.6.2",
|
|
52
|
+
"path-to-regexp": "6.2.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@babel/core": "7.16.0",
|
|
55
|
-
"@babel/eslint-parser": "7.16.0",
|
|
56
55
|
"@jsenv/assert": "2.3.2",
|
|
57
|
-
"@jsenv/
|
|
58
|
-
"@jsenv/
|
|
59
|
-
"@jsenv/eslint-config": "16.0.8",
|
|
56
|
+
"@jsenv/core": "23.10.0",
|
|
57
|
+
"@jsenv/eslint-config": "16.0.9",
|
|
60
58
|
"@jsenv/filesystem": "2.5.1",
|
|
61
59
|
"@jsenv/github-release-package": "1.2.3",
|
|
62
|
-
"@jsenv/https-local": "1.0.
|
|
60
|
+
"@jsenv/https-local": "1.0.3",
|
|
63
61
|
"@jsenv/importmap-eslint-resolver": "5.1.2",
|
|
64
|
-
"@jsenv/importmap-node-module": "2.
|
|
62
|
+
"@jsenv/importmap-node-module": "2.7.1",
|
|
65
63
|
"@jsenv/package-publish": "1.6.2",
|
|
66
64
|
"@jsenv/performance-impact": "2.2.1",
|
|
67
|
-
"
|
|
68
|
-
"eslint": "
|
|
69
|
-
"eslint-plugin-import": "2.25.2",
|
|
65
|
+
"eslint": "8.2.0",
|
|
66
|
+
"eslint-plugin-import": "2.25.3",
|
|
70
67
|
"eventsource": "1.1.0",
|
|
71
|
-
"fastify": "3.
|
|
72
|
-
"fastify-static": "4.
|
|
68
|
+
"fastify": "3.24.0",
|
|
69
|
+
"fastify-static": "4.5.0",
|
|
73
70
|
"playwright": "1.16.3",
|
|
74
71
|
"prettier": "2.4.1"
|
|
75
72
|
}
|
|
@@ -1,125 +1,93 @@
|
|
|
1
|
-
import { urlToMeta, normalizeStructuredMetaMap } from "@jsenv/url-meta"
|
|
2
|
-
|
|
3
1
|
import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
|
|
4
2
|
import { headersFromObject } from "./headersFromObject.js"
|
|
5
3
|
|
|
6
|
-
export const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return alias
|
|
4
|
+
export const fromNodeRequest = (nodeRequest, { serverOrigin, signal }) => {
|
|
5
|
+
const headers = headersFromObject(nodeRequest.headers)
|
|
6
|
+
const body = nodeStreamToObservable(nodeRequest)
|
|
7
|
+
|
|
8
|
+
let requestOrigin
|
|
9
|
+
if (nodeRequest.authority) {
|
|
10
|
+
requestOrigin = nodeRequest.connection.encrypted
|
|
11
|
+
? `https://${nodeRequest.authority}`
|
|
12
|
+
: `http://${nodeRequest.authority}`
|
|
13
|
+
} else if (nodeRequest.headers.host) {
|
|
14
|
+
requestOrigin = nodeRequest.connection.encrypted
|
|
15
|
+
? `https://${nodeRequest.headers.host}`
|
|
16
|
+
: `http://${nodeRequest.headers.host}`
|
|
17
|
+
} else {
|
|
18
|
+
requestOrigin = serverOrigin
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
signal,
|
|
23
|
+
http2: Boolean(nodeRequest.stream),
|
|
24
|
+
origin: requestOrigin,
|
|
25
|
+
...getPropertiesFromRessource({
|
|
26
|
+
ressource: nodeRequest.url,
|
|
27
|
+
baseUrl: requestOrigin,
|
|
28
|
+
}),
|
|
29
|
+
method: nodeRequest.method,
|
|
30
|
+
headers,
|
|
31
|
+
body,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
export const applyRedirectionToRequest = (
|
|
36
|
+
request,
|
|
37
|
+
{ ressource, pathname, ...rest },
|
|
38
|
+
) => {
|
|
39
|
+
return {
|
|
40
|
+
...request,
|
|
41
|
+
...(ressource
|
|
42
|
+
? getPropertiesFromRessource({
|
|
43
|
+
ressource,
|
|
44
|
+
baseUrl: request.url,
|
|
45
|
+
})
|
|
46
|
+
: pathname
|
|
47
|
+
? getPropertiesFromPathname({
|
|
48
|
+
pathname,
|
|
49
|
+
baseUrl: request.url,
|
|
50
|
+
})
|
|
51
|
+
: {}),
|
|
52
|
+
...rest,
|
|
44
53
|
}
|
|
54
|
+
}
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
baseUrl,
|
|
50
|
-
})
|
|
51
|
-
}
|
|
56
|
+
const getPropertiesFromRessource = ({ ressource, baseUrl }) => {
|
|
57
|
+
const urlObject = new URL(ressource, baseUrl)
|
|
58
|
+
let pathname = urlObject.pathname
|
|
52
59
|
|
|
53
60
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (nodeRequest.authority) {
|
|
60
|
-
requestOrigin = nodeRequest.connection.encrypted
|
|
61
|
-
? `https://${nodeRequest.authority}`
|
|
62
|
-
: `http://${nodeRequest.authority}`
|
|
63
|
-
} else if (nodeRequest.headers.host) {
|
|
64
|
-
requestOrigin = nodeRequest.connection.encrypted
|
|
65
|
-
? `https://${nodeRequest.headers.host}`
|
|
66
|
-
: `http://${nodeRequest.headers.host}`
|
|
67
|
-
} else {
|
|
68
|
-
requestOrigin = serverOrigin
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return Object.freeze({
|
|
72
|
-
signal,
|
|
73
|
-
http2: Boolean(nodeRequest.stream),
|
|
74
|
-
origin: requestOrigin,
|
|
75
|
-
...getPropertiesFromRessource({
|
|
76
|
-
ressource: nodeRequest.url,
|
|
77
|
-
baseUrl: requestOrigin,
|
|
78
|
-
}),
|
|
79
|
-
method: nodeRequest.method,
|
|
80
|
-
headers,
|
|
81
|
-
body,
|
|
82
|
-
})
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
createPushRequest: (request, { signal, pathname, method }) => {
|
|
86
|
-
const pushRequest = Object.freeze({
|
|
87
|
-
...request,
|
|
88
|
-
parent: request,
|
|
89
|
-
signal,
|
|
90
|
-
http2: true,
|
|
91
|
-
...(pathname
|
|
92
|
-
? getPropertiesFromPathname({
|
|
93
|
-
pathname,
|
|
94
|
-
baseUrl: request.url,
|
|
95
|
-
})
|
|
96
|
-
: {}),
|
|
97
|
-
method: method || request.method,
|
|
98
|
-
headers: getHeadersInheritedByPushRequest(request),
|
|
99
|
-
body: undefined,
|
|
100
|
-
})
|
|
61
|
+
url: String(urlObject),
|
|
62
|
+
pathname,
|
|
63
|
+
ressource,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
101
66
|
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
const getPropertiesFromPathname = ({ pathname, baseUrl }) => {
|
|
68
|
+
return getPropertiesFromRessource({
|
|
69
|
+
ressource: `${pathname}${new URL(baseUrl).search}`,
|
|
70
|
+
baseUrl,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
104
73
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
74
|
+
export const createPushRequest = (request, { signal, pathname, method }) => {
|
|
75
|
+
const pushRequest = Object.freeze({
|
|
76
|
+
...request,
|
|
77
|
+
parent: request,
|
|
78
|
+
signal,
|
|
79
|
+
http2: true,
|
|
80
|
+
...(pathname
|
|
81
|
+
? getPropertiesFromPathname({
|
|
82
|
+
pathname,
|
|
83
|
+
baseUrl: request.url,
|
|
84
|
+
})
|
|
85
|
+
: {}),
|
|
86
|
+
method: method || request.method,
|
|
87
|
+
headers: getHeadersInheritedByPushRequest(request),
|
|
88
|
+
body: undefined,
|
|
89
|
+
})
|
|
90
|
+
return pushRequest
|
|
123
91
|
}
|
|
124
92
|
|
|
125
93
|
const getHeadersInheritedByPushRequest = (request) => {
|
|
@@ -135,17 +103,3 @@ const getHeadersInheritedByPushRequest = (request) => {
|
|
|
135
103
|
|
|
136
104
|
return headersInherited
|
|
137
105
|
}
|
|
138
|
-
|
|
139
|
-
const replaceUrlRessource = (url, ressource) => {
|
|
140
|
-
const urlObject = new URL(url)
|
|
141
|
-
|
|
142
|
-
const searchSeparatorIndex = ressource.indexOf("?")
|
|
143
|
-
if (searchSeparatorIndex === -1) {
|
|
144
|
-
urlObject.pathname = ressource
|
|
145
|
-
return urlObject
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const pathname = ressource.slice(0, searchSeparatorIndex)
|
|
149
|
-
urlObject.pathname = pathname
|
|
150
|
-
return urlObject
|
|
151
|
-
}
|
|
@@ -11,13 +11,13 @@ import { listenEvent } from "./listenEvent.js"
|
|
|
11
11
|
export const createPolyglotServer = async ({
|
|
12
12
|
http2 = false,
|
|
13
13
|
http1Allowed = true,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
certificate,
|
|
15
|
+
privateKey,
|
|
16
16
|
}) => {
|
|
17
17
|
const httpServer = http.createServer()
|
|
18
18
|
const tlsServer = await createSecureServer({
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
certificate,
|
|
20
|
+
privateKey,
|
|
21
21
|
http2,
|
|
22
22
|
http1Allowed,
|
|
23
23
|
})
|
|
@@ -63,24 +63,24 @@ export const createPolyglotServer = async ({
|
|
|
63
63
|
// The async part is just to lazyly import "http2" or "https"
|
|
64
64
|
// so that these module are parsed only if used.
|
|
65
65
|
const createSecureServer = async ({
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
certificate,
|
|
67
|
+
privateKey,
|
|
68
68
|
http2,
|
|
69
69
|
http1Allowed,
|
|
70
70
|
}) => {
|
|
71
71
|
if (http2) {
|
|
72
72
|
const { createSecureServer } = await import("http2")
|
|
73
73
|
return createSecureServer({
|
|
74
|
-
cert:
|
|
75
|
-
key:
|
|
74
|
+
cert: certificate,
|
|
75
|
+
key: privateKey,
|
|
76
76
|
allowHTTP1: http1Allowed,
|
|
77
77
|
})
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const { createServer } = await import("https")
|
|
81
81
|
return createServer({
|
|
82
|
-
cert:
|
|
83
|
-
key:
|
|
82
|
+
cert: certificate,
|
|
83
|
+
key: privateKey,
|
|
84
84
|
})
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -16,8 +16,6 @@ export const pluginRequestWaitingCheck = ({
|
|
|
16
16
|
} = {}) => {
|
|
17
17
|
return {
|
|
18
18
|
plugin_request_waiting_check: {
|
|
19
|
-
init: () => {},
|
|
20
|
-
|
|
21
19
|
onRequest: (request, { logger }) => {
|
|
22
20
|
const timeout = setTimeout(
|
|
23
21
|
() => requestWaitingCallback({ request, logger, requestWaitingMs }),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { urlToMeta, normalizeStructuredMetaMap } from "@jsenv/url-meta"
|
|
2
|
+
|
|
3
|
+
export const pluginRessourceAliases = (ressourceAliases) => {
|
|
4
|
+
const ressourceAliasMap = {}
|
|
5
|
+
Object.keys(ressourceAliases).forEach((key) => {
|
|
6
|
+
ressourceAliasMap[ressourceAliases[key]] = key
|
|
7
|
+
})
|
|
8
|
+
const structuredMetaMap = normalizeStructuredMetaMap(
|
|
9
|
+
{ alias: ressourceAliasMap },
|
|
10
|
+
"file:///",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const getRessourceAlias = (pathname) => {
|
|
14
|
+
const { alias } = urlToMeta({
|
|
15
|
+
url: `file://${pathname}`,
|
|
16
|
+
structuredMetaMap,
|
|
17
|
+
})
|
|
18
|
+
return alias
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
ressource_aliases: {
|
|
23
|
+
onRequest: (request, { redirectRequest }) => {
|
|
24
|
+
const ressourceAlias = getRessourceAlias(request.pathname)
|
|
25
|
+
if (!ressourceAlias) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const urlObjectAfterAlias = replaceUrlRessource(
|
|
30
|
+
request.url,
|
|
31
|
+
ressourceAlias,
|
|
32
|
+
)
|
|
33
|
+
const ressource = `${urlObjectAfterAlias.pathname}${urlObjectAfterAlias.search}`
|
|
34
|
+
redirectRequest({
|
|
35
|
+
ressourceBeforeAlias: request.ressource,
|
|
36
|
+
ressource,
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const replaceUrlRessource = (url, ressource) => {
|
|
44
|
+
const urlObject = new URL(url)
|
|
45
|
+
|
|
46
|
+
const searchSeparatorIndex = ressource.indexOf("?")
|
|
47
|
+
if (searchSeparatorIndex === -1) {
|
|
48
|
+
urlObject.pathname = ressource
|
|
49
|
+
return urlObject
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pathname = ressource.slice(0, searchSeparatorIndex)
|
|
53
|
+
urlObject.pathname = pathname
|
|
54
|
+
return urlObject
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url)
|
|
4
|
+
|
|
5
|
+
export const setupRoutes = (routes) => {
|
|
6
|
+
const { match } = require("path-to-regexp")
|
|
7
|
+
|
|
8
|
+
const candidates = Object.keys(routes).map((pathPattern) => {
|
|
9
|
+
const applyPatternMatching = match(pathPattern, {
|
|
10
|
+
decode: decodeURIComponent,
|
|
11
|
+
})
|
|
12
|
+
return {
|
|
13
|
+
applyPatternMatching,
|
|
14
|
+
requestHandler: routes[pathPattern],
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return (request, { pushResponse, redirectRequest }) => {
|
|
19
|
+
let result
|
|
20
|
+
const found = candidates.find((candidate) => {
|
|
21
|
+
result = candidate.applyPatternMatching(request.pathname)
|
|
22
|
+
return Boolean(result)
|
|
23
|
+
})
|
|
24
|
+
if (found) {
|
|
25
|
+
return found.requestHandler(
|
|
26
|
+
{
|
|
27
|
+
...request,
|
|
28
|
+
routeParams: result.params,
|
|
29
|
+
},
|
|
30
|
+
{ pushResponse, redirectRequest },
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
import { timeFunction } from "@jsenv/server/src/server_timing/timing_measure.js"
|
|
2
2
|
import { composeTwoResponses } from "@jsenv/server/src/internal/response_composition.js"
|
|
3
|
+
import { applyRedirectionToRequest } from "@jsenv/server/src/internal/request_factory.js"
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const composeServices = (...callbacks) => {
|
|
13
|
-
return (request, { pushResponse, requestFactory }) => {
|
|
14
|
-
return firstOperationMatching({
|
|
15
|
-
array: callbacks,
|
|
16
|
-
start: (callback) => callback(request, { pushResponse }),
|
|
17
|
-
predicate: (returnValue) => {
|
|
18
|
-
if (returnValue === null || typeof returnValue !== "object") {
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
const requestRedirection = returnValue[requestRedirectionSymbol]
|
|
22
|
-
if (requestRedirection) {
|
|
23
|
-
request = requestFactory.applyRedirectionToRequest(
|
|
24
|
-
request,
|
|
25
|
-
requestRedirection,
|
|
26
|
-
)
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
return true
|
|
30
|
-
},
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
}
|
|
5
|
+
export const composeServices = (namedServices) => {
|
|
6
|
+
return async (request, { pushResponse }) => {
|
|
7
|
+
const redirectRequest = (requestRedirection) => {
|
|
8
|
+
request = applyRedirectionToRequest(request, requestRedirection)
|
|
9
|
+
}
|
|
34
10
|
|
|
35
|
-
export const composeServicesWithTiming = (namedServices) => {
|
|
36
|
-
return async (request, { pushResponse, requestFactory }) => {
|
|
37
11
|
const servicesTiming = {}
|
|
38
12
|
const response = await firstOperationMatching({
|
|
39
13
|
array: Object.keys(namedServices).map((serviceName) => {
|
|
@@ -41,7 +15,7 @@ export const composeServicesWithTiming = (namedServices) => {
|
|
|
41
15
|
}),
|
|
42
16
|
start: async ({ serviceName, serviceFn }) => {
|
|
43
17
|
const [serviceTiming, value] = await timeFunction(serviceName, () =>
|
|
44
|
-
serviceFn(request, { pushResponse }),
|
|
18
|
+
serviceFn(request, { pushResponse, redirectRequest }),
|
|
45
19
|
)
|
|
46
20
|
Object.assign(servicesTiming, serviceTiming)
|
|
47
21
|
return value
|
|
@@ -50,14 +24,6 @@ export const composeServicesWithTiming = (namedServices) => {
|
|
|
50
24
|
if (returnValue === null || typeof returnValue !== "object") {
|
|
51
25
|
return false
|
|
52
26
|
}
|
|
53
|
-
const requestRedirection = returnValue[requestRedirectionSymbol]
|
|
54
|
-
if (requestRedirection) {
|
|
55
|
-
request = requestFactory.applyRedirectionToRequest(
|
|
56
|
-
request,
|
|
57
|
-
requestRedirection,
|
|
58
|
-
)
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
27
|
return true
|
|
62
28
|
},
|
|
63
29
|
})
|
package/src/sse/createSSERoom.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
// https://www.html5rocks.com/en/tutorials/eventsource/basics/
|
|
9
9
|
export const createSSERoom = ({
|
|
10
10
|
logLevel,
|
|
11
|
+
effect = () => {},
|
|
11
12
|
// do not keep process alive because of rooms, something else must keep it alive
|
|
12
13
|
keepProcessAlive = false,
|
|
13
14
|
keepaliveDuration = 30 * 1000,
|
|
@@ -29,6 +30,7 @@ export const createSSERoom = ({
|
|
|
29
30
|
let previousEventId = 0
|
|
30
31
|
let opened = false
|
|
31
32
|
let interval
|
|
33
|
+
let cleanupEffect = CLEANUP_NOOP
|
|
32
34
|
|
|
33
35
|
const join = (request) => {
|
|
34
36
|
// should we ensure a given request can join a room only once?
|
|
@@ -56,6 +58,14 @@ export const createSSERoom = ({
|
|
|
56
58
|
next,
|
|
57
59
|
complete,
|
|
58
60
|
}
|
|
61
|
+
if (clients.size === 0) {
|
|
62
|
+
const effectReturnValue = effect()
|
|
63
|
+
if (typeof effectReturnValue === "function") {
|
|
64
|
+
cleanupEffect = effectReturnValue
|
|
65
|
+
} else {
|
|
66
|
+
cleanupEffect = CLEANUP_NOOP
|
|
67
|
+
}
|
|
68
|
+
}
|
|
59
69
|
clients.add(client)
|
|
60
70
|
logger.debug(
|
|
61
71
|
`A client has joined. Number of client in room: ${clients.size}`,
|
|
@@ -101,6 +111,10 @@ export const createSSERoom = ({
|
|
|
101
111
|
|
|
102
112
|
return () => {
|
|
103
113
|
clients.delete(client)
|
|
114
|
+
if (clients.size === 0) {
|
|
115
|
+
cleanupEffect()
|
|
116
|
+
cleanupEffect = CLEANUP_NOOP
|
|
117
|
+
}
|
|
104
118
|
logger.debug(`A client left. Number of client in room: ${clients.size}`)
|
|
105
119
|
}
|
|
106
120
|
})
|
|
@@ -210,6 +224,8 @@ export const createSSERoom = ({
|
|
|
210
224
|
return room
|
|
211
225
|
}
|
|
212
226
|
|
|
227
|
+
const CLEANUP_NOOP = () => {}
|
|
228
|
+
|
|
213
229
|
const requestMap = new Map()
|
|
214
230
|
|
|
215
231
|
const connectRequestAndRoom = (request, room, roomObservable) => {
|
package/src/startServer.js
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
import { createPolyglotServer } from "./internal/server-polyglot.js"
|
|
12
12
|
import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"
|
|
13
13
|
import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
fromNodeRequest,
|
|
16
|
+
createPushRequest,
|
|
17
|
+
applyRedirectionToRequest,
|
|
18
|
+
} from "./internal/request_factory.js"
|
|
15
19
|
import { populateNodeResponse } from "./internal/populateNodeResponse.js"
|
|
16
20
|
import { colorizeResponseStatus } from "./internal/colorizeResponseStatus.js"
|
|
17
21
|
import { getServerOrigins } from "./internal/getServerOrigins.js"
|
|
@@ -43,8 +47,8 @@ export const startServer = async ({
|
|
|
43
47
|
ip = "0.0.0.0", // will it work on windows ? https://github.com/nodejs/node/issues/14900
|
|
44
48
|
port = 0, // assign a random available port
|
|
45
49
|
portHint,
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
privateKey,
|
|
51
|
+
certificate,
|
|
48
52
|
|
|
49
53
|
// when inside a worker, we should not try to stop server on SIGINT
|
|
50
54
|
// otherwise it can create an EPIPE error while primary process tries
|
|
@@ -56,7 +60,6 @@ export const startServer = async ({
|
|
|
56
60
|
stopOnInternalError = false,
|
|
57
61
|
keepProcessAlive = true,
|
|
58
62
|
requestToResponse = () => null,
|
|
59
|
-
ressourceAliases = {},
|
|
60
63
|
plugins = {},
|
|
61
64
|
|
|
62
65
|
sendServerInternalErrorDetails = false,
|
|
@@ -71,11 +74,11 @@ export const startServer = async ({
|
|
|
71
74
|
throw new Error(`protocol must be http or https, got ${protocol}`)
|
|
72
75
|
}
|
|
73
76
|
if (protocol === "https") {
|
|
74
|
-
if (!
|
|
75
|
-
throw new Error(`missing
|
|
77
|
+
if (!certificate) {
|
|
78
|
+
throw new Error(`missing certificate for https server`)
|
|
76
79
|
}
|
|
77
|
-
if (!
|
|
78
|
-
throw new Error(`missing
|
|
80
|
+
if (!privateKey) {
|
|
81
|
+
throw new Error(`missing privateKey for https server`)
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
if (http2 && protocol !== "https") {
|
|
@@ -131,8 +134,8 @@ export const startServer = async ({
|
|
|
131
134
|
protocol,
|
|
132
135
|
redirectHttpToHttps,
|
|
133
136
|
allowHttpRequestOnHttps,
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
certificate,
|
|
138
|
+
privateKey,
|
|
136
139
|
http2,
|
|
137
140
|
http1Allowed,
|
|
138
141
|
})
|
|
@@ -226,11 +229,6 @@ export const startServer = async ({
|
|
|
226
229
|
})
|
|
227
230
|
})
|
|
228
231
|
|
|
229
|
-
const requestFactory = createRequestFactory({
|
|
230
|
-
serverOrigin,
|
|
231
|
-
ressourceAliases,
|
|
232
|
-
})
|
|
233
|
-
|
|
234
232
|
const requestCallback = async (nodeRequest, nodeResponse) => {
|
|
235
233
|
if (!nagle) {
|
|
236
234
|
nodeRequest.connection.setNoDelay(true)
|
|
@@ -268,7 +266,8 @@ export const startServer = async ({
|
|
|
268
266
|
return stopCallbackList.add(abort)
|
|
269
267
|
})
|
|
270
268
|
|
|
271
|
-
const request =
|
|
269
|
+
const request = fromNodeRequest(nodeRequest, {
|
|
270
|
+
serverOrigin,
|
|
272
271
|
signal: receiveRequestOperation.signal,
|
|
273
272
|
})
|
|
274
273
|
|
|
@@ -333,29 +332,6 @@ export const startServer = async ({
|
|
|
333
332
|
const pushResponse = async ({ path, method }, { requestNode }) => {
|
|
334
333
|
const http2Stream = nodeResponse.stream
|
|
335
334
|
|
|
336
|
-
if (typeof path !== "string" || path[0] !== "/") {
|
|
337
|
-
addRequestLog(requestNode, {
|
|
338
|
-
type: "warn",
|
|
339
|
-
value: `pushResponse call ignored because path is invalid (must be a string starting with "/", found ${path})`,
|
|
340
|
-
})
|
|
341
|
-
return
|
|
342
|
-
}
|
|
343
|
-
if (!request.http2) {
|
|
344
|
-
addRequestLog(requestNode, {
|
|
345
|
-
type: "warn",
|
|
346
|
-
value: `pushResponse call ignored because request is not http2`,
|
|
347
|
-
})
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
const canPushStream = testCanPushStream(http2Stream)
|
|
351
|
-
if (!canPushStream.can) {
|
|
352
|
-
addRequestLog(requestNode, {
|
|
353
|
-
type: "debug",
|
|
354
|
-
value: `Ignoring call to pushResponse because ${canPushStream.reason}`,
|
|
355
|
-
})
|
|
356
|
-
return
|
|
357
|
-
}
|
|
358
|
-
|
|
359
335
|
// being able to push a stream is nice to have
|
|
360
336
|
// so when it fails it's not critical
|
|
361
337
|
const onPushStreamError = (e) => {
|
|
@@ -414,7 +390,7 @@ export const startServer = async ({
|
|
|
414
390
|
pushResponseOperation.addAbortSignal(signal)
|
|
415
391
|
pushResponseOperation.addAbortSignal(abortController.signal)
|
|
416
392
|
|
|
417
|
-
const pushRequest =
|
|
393
|
+
const pushRequest = createPushRequest(request, {
|
|
418
394
|
signal: pushResponseOperation.signal,
|
|
419
395
|
pathname: path,
|
|
420
396
|
method,
|
|
@@ -476,15 +452,23 @@ export const startServer = async ({
|
|
|
476
452
|
const shortcircuitResponse = (value) => {
|
|
477
453
|
responseFromShortcircuit = value
|
|
478
454
|
}
|
|
455
|
+
const redirectRequest = (requestRedirection) => {
|
|
456
|
+
request = applyRedirectionToRequest(request, requestRedirection)
|
|
457
|
+
}
|
|
479
458
|
Object.keys(plugins).forEach((pluginName) => {
|
|
480
459
|
const { onRequest } = plugins[pluginName]
|
|
481
460
|
if (onRequest) {
|
|
482
461
|
const returnValue = onRequest(request, {
|
|
483
462
|
warn,
|
|
463
|
+
logger,
|
|
484
464
|
shortcircuitResponse,
|
|
465
|
+
redirectRequest,
|
|
485
466
|
})
|
|
486
467
|
if (returnValue) {
|
|
487
|
-
requestPlugins.push(
|
|
468
|
+
requestPlugins.push({
|
|
469
|
+
pluginName,
|
|
470
|
+
...returnValue,
|
|
471
|
+
})
|
|
488
472
|
}
|
|
489
473
|
}
|
|
490
474
|
})
|
|
@@ -493,10 +477,53 @@ export const startServer = async ({
|
|
|
493
477
|
: await generateResponseDescription({
|
|
494
478
|
requestToResponse,
|
|
495
479
|
request,
|
|
496
|
-
pushResponse: ({ path, method }) => {
|
|
480
|
+
pushResponse: async ({ path, method }) => {
|
|
481
|
+
if (typeof path !== "string" || path[0] !== "/") {
|
|
482
|
+
addRequestLog(requestNode, {
|
|
483
|
+
type: "warn",
|
|
484
|
+
value: `response push ignored because path is invalid (must be a string starting with "/", found ${path})`,
|
|
485
|
+
})
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
if (!request.http2) {
|
|
489
|
+
addRequestLog(requestNode, {
|
|
490
|
+
type: "warn",
|
|
491
|
+
value: `response push ignored because request is not http2`,
|
|
492
|
+
})
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
const canPushStream = testCanPushStream(nodeResponse.stream)
|
|
496
|
+
if (!canPushStream.can) {
|
|
497
|
+
addRequestLog(requestNode, {
|
|
498
|
+
type: "debug",
|
|
499
|
+
value: `response push ignored because ${canPushStream.reason}`,
|
|
500
|
+
})
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let prevented = false
|
|
505
|
+
const prevent = () => {
|
|
506
|
+
prevented = true
|
|
507
|
+
}
|
|
508
|
+
const preventedByPlugin = requestPlugins.find((requestPlugin) => {
|
|
509
|
+
const { onPushResponse } = requestPlugin
|
|
510
|
+
if (!onPushResponse) {
|
|
511
|
+
return false
|
|
512
|
+
}
|
|
513
|
+
onPushResponse({ path, method }, { prevent })
|
|
514
|
+
return prevented
|
|
515
|
+
})
|
|
516
|
+
if (prevented) {
|
|
517
|
+
addRequestLog(requestNode, {
|
|
518
|
+
type: "debug",
|
|
519
|
+
value: `response push prevented by "${preventedByPlugin.pluginName}" plugin`,
|
|
520
|
+
})
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
497
524
|
const requestChildNode = { logs: [], children: [] }
|
|
498
525
|
requestNode.children.push(requestChildNode)
|
|
499
|
-
|
|
526
|
+
await pushResponse(
|
|
500
527
|
{ path, method },
|
|
501
528
|
{
|
|
502
529
|
requestNode: requestChildNode,
|
|
@@ -504,7 +531,6 @@ export const startServer = async ({
|
|
|
504
531
|
},
|
|
505
532
|
)
|
|
506
533
|
},
|
|
507
|
-
requestFactory,
|
|
508
534
|
})
|
|
509
535
|
|
|
510
536
|
if (aborted) {
|
|
@@ -698,8 +724,8 @@ const createNodeServer = async ({
|
|
|
698
724
|
protocol,
|
|
699
725
|
redirectHttpToHttps,
|
|
700
726
|
allowHttpRequestOnHttps,
|
|
701
|
-
|
|
702
|
-
|
|
727
|
+
certificate,
|
|
728
|
+
privateKey,
|
|
703
729
|
http2,
|
|
704
730
|
http1Allowed,
|
|
705
731
|
}) => {
|
|
@@ -709,8 +735,8 @@ const createNodeServer = async ({
|
|
|
709
735
|
|
|
710
736
|
if (redirectHttpToHttps || allowHttpRequestOnHttps) {
|
|
711
737
|
return createPolyglotServer({
|
|
712
|
-
|
|
713
|
-
|
|
738
|
+
certificate,
|
|
739
|
+
privateKey,
|
|
714
740
|
http2,
|
|
715
741
|
http1Allowed,
|
|
716
742
|
})
|
|
@@ -718,8 +744,8 @@ const createNodeServer = async ({
|
|
|
718
744
|
|
|
719
745
|
const { createServer } = await import("https")
|
|
720
746
|
return createServer({
|
|
721
|
-
cert:
|
|
722
|
-
key:
|
|
747
|
+
cert: certificate,
|
|
748
|
+
key: privateKey,
|
|
723
749
|
})
|
|
724
750
|
}
|
|
725
751
|
|
|
@@ -727,12 +753,10 @@ const generateResponseDescription = async ({
|
|
|
727
753
|
requestToResponse,
|
|
728
754
|
request,
|
|
729
755
|
pushResponse,
|
|
730
|
-
requestFactory,
|
|
731
756
|
}) => {
|
|
732
757
|
try {
|
|
733
758
|
const returnValue = await requestToResponse(request, {
|
|
734
759
|
pushResponse,
|
|
735
|
-
requestFactory,
|
|
736
760
|
})
|
|
737
761
|
return {
|
|
738
762
|
responseProperties: returnValue,
|