@jsenv/server 12.8.0 → 13.0.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.
Files changed (25) hide show
  1. package/README.md +68 -50
  2. package/package.json +1 -1
  3. package/src/content_negotiation/{applyContentNegotiation.js → pick_accepted_content.js} +1 -3
  4. package/src/content_negotiation/{negotiateContentEncoding.js → pick_content_encoding.js} +3 -3
  5. package/src/content_negotiation/{negotiateContentLanguage.js → pick_content_language.js} +3 -3
  6. package/src/content_negotiation/{negotiateContentType.js → pick_content_type.js} +3 -3
  7. package/src/fetch_filesystem.js +2 -2
  8. package/src/internal/dns_resolution.js +17 -0
  9. package/src/internal/getServerOrigins.js +37 -26
  10. package/src/internal/listen.js +15 -13
  11. package/src/main.js +11 -13
  12. package/src/serve_directory.js +2 -2
  13. package/src/{startServer.js → server.js} +150 -122
  14. package/src/service_controller.js +173 -0
  15. package/src/{cors/plugin_cors.js → services/cors/jsenv_service_cors.js} +29 -45
  16. package/src/services/error_handler/jsenv_service_error_handler.js +98 -0
  17. package/src/{content_negotiation/checkContentNegotiation.js → services/response_acceptance_check/jsenv_service_response_acceptance_check.js} +16 -7
  18. package/src/services/ressource_aliases/jsenv_service_ressource_aliases.js +40 -0
  19. package/src/sse/{createSSERoom.js → sse_room.js} +18 -16
  20. package/src/content_negotiation/plugin_content_negotiation_check.js +0 -15
  21. package/src/error_to_response_default.js +0 -94
  22. package/src/plugin_request_waiting_check.js +0 -32
  23. package/src/ressource_aliases/plugin_ressource_aliases.js +0 -44
  24. package/src/server_timing/plugin_server_timing.js +0 -28
  25. package/src/service_composition/service_composition.js +0 -55
package/README.md CHANGED
@@ -8,15 +8,19 @@ import { startServer } from "@jsenv/server"
8
8
  await startServer({
9
9
  protocol: "http",
10
10
  port: 8080,
11
- requestToResponse: () => {
12
- return {
13
- status: 200,
14
- headers: {
15
- "content-type": "text/plain",
11
+ services: [
12
+ {
13
+ handleRequest: () => {
14
+ return {
15
+ status: 200,
16
+ headers: {
17
+ "content-type": "text/plain",
18
+ },
19
+ body: "Hello world",
20
+ }
16
21
  },
17
- body: "Hello world",
18
- }
19
- },
22
+ },
23
+ ],
20
24
  })
21
25
  ```
22
26
 
@@ -29,15 +33,19 @@ import fetch from "node-fetch"
29
33
  import { startServer } from "@jsenv/server"
30
34
 
31
35
  const server = await startServer({
32
- requestToResponse: () => {
33
- return {
34
- status: 200,
35
- headers: {
36
- "content-type": "text/plain",
36
+ services: [
37
+ {
38
+ handleRequest: () => {
39
+ return {
40
+ status: 200,
41
+ headers: {
42
+ "content-type": "text/plain",
43
+ },
44
+ body: "Hello world",
45
+ }
37
46
  },
38
- body: "Hello world",
39
- }
40
- },
47
+ },
48
+ ],
41
49
  })
42
50
 
43
51
  const response = await fetch(server.origin)
@@ -57,22 +65,24 @@ _Code starting a server with 2 request handlers:_
57
65
  */
58
66
  import { startServer, composeServices } from "@jsenv/server"
59
67
 
60
- const indexService = (request) => {
61
- if (request.ressource === "/") {
62
- return { status: 200 }
63
- }
64
- return null // means "I don't handle that request"
65
- }
66
-
67
- const notFoundService = (request) => {
68
- return { status: 404 }
69
- }
70
-
71
68
  const server = await startServer({
72
- requestToResponse: composeServices({
73
- index: indexService,
74
- otherwise: notFoundService,
75
- }),
69
+ services: [
70
+ {
71
+ name: "index",
72
+ handleRequest: (request) => {
73
+ if (request.ressource === "/") {
74
+ return { status: 200 }
75
+ }
76
+ return null // means "I don't handle that request"
77
+ },
78
+ },
79
+ {
80
+ name: "otherwise",
81
+ handleRequest: () => {
82
+ return { status: 404 }
83
+ },
84
+ },
85
+ ],
76
86
  })
77
87
 
78
88
  const fetch = await import("node-fetch")
@@ -94,17 +104,21 @@ await startServer({
94
104
  certificate: readFileSyncAsString("./server.crt"),
95
105
  privateKey: readFileSyncAsString("./server.key"),
96
106
  allowHttpRequestOnHttps: true,
97
- requestToResponse: (request) => {
98
- const clientUsesHttp = request.origin.startsWith("http:")
99
-
100
- return {
101
- status: 200,
102
- headers: {
103
- "content-type": "text/plain",
107
+ services: [
108
+ {
109
+ handleRequest: (request) => {
110
+ const clientUsesHttp = request.origin.startsWith("http:")
111
+
112
+ return {
113
+ status: 200,
114
+ headers: {
115
+ "content-type": "text/plain",
116
+ },
117
+ body: clientUsesHttp ? `Welcome http user` : `Welcome https user`,
118
+ }
104
119
  },
105
- body: clientUsesHttp ? `Welcome http user` : `Welcome https user`,
106
- }
107
- },
120
+ },
121
+ ],
108
122
  })
109
123
 
110
124
  function readFileSyncAsString(relativeUrl) {
@@ -119,24 +133,28 @@ _Code starting a server for static files:_
119
133
  import { startServer, fetchFileSystem } from "@jsenv/server"
120
134
 
121
135
  await startServer({
122
- requestToResponse: async (request) => {
123
- const fileUrl = new URL(request.ressource.slice(1), import.meta.url)
124
- const response = await fetchFileSystem(fileUrl, {
125
- ...request,
126
- })
127
- return response
128
- },
136
+ services: [
137
+ {
138
+ handleRequest: async (request) => {
139
+ const fileUrl = new URL(request.ressource.slice(1), import.meta.url)
140
+ const response = await fetchFileSystem(fileUrl, {
141
+ ...request,
142
+ })
143
+ return response
144
+ },
145
+ },
146
+ ],
129
147
  })
130
148
  ```
131
149
 
132
150
  # Documentation
133
151
 
134
- - [https](./docs/https/https.md)
135
- - [Serving files](./docs/serving_files/serving_files.md)
136
152
  - [Handling requests](./docs/handling_requests/handling_requests.md)
137
153
  - [Handling errors](./docs/handling_errors/handling_errors.md)
138
154
  - [Server timing](./docs/server_timing/server_timing.md)
139
155
  - [CORS](./docs/cors/cors.md)
156
+ - [https](./docs/https/https.md)
157
+ - [Serving files](./docs/serving_files/serving_files.md)
140
158
  - [Content negotiation](./docs/content_negotiation/content_negotiation.md)
141
159
  - [Server Sent Events](./docs/sse/sse.md)
142
160
  - [Cluster](./docs/cluster/cluster.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "12.8.0",
3
+ "version": "13.0.0",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,11 +1,10 @@
1
- export const applyContentNegotiation = ({
1
+ export const pickAcceptedContent = ({
2
2
  availables,
3
3
  accepteds,
4
4
  getAcceptanceScore,
5
5
  }) => {
6
6
  let highestScore = -1
7
7
  let availableWithHighestScore = null
8
-
9
8
  let availableIndex = 0
10
9
  while (availableIndex < availables.length) {
11
10
  const available = availables[availableIndex]
@@ -23,6 +22,5 @@ export const applyContentNegotiation = ({
23
22
  }
24
23
  }
25
24
  }
26
-
27
25
  return availableWithHighestScore
28
26
  }
@@ -1,7 +1,7 @@
1
1
  import { parseMultipleHeader } from "../internal/multiple-header.js"
2
- import { applyContentNegotiation } from "./applyContentNegotiation.js"
2
+ import { pickAcceptedContent } from "./pick_accepted_content.js"
3
3
 
4
- export const negotiateContentEncoding = (request, availableEncodings) => {
4
+ export const pickContentEncoding = (request, availableEncodings) => {
5
5
  const { headers = {} } = request
6
6
  const requestAcceptEncodingHeader = headers["accept-encoding"]
7
7
  if (!requestAcceptEncodingHeader) {
@@ -11,7 +11,7 @@ export const negotiateContentEncoding = (request, availableEncodings) => {
11
11
  const encodingsAccepted = parseAcceptEncodingHeader(
12
12
  requestAcceptEncodingHeader,
13
13
  )
14
- return applyContentNegotiation({
14
+ return pickAcceptedContent({
15
15
  accepteds: encodingsAccepted,
16
16
  availables: availableEncodings,
17
17
  getAcceptanceScore: getEncodingAcceptanceScore,
@@ -1,7 +1,7 @@
1
1
  import { parseMultipleHeader } from "../internal/multiple-header.js"
2
- import { applyContentNegotiation } from "./applyContentNegotiation.js"
2
+ import { pickAcceptedContent } from "./pick_accepted_content.js"
3
3
 
4
- export const negotiateContentLanguage = (request, availableLanguages) => {
4
+ export const pickContentLanguage = (request, availableLanguages) => {
5
5
  const { headers = {} } = request
6
6
  const requestAcceptLanguageHeader = headers["accept-language"]
7
7
  if (!requestAcceptLanguageHeader) {
@@ -11,7 +11,7 @@ export const negotiateContentLanguage = (request, availableLanguages) => {
11
11
  const languagesAccepted = parseAcceptLanguageHeader(
12
12
  requestAcceptLanguageHeader,
13
13
  )
14
- return applyContentNegotiation({
14
+ return pickAcceptedContent({
15
15
  accepteds: languagesAccepted,
16
16
  availables: availableLanguages,
17
17
  getAcceptanceScore: getLanguageAcceptanceScore,
@@ -1,7 +1,7 @@
1
1
  import { parseMultipleHeader } from "../internal/multiple-header.js"
2
- import { applyContentNegotiation } from "./applyContentNegotiation.js"
2
+ import { pickAcceptedContent } from "./pick_accepted_content.js"
3
3
 
4
- export const negotiateContentType = (request, availableContentTypes) => {
4
+ export const pickContentType = (request, availableContentTypes) => {
5
5
  const { headers = {} } = request
6
6
  const requestAcceptHeader = headers.accept
7
7
  if (!requestAcceptHeader) {
@@ -9,7 +9,7 @@ export const negotiateContentType = (request, availableContentTypes) => {
9
9
  }
10
10
 
11
11
  const contentTypesAccepted = parseAcceptHeader(requestAcceptHeader)
12
- return applyContentNegotiation({
12
+ return pickAcceptedContent({
13
13
  accepteds: contentTypesAccepted,
14
14
  availables: availableContentTypes,
15
15
  getAcceptanceScore: getContentTypeAcceptanceScore,
@@ -12,7 +12,7 @@ import { bufferToEtag } from "./internal/etag.js"
12
12
  import { composeTwoResponses } from "./internal/response_composition.js"
13
13
  import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js"
14
14
  import { timeFunction } from "./server_timing/timing_measure.js"
15
- import { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js"
15
+ import { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js"
16
16
  import { serveDirectory } from "./serve_directory.js"
17
17
 
18
18
  export const fetchFileSystem = async (
@@ -360,7 +360,7 @@ const getMtimeResponse = async ({ headers, sourceStat }) => {
360
360
  }
361
361
 
362
362
  const getCompressedResponse = async ({ sourceUrl, headers }) => {
363
- const acceptedCompressionFormat = negotiateContentEncoding(
363
+ const acceptedCompressionFormat = pickContentEncoding(
364
364
  { headers },
365
365
  Object.keys(availableCompressionFormats),
366
366
  )
@@ -0,0 +1,17 @@
1
+ import { lookup } from "node:dns"
2
+
3
+ export const applyDnsResolution = async (
4
+ hostname,
5
+ { verbatim = false } = {},
6
+ ) => {
7
+ const dnsResolution = await new Promise((resolve, reject) => {
8
+ lookup(hostname, { verbatim }, (error, address, family) => {
9
+ if (error) {
10
+ reject(error)
11
+ } else {
12
+ resolve({ address, family })
13
+ }
14
+ })
15
+ })
16
+ return dnsResolution
17
+ }
@@ -1,10 +1,11 @@
1
1
  import { networkInterfaces } from "node:os"
2
- import { lookup } from "node:dns"
3
2
 
4
- export const getServerOrigins = async ({ protocol, ip, port }) => {
5
- const isInternalIp = ip === "127.0.0.1"
3
+ import { applyDnsResolution } from "./dns_resolution.js"
4
+
5
+ export const getServerOrigins = async ({ protocol, host, port }) => {
6
+ const isLocal = LOOPBACK_HOSTNAMES.includes(host)
6
7
  const localhostDnsResolution = await applyDnsResolution("localhost")
7
- const internalOrigin = createServerOrigin({
8
+ const localOrigin = createServerOrigin({
8
9
  protocol,
9
10
  hostname:
10
11
  localhostDnsResolution.address === "127.0.0.1"
@@ -12,32 +13,34 @@ export const getServerOrigins = async ({ protocol, ip, port }) => {
12
13
  : "127.0.0.1",
13
14
  port,
14
15
  })
15
- if (isInternalIp) {
16
- return { internal: internalOrigin }
16
+ if (isLocal) {
17
+ return { local: localOrigin }
17
18
  }
18
- const isAnyIp = !ip || ip === "::" || ip === "0.0.0.0"
19
+ const isAnyIp = WILDCARD_HOSTNAMES.includes(host)
20
+ const networkOrigin = createServerOrigin({
21
+ protocol,
22
+ hostname: isAnyIp ? getExternalIp() : host,
23
+ port,
24
+ })
19
25
  return {
20
- internal: internalOrigin,
21
- external: createServerOrigin({
22
- protocol,
23
- hostname: isAnyIp ? getExternalIp(ip) : ip,
24
- port,
25
- }),
26
+ local: localOrigin,
27
+ network: networkOrigin,
26
28
  }
27
29
  }
28
30
 
29
- const applyDnsResolution = async (hostname) => {
30
- const dnsResolution = await new Promise((resolve, reject) => {
31
- lookup(hostname, (error, address, family) => {
32
- if (error) {
33
- reject(error)
34
- } else {
35
- resolve({ address, family })
36
- }
37
- })
38
- })
39
- return dnsResolution
40
- }
31
+ const LOOPBACK_HOSTNAMES = [
32
+ "localhost",
33
+ "127.0.0.1",
34
+ "::1",
35
+ "0000:0000:0000:0000:0000:0000:0000:0001",
36
+ ]
37
+
38
+ const WILDCARD_HOSTNAMES = [
39
+ undefined,
40
+ "0.0.0.0",
41
+ "::",
42
+ "0000:0000:0000:0000:0000:0000:0000:0000",
43
+ ]
41
44
 
42
45
  const createServerOrigin = ({ protocol, hostname, port }) => {
43
46
  const url = new URL("https://127.0.0.1:80")
@@ -54,10 +57,18 @@ const getExternalIp = () => {
54
57
  const networkAddressArray = networkInterfaceMap[key]
55
58
  return networkAddressArray.find((networkAddress) => {
56
59
  if (networkAddress.internal) return false
57
- if (networkAddress.family !== "IPv4") return false
60
+ if (!isIpV4(networkAddress)) return false
58
61
  internalIPV4NetworkAddress = networkAddress
59
62
  return true
60
63
  })
61
64
  })
62
65
  return internalIPV4NetworkAddress ? internalIPV4NetworkAddress.address : null
63
66
  }
67
+
68
+ const isIpV4 = (networkAddress) => {
69
+ // node 18+
70
+ if (typeof networkAddress.family === "number") {
71
+ return networkAddress.family === 4
72
+ }
73
+ return networkAddress.family === "IPv4"
74
+ }
@@ -6,7 +6,7 @@ const listen = async ({
6
6
  server,
7
7
  port,
8
8
  portHint,
9
- ip,
9
+ host,
10
10
  }) => {
11
11
  const listeningOperation = Abort.startOperation()
12
12
 
@@ -17,11 +17,11 @@ const listen = async ({
17
17
  listeningOperation.throwIfAborted()
18
18
  port = await findFreePort(portHint, {
19
19
  signal: listeningOperation.signal,
20
- ip,
20
+ host,
21
21
  })
22
22
  }
23
23
  listeningOperation.throwIfAborted()
24
- port = await startListening({ server, port, ip })
24
+ port = await startListening({ server, port, host })
25
25
  listeningOperation.addAbortCallback(() => stopListening(server))
26
26
  listeningOperation.throwIfAborted()
27
27
 
@@ -35,7 +35,7 @@ export const findFreePort = async (
35
35
  initialPort = 1,
36
36
  {
37
37
  signal = new AbortController().signal,
38
- ip = "127.0.0.1",
38
+ host = "127.0.0.1",
39
39
  min = 1,
40
40
  max = 65534,
41
41
  next = (port) => port + 1,
@@ -46,34 +46,36 @@ export const findFreePort = async (
46
46
  findFreePortOperation.addAbortSignal(signal)
47
47
  findFreePortOperation.throwIfAborted()
48
48
 
49
- const testUntil = async (port, ip) => {
49
+ const testUntil = async (port, host) => {
50
50
  findFreePortOperation.throwIfAborted()
51
- const free = await portIsFree(port, ip)
51
+ const free = await portIsFree(port, host)
52
52
  if (free) {
53
53
  return port
54
54
  }
55
55
 
56
56
  const nextPort = next(port)
57
57
  if (nextPort > max) {
58
- throw new Error(`${ip} has no available port between ${min} and ${max}`)
58
+ throw new Error(
59
+ `${host} has no available port between ${min} and ${max}`,
60
+ )
59
61
  }
60
- return testUntil(nextPort, ip)
62
+ return testUntil(nextPort, host)
61
63
  }
62
- const freePort = await testUntil(initialPort, ip)
64
+ const freePort = await testUntil(initialPort, host)
63
65
  return freePort
64
66
  } finally {
65
67
  await findFreePortOperation.end()
66
68
  }
67
69
  }
68
70
 
69
- const portIsFree = async (port, ip) => {
71
+ const portIsFree = async (port, host) => {
70
72
  const server = createServer()
71
73
 
72
74
  try {
73
75
  await startListening({
74
76
  server,
75
77
  port,
76
- ip,
78
+ host,
77
79
  })
78
80
  } catch (error) {
79
81
  if (error && error.code === "EADDRINUSE") {
@@ -89,7 +91,7 @@ const portIsFree = async (port, ip) => {
89
91
  return true
90
92
  }
91
93
 
92
- const startListening = ({ server, port, ip }) => {
94
+ const startListening = ({ server, port, host }) => {
93
95
  return new Promise((resolve, reject) => {
94
96
  server.on("error", reject)
95
97
  server.on("listening", () => {
@@ -97,7 +99,7 @@ const startListening = ({ server, port, ip }) => {
97
99
  // https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback
98
100
  resolve(server.address().port)
99
101
  })
100
- server.listen(port, ip)
102
+ server.listen(port, host)
101
103
  })
102
104
  }
103
105
 
package/src/main.js CHANGED
@@ -4,8 +4,7 @@
4
4
  * Ideally this file should be kept simple to help discovering codebase progressively.
5
5
  */
6
6
 
7
- export { startServer } from "./startServer.js"
8
- export { composeServices } from "./service_composition/service_composition.js"
7
+ export { startServer } from "./server.js"
9
8
  export { setupRoutes } from "./service_composition/routing.js"
10
9
  export { readRequestBody } from "./readRequestBody.js"
11
10
  export { fetchFileSystem } from "./fetch_filesystem.js"
@@ -18,31 +17,30 @@ export {
18
17
  STOP_REASON_PROCESS_EXIT,
19
18
  STOP_REASON_NOT_SPECIFIED,
20
19
  } from "./stopReasons.js"
20
+ export { jsenvServiceErrorHandler } from "./services/error_handler/jsenv_service_error_handler.js"
21
21
 
22
22
  // CORS
23
23
  export {
24
- pluginCORS,
24
+ jsenvServiceCORS,
25
25
  jsenvAccessControlAllowedHeaders,
26
26
  jsenvAccessControlAllowedMethods,
27
- } from "./cors/plugin_cors.js"
27
+ } from "./services/cors/jsenv_service_cors.js"
28
28
 
29
- // server timings
30
- export { pluginServerTiming } from "./server_timing/plugin_server_timing.js"
29
+ // server timing
31
30
  export { timeFunction, timeStart } from "./server_timing/timing_measure.js"
32
31
 
33
32
  // SSE
34
- export { createSSERoom } from "./sse/createSSERoom.js"
33
+ export { createSSERoom } from "./sse/sse_room.js"
35
34
 
36
35
  // content-negotiation
37
- export { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
38
- export { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js"
39
- export { negotiateContentLanguage } from "./content_negotiation/negotiateContentLanguage.js"
40
- export { pluginContentNegotiationCheck } from "./content_negotiation/plugin_content_negotiation_check.js"
36
+ export { pickContentType } from "./content_negotiation/pick_content_type.js"
37
+ export { pickContentEncoding } from "./content_negotiation/pick_content_encoding.js"
38
+ export { pickContentLanguage } from "./content_negotiation/pick_content_language.js"
39
+ export { jsenvServiceResponseAcceptanceCheck } from "./services/response_acceptance_check/jsenv_service_response_acceptance_check.js"
41
40
 
42
41
  // others
43
42
  export { serveDirectory } from "./serve_directory.js"
44
43
  export { fromFetchResponse } from "./from_fetch_response.js"
45
44
  export { composeTwoResponses } from "./internal/response_composition.js"
46
- export { pluginRessourceAliases } from "./ressource_aliases/plugin_ressource_aliases.js"
47
- export { pluginRequestWaitingCheck } from "./plugin_request_waiting_check.js"
45
+ export { jsenvServiceRessourceAliases } from "./services/ressource_aliases/jsenv_service_ressource_aliases.js"
48
46
  export { findFreePort } from "./internal/listen.js"
@@ -1,6 +1,6 @@
1
1
  import { readdirSync } from "node:fs"
2
2
 
3
- import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
3
+ import { pickContentType } from "./content_negotiation/pick_content_type.js"
4
4
 
5
5
  export const serveDirectory = (
6
6
  url,
@@ -57,7 +57,7 @@ export const serveDirectory = (
57
57
  }
58
58
  },
59
59
  }
60
- const bestContentType = negotiateContentType(
60
+ const bestContentType = pickContentType(
61
61
  { headers },
62
62
  Object.keys(responseProducers),
63
63
  )