@jsenv/server 7.1.0 → 8.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 (44) hide show
  1. package/README.md +63 -23
  2. package/main.js +32 -17
  3. package/package.json +20 -15
  4. package/src/{internal → content_negotiation}/applyContentNegotiation.js +5 -1
  5. package/src/{internal → content_negotiation}/checkContentNegotiation.js +3 -3
  6. package/src/{negotiateContentEncoding.js → content_negotiation/negotiateContentEncoding.js} +5 -3
  7. package/src/{negotiateContentLanguage.js → content_negotiation/negotiateContentLanguage.js} +9 -5
  8. package/src/{negotiateContentType.js → content_negotiation/negotiateContentType.js} +10 -5
  9. package/src/content_negotiation/plugin_content_negotiation_check.js +15 -0
  10. package/src/cors/plugin_cors.js +156 -0
  11. package/src/fetchUrl.js +12 -7
  12. package/src/findFreePort.js +2 -1
  13. package/src/{composeResponse.js → internal/composeResponse.js} +5 -3
  14. package/src/internal/compositionMappingToCompose.js +26 -6
  15. package/src/{convertFileSystemErrorToResponseProperties.js → internal/convertFileSystemErrorToResponseProperties.js} +0 -0
  16. package/src/internal/getServerOrigins.js +5 -1
  17. package/src/internal/headersFromObject.js +3 -1
  18. package/src/{headersToObject.js → internal/headersToObject.js} +0 -0
  19. package/src/internal/listenClientError.js +5 -1
  20. package/src/internal/listenEvent.js +6 -1
  21. package/src/internal/listenRequest.js +5 -1
  22. package/src/internal/listenServerConnectionError.js +31 -23
  23. package/src/internal/multiple-header.js +11 -5
  24. package/src/internal/nodeRequestToRequest.js +10 -7
  25. package/src/internal/observable.js +5 -1
  26. package/src/internal/populateNodeResponse.js +11 -2
  27. package/src/internal/server-polyglot.js +11 -2
  28. package/src/internal/trackServerPendingConnections.js +15 -11
  29. package/src/internal/trackServerPendingRequests.js +10 -7
  30. package/src/jsenvServerInternalErrorToResponse.js +7 -3
  31. package/src/plugin_request_waiting_check.js +34 -0
  32. package/src/serveFile.js +88 -41
  33. package/src/server_timing/plugin_server_timing.js +28 -0
  34. package/src/{serverTiming.js → server_timing/timing_header.js} +43 -62
  35. package/src/server_timing/timing_measure.js +26 -0
  36. package/src/service_composition/service_composition.js +65 -0
  37. package/src/{createSSERoom.js → sse/createSSERoom.js} +22 -6
  38. package/src/startServer.js +197 -252
  39. package/src/stopReasons.js +3 -1
  40. package/src/urlToContentType.js +13 -6
  41. package/src/jsenvAccessControlAllowedHeaders.js +0 -1
  42. package/src/jsenvAccessControlAllowedMethods.js +0 -1
  43. package/src/service-composition.js +0 -41
  44. package/src/urlToSearchParamValue.js +0 -3
@@ -20,7 +20,11 @@ const createServerOrigin = ({ protocol, ip, port }) => {
20
20
 
21
21
  const ipToHostname = (
22
22
  ip,
23
- { preferLocalhost = true, preferLocalIp = false, preferExternalIp = false } = {},
23
+ {
24
+ preferLocalhost = true,
25
+ preferLocalIp = false,
26
+ preferExternalIp = false,
27
+ } = {},
24
28
  ) => {
25
29
  if (ip === "0.0.0.0" || !ip) {
26
30
  if (preferLocalhost) return "localhost"
@@ -14,7 +14,9 @@ export const headersFromObject = (headersObject) => {
14
14
  // exclude http2 headers
15
15
  return
16
16
  }
17
- headers[normalizeHeaderName(headerName)] = normalizeHeaderValue(headersObject[headerName])
17
+ headers[normalizeHeaderName(headerName)] = normalizeHeaderValue(
18
+ headersObject[headerName],
19
+ )
18
20
  })
19
21
 
20
22
  return headers
@@ -2,7 +2,11 @@ import { listenEvent } from "./listenEvent.js"
2
2
 
3
3
  export const listenClientError = (nodeServer, clientErrorCallback) => {
4
4
  if (nodeServer._httpServer) {
5
- const removeNetClientError = listenEvent(nodeServer, "clientError", clientErrorCallback)
5
+ const removeNetClientError = listenEvent(
6
+ nodeServer,
7
+ "clientError",
8
+ clientErrorCallback,
9
+ )
6
10
  const removeHttpClientError = listenEvent(
7
11
  nodeServer._httpServer,
8
12
  "clientError",
@@ -1,4 +1,9 @@
1
- export const listenEvent = (objectWithEventEmitter, eventName, callback, { once = false } = {}) => {
1
+ export const listenEvent = (
2
+ objectWithEventEmitter,
3
+ eventName,
4
+ callback,
5
+ { once = false } = {},
6
+ ) => {
2
7
  if (once) {
3
8
  objectWithEventEmitter.once(eventName, callback)
4
9
  } else {
@@ -7,7 +7,11 @@ export const listenRequest = (nodeServer, requestCallback) => {
7
7
  "request",
8
8
  requestCallback,
9
9
  )
10
- const removeTlsRequestListener = listenEvent(nodeServer._tlsServer, "request", requestCallback)
10
+ const removeTlsRequestListener = listenEvent(
11
+ nodeServer._tlsServer,
12
+ "request",
13
+ requestCallback,
14
+ )
11
15
  return () => {
12
16
  removeHttpRequestListener()
13
17
  removeTlsRequestListener()
@@ -7,30 +7,38 @@ export const listenServerConnectionError = (
7
7
  ) => {
8
8
  const cleanupSet = new Set()
9
9
 
10
- const removeConnectionListener = listenEvent(nodeServer, "connection", (socket) => {
11
- const removeSocketErrorListener = listenEvent(socket, "error", (error) => {
12
- if (ignoreErrorAfterConnectionIsDestroyed && socket.destroyed) {
13
- return
14
- }
15
- connectionErrorCallback(error, socket)
16
- })
17
- const removeOnceSocketCloseListener = listenEvent(
18
- socket,
19
- "close",
20
- () => {
10
+ const removeConnectionListener = listenEvent(
11
+ nodeServer,
12
+ "connection",
13
+ (socket) => {
14
+ const removeSocketErrorListener = listenEvent(
15
+ socket,
16
+ "error",
17
+ (error) => {
18
+ if (ignoreErrorAfterConnectionIsDestroyed && socket.destroyed) {
19
+ return
20
+ }
21
+ connectionErrorCallback(error, socket)
22
+ },
23
+ )
24
+ const removeOnceSocketCloseListener = listenEvent(
25
+ socket,
26
+ "close",
27
+ () => {
28
+ removeSocketErrorListener()
29
+ cleanupSet.delete(cleanup)
30
+ },
31
+ {
32
+ once: true,
33
+ },
34
+ )
35
+ const cleanup = () => {
21
36
  removeSocketErrorListener()
22
- cleanupSet.delete(cleanup)
23
- },
24
- {
25
- once: true,
26
- },
27
- )
28
- const cleanup = () => {
29
- removeSocketErrorListener()
30
- removeOnceSocketCloseListener()
31
- }
32
- cleanupSet.add(cleanup)
33
- })
37
+ removeOnceSocketCloseListener()
38
+ }
39
+ cleanupSet.add(cleanup)
40
+ },
41
+ )
34
42
  return () => {
35
43
  removeConnectionListener()
36
44
  cleanupSet.forEach((cleanup) => {
@@ -26,7 +26,9 @@ export const parseMultipleHeader = (
26
26
  return
27
27
  }
28
28
 
29
- const properties = parseHeaderProperties(valueParts.slice(1), { validateProperty })
29
+ const properties = parseHeaderProperties(valueParts.slice(1), {
30
+ validateProperty,
31
+ })
30
32
  multipleHeader[name] = properties
31
33
  })
32
34
  return multipleHeader
@@ -51,7 +53,8 @@ const parseHeaderProperties = (headerProperties, { validateProperty }) => {
51
53
 
52
54
  const parseHeaderPropertyValue = (headerPropertyValueString) => {
53
55
  const firstChar = headerPropertyValueString[0]
54
- const lastChar = headerPropertyValueString[headerPropertyValueString.length - 1]
56
+ const lastChar =
57
+ headerPropertyValueString[headerPropertyValueString.length - 1]
55
58
  if (firstChar === '"' && lastChar === '"') {
56
59
  return headerPropertyValueString.slice(1, -1)
57
60
  }
@@ -82,9 +85,12 @@ export const stringifyMultipleHeader = (
82
85
  })
83
86
  .map((name) => {
84
87
  const headerProperties = multipleHeader[name]
85
- const headerPropertiesString = stringifyHeaderProperties(headerProperties, {
86
- validateProperty,
87
- })
88
+ const headerPropertiesString = stringifyHeaderProperties(
89
+ headerProperties,
90
+ {
91
+ validateProperty,
92
+ },
93
+ )
88
94
  if (headerPropertiesString.length) {
89
95
  return `${name};${headerPropertiesString}`
90
96
  }
@@ -1,15 +1,18 @@
1
- import { composeCancellationToken, createCancellationSource } from "@jsenv/cancellation"
1
+ import {
2
+ composeCancellationToken,
3
+ createCancellationSource,
4
+ } from "@jsenv/cancellation"
2
5
  import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
3
6
  import { headersFromObject } from "./headersFromObject.js"
4
7
 
5
- export const nodeRequestToRequest = (nodeRequest, { serverCancellationToken, serverOrigin }) => {
8
+ export const nodeRequestToRequest = (
9
+ nodeRequest,
10
+ { serverCancellationToken, serverOrigin },
11
+ ) => {
6
12
  const { method } = nodeRequest
7
13
  const { url: ressource } = nodeRequest
8
14
  const headers = headersFromObject(nodeRequest.headers)
9
- const body =
10
- method === "POST" || method === "PUT" || method === "PATCH"
11
- ? nodeStreamToObservable(nodeRequest)
12
- : undefined
15
+ const body = nodeStreamToObservable(nodeRequest)
13
16
 
14
17
  let requestOrigin
15
18
  if (nodeRequest.authority) {
@@ -44,7 +47,7 @@ export const nodeRequestToRequest = (nodeRequest, { serverCancellationToken, ser
44
47
  const nodeRequestToCancellationToken = (nodeRequest) => {
45
48
  const { cancel, token } = createCancellationSource()
46
49
  nodeRequest.on("abort", () => {
47
- cancel("request aborted")
50
+ cancel("request aborted by client")
48
51
  })
49
52
  nodeRequest.on("close", () => {
50
53
  cancel("request closed")
@@ -105,7 +105,11 @@ export const createCompositeProducer = ({ cleanup = () => {} } = {}) => {
105
105
  return true
106
106
  }
107
107
 
108
- const producer = ({ next = () => {}, complete = () => {}, error = () => {} }) => {
108
+ const producer = ({
109
+ next = () => {},
110
+ complete = () => {},
111
+ error = () => {},
112
+ }) => {
109
113
  let completeCount = 0
110
114
 
111
115
  const checkComplete = () => {
@@ -5,7 +5,12 @@ import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
5
5
  export const populateNodeResponse = (
6
6
  nodeResponse,
7
7
  { status, statusText, headers, body, bodyEncoding },
8
- { cancellationToken, ignoreBody, ignoreStatusText, ignoreConnectionHeader } = {},
8
+ {
9
+ cancellationToken,
10
+ ignoreBody,
11
+ ignoreStatusText,
12
+ ignoreConnectionHeader,
13
+ } = {},
9
14
  ) => {
10
15
  const nodeHeaders = headersToNodeHeaders(headers, { ignoreConnectionHeader })
11
16
  // nodejs strange signature for writeHead force this
@@ -101,7 +106,11 @@ const isNodeStream = (value) => {
101
106
  return false
102
107
  }
103
108
 
104
- if (value instanceof Stream || value instanceof Writable || value instanceof Readable) {
109
+ if (
110
+ value instanceof Stream ||
111
+ value instanceof Writable ||
112
+ value instanceof Readable
113
+ ) {
105
114
  return true
106
115
  }
107
116
 
@@ -37,11 +37,20 @@ export const createPolyglotServer = async ({
37
37
  return
38
38
  }
39
39
 
40
- const response = [`HTTP/1.1 400 Bad Request`, `Content-Length: 0`, "", ""].join("\r\n")
40
+ const response = [
41
+ `HTTP/1.1 400 Bad Request`,
42
+ `Content-Length: 0`,
43
+ "",
44
+ "",
45
+ ].join("\r\n")
41
46
  socket.write(response)
42
47
  socket.end()
43
48
  socket.destroy()
44
- netServer.emit("clientError", new Error("protocol error, Neither http, nor tls"), socket)
49
+ netServer.emit(
50
+ "clientError",
51
+ new Error("protocol error, Neither http, nor tls"),
52
+ socket,
53
+ )
45
54
  })
46
55
  })
47
56
 
@@ -13,17 +13,21 @@ export const trackServerPendingConnections = (nodeServer, { http2 }) => {
13
13
  const trackHttp1ServerPendingConnections = (nodeServer) => {
14
14
  const pendingConnections = new Set()
15
15
 
16
- const removeConnectionListener = listenEvent(nodeServer, "connection", (connection) => {
17
- pendingConnections.add(connection)
18
- listenEvent(
19
- connection,
20
- "close",
21
- () => {
22
- pendingConnections.delete(connection)
23
- },
24
- { once: true },
25
- )
26
- })
16
+ const removeConnectionListener = listenEvent(
17
+ nodeServer,
18
+ "connection",
19
+ (connection) => {
20
+ pendingConnections.add(connection)
21
+ listenEvent(
22
+ connection,
23
+ "close",
24
+ () => {
25
+ pendingConnections.delete(connection)
26
+ },
27
+ { once: true },
28
+ )
29
+ },
30
+ )
27
31
 
28
32
  const stop = async (reason) => {
29
33
  removeConnectionListener()
@@ -13,13 +13,16 @@ export const trackServerPendingRequests = (nodeServer, { http2 }) => {
13
13
  const trackHttp1ServerPendingRequests = (nodeServer) => {
14
14
  const pendingClients = new Set()
15
15
 
16
- const removeRequestListener = listenRequest(nodeServer, (nodeRequest, nodeResponse) => {
17
- const client = { nodeRequest, nodeResponse }
18
- pendingClients.add(client)
19
- nodeResponse.once("close", () => {
20
- pendingClients.delete(client)
21
- })
22
- })
16
+ const removeRequestListener = listenRequest(
17
+ nodeServer,
18
+ (nodeRequest, nodeResponse) => {
19
+ const client = { nodeRequest, nodeResponse }
20
+ pendingClients.add(client)
21
+ nodeResponse.once("close", () => {
22
+ pendingClients.delete(client)
23
+ })
24
+ },
25
+ )
23
26
 
24
27
  const stop = async ({ status, reason }) => {
25
28
  removeRequestListener()
@@ -1,4 +1,4 @@
1
- import { negotiateContentType } from "./negotiateContentType.js"
1
+ import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
2
2
 
3
3
  export const jsenvServerInternalErrorToResponse = (
4
4
  serverInternalError,
@@ -6,7 +6,8 @@ export const jsenvServerInternalErrorToResponse = (
6
6
  ) => {
7
7
  const serverInternalErrorIsAPrimitive =
8
8
  serverInternalError === null ||
9
- (typeof serverInternalError !== "object" && typeof serverInternalError !== "function")
9
+ (typeof serverInternalError !== "object" &&
10
+ typeof serverInternalError !== "function")
10
11
 
11
12
  const dataToSend = serverInternalErrorIsAPrimitive
12
13
  ? {
@@ -81,6 +82,9 @@ export const jsenvServerInternalErrorToResponse = (
81
82
  }
82
83
  },
83
84
  }
84
- const bestContentType = negotiateContentType(request, Object.keys(availableContentTypes))
85
+ const bestContentType = negotiateContentType(
86
+ request,
87
+ Object.keys(availableContentTypes),
88
+ )
85
89
  return availableContentTypes[bestContentType || "application/json"]()
86
90
  }
@@ -0,0 +1,34 @@
1
+ import { createDetailedMessage } from "@jsenv/logger"
2
+
3
+ export const pluginRequestWaitingCheck = ({
4
+ requestWaitingMs = 20000,
5
+ requestWaitingCallback = ({ request, logger, requestWaitingMs }) => {
6
+ logger.warn(
7
+ createDetailedMessage(
8
+ `still no response found for request after ${requestWaitingMs} ms`,
9
+ {
10
+ "request url": `${request.origin}${request.ressource}`,
11
+ "request headers": JSON.stringify(request.headers, null, " "),
12
+ },
13
+ ),
14
+ )
15
+ },
16
+ } = {}) => {
17
+ return {
18
+ plugin_request_waiting_check: {
19
+ init: () => {},
20
+
21
+ onRequest: (request, { logger }) => {
22
+ const timeout = setTimeout(
23
+ () => requestWaitingCallback({ request, logger, requestWaitingMs }),
24
+ requestWaitingMs,
25
+ )
26
+ return {
27
+ onResponse: () => {
28
+ clearTimeout(timeout)
29
+ },
30
+ }
31
+ },
32
+ },
33
+ }
34
+ }
package/src/serveFile.js CHANGED
@@ -1,5 +1,4 @@
1
- /* eslint-disable import/max-dependencies */
2
- import { createReadStream, promises, statSync } from "node:fs"
1
+ import { createReadStream, statSync } from "node:fs"
3
2
  import {
4
3
  assertAndNormalizeDirectoryUrl,
5
4
  resolveUrl,
@@ -8,21 +7,19 @@ import {
8
7
  readDirectory,
9
8
  urlToFileSystemPath,
10
9
  urlToRelativeUrl,
10
+ readFile,
11
11
  } from "@jsenv/filesystem"
12
12
  import { createOperation } from "@jsenv/cancellation"
13
13
 
14
- import { negotiateContentType } from "./negotiateContentType.js"
15
- import { timeFunction } from "./serverTiming.js"
16
- import { convertFileSystemErrorToResponseProperties } from "./convertFileSystemErrorToResponseProperties.js"
17
- import { urlToContentType } from "./urlToContentType.js"
14
+ import { composeResponse } from "./internal/composeResponse.js"
15
+ import { convertFileSystemErrorToResponseProperties } from "./internal/convertFileSystemErrorToResponseProperties.js"
18
16
  import { jsenvContentTypeMap } from "./jsenvContentTypeMap.js"
19
- import { composeResponse } from "./composeResponse.js"
20
- import { negotiateContentEncoding } from "./negotiateContentEncoding.js"
21
-
22
- const { readFile } = promises
17
+ import { timeFunction } from "./server_timing/timing_measure.js"
18
+ import { negotiateContentType } from "./content_negotiation/negotiateContentType.js"
19
+ import { negotiateContentEncoding } from "./content_negotiation/negotiateContentEncoding.js"
20
+ import { urlToContentType } from "./urlToContentType.js"
23
21
 
24
- const ETAG_CACHE = new Map()
25
- const ETAG_CACHE_MAX_SIZE = 500
22
+ const ETAG_MEMORY_MAP = new Map()
26
23
 
27
24
  export const serveFile = async (
28
25
  request,
@@ -30,11 +27,14 @@ export const serveFile = async (
30
27
  rootDirectoryUrl,
31
28
  contentTypeMap = jsenvContentTypeMap,
32
29
  etagEnabled = false,
33
- etagCacheDisabled = false,
30
+ etagMemory = true,
31
+ etagMemoryMaxSize = 1000,
34
32
  mtimeEnabled = false,
35
33
  compressionEnabled = false,
36
34
  compressionSizeThreshold = 1024,
37
- cacheControl = etagEnabled || mtimeEnabled ? "private,max-age=0,must-revalidate" : "no-store",
35
+ cacheControl = etagEnabled || mtimeEnabled
36
+ ? "private,max-age=0,must-revalidate"
37
+ : "no-store",
38
38
  canReadDirectory = false,
39
39
  readableStreamLifetimeInSeconds = 120,
40
40
  } = {},
@@ -66,7 +66,9 @@ export const serveFile = async (
66
66
  }
67
67
  }
68
68
  if (etagEnabled && mtimeEnabled) {
69
- console.warn(`cannot enable both etag and mtime, mtime disabled in favor of etag.`)
69
+ console.warn(
70
+ `cannot enable both etag and mtime, mtime disabled in favor of etag.`,
71
+ )
70
72
  mtimeEnabled = false
71
73
  }
72
74
 
@@ -81,8 +83,9 @@ export const serveFile = async (
81
83
  const sourceFileSystemPath = urlToFileSystemPath(sourceUrl)
82
84
 
83
85
  try {
84
- const [readStatTiming, sourceStat] = await timeFunction("file service>read file stat", () =>
85
- statSync(sourceFileSystemPath),
86
+ const [readStatTiming, sourceStat] = await timeFunction(
87
+ "file service>read file stat",
88
+ () => statSync(sourceFileSystemPath),
86
89
  )
87
90
 
88
91
  if (sourceStat.isDirectory()) {
@@ -135,7 +138,10 @@ export const serveFile = async (
135
138
  <ul>
136
139
  ${directoryContentArray.map((filename) => {
137
140
  const fileUrl = resolveUrl(filename, sourceUrl)
138
- const fileUrlRelativeToServer = urlToRelativeUrl(fileUrl, rootDirectoryUrl)
141
+ const fileUrlRelativeToServer = urlToRelativeUrl(
142
+ fileUrl,
143
+ rootDirectoryUrl,
144
+ )
139
145
  return `<li>
140
146
  <a href="/${fileUrlRelativeToServer}">${fileUrlRelativeToServer}</a>
141
147
  </li>`
@@ -155,7 +161,10 @@ export const serveFile = async (
155
161
  }
156
162
  },
157
163
  }
158
- const bestContentType = negotiateContentType(request, Object.keys(responseProducers))
164
+ const bestContentType = negotiateContentType(
165
+ request,
166
+ Object.keys(responseProducers),
167
+ )
159
168
  return responseProducers[bestContentType || "application/json"]()
160
169
  }
161
170
 
@@ -169,7 +178,8 @@ export const serveFile = async (
169
178
 
170
179
  const clientCacheResponse = await getClientCacheResponse(request, {
171
180
  etagEnabled,
172
- etagCacheDisabled,
181
+ etagMemory,
182
+ etagMemoryMaxSize,
173
183
  mtimeEnabled,
174
184
  sourceStat,
175
185
  sourceUrl,
@@ -213,7 +223,10 @@ export const serveFile = async (
213
223
  // moreover it will prevent to unlink the file on windows.
214
224
  if (clientCacheResponse.body) {
215
225
  response.body.destroy()
216
- } else if (readableStreamLifetimeInSeconds && readableStreamLifetimeInSeconds !== Infinity) {
226
+ } else if (
227
+ readableStreamLifetimeInSeconds &&
228
+ readableStreamLifetimeInSeconds !== Infinity
229
+ ) {
217
230
  // safe measure, ensure the readable stream gets used in the next ${readableStreamLifetimeInSeconds} otherwise destroys it
218
231
  const timeout = setTimeout(() => {
219
232
  console.warn(
@@ -250,7 +263,14 @@ export const serveFile = async (
250
263
 
251
264
  const getClientCacheResponse = async (
252
265
  request,
253
- { etagEnabled, etagCacheDisabled, mtimeEnabled, sourceStat, sourceUrl },
266
+ {
267
+ etagEnabled,
268
+ etagMemory,
269
+ etagMemoryMaxSize,
270
+ mtimeEnabled,
271
+ sourceStat,
272
+ sourceUrl,
273
+ },
254
274
  ) => {
255
275
  // here you might be tempted to add || headers["cache-control"] === "no-cache"
256
276
  // but no-cache means ressource can be cache but must be revalidated (yeah naming is strange)
@@ -259,7 +279,7 @@ const getClientCacheResponse = async (
259
279
  const { headers = {} } = request
260
280
  if (
261
281
  headers["cache-control"] === "no-store" ||
262
- // let's disable it on no-cache too (https://github.com/jsenv/server/issues/17)
282
+ // let's disable it on no-cache too
263
283
  headers["cache-control"] === "no-cache"
264
284
  ) {
265
285
  return { status: 200 }
@@ -267,7 +287,8 @@ const getClientCacheResponse = async (
267
287
 
268
288
  if (etagEnabled) {
269
289
  return getEtagResponse(request, {
270
- etagCacheDisabled,
290
+ etagMemory,
291
+ etagMemoryMaxSize,
271
292
  sourceStat,
272
293
  sourceUrl,
273
294
  })
@@ -282,15 +303,27 @@ const getClientCacheResponse = async (
282
303
  return { status: 200 }
283
304
  }
284
305
 
285
- const getEtagResponse = async (request, { etagCacheDisabled, sourceUrl, sourceStat }) => {
306
+ const getEtagResponse = async (
307
+ request,
308
+ { etagMemory, etagMemoryMaxSize, sourceUrl, sourceStat },
309
+ ) => {
286
310
  const [computeEtagTiming, fileContentEtag] = await timeFunction(
287
311
  "file service>generate file etag",
288
- () => computeEtag(request, { etagCacheDisabled, sourceUrl, sourceStat }),
312
+ () =>
313
+ computeEtag(request, {
314
+ etagMemory,
315
+ etagMemoryMaxSize,
316
+ sourceUrl,
317
+ sourceStat,
318
+ }),
289
319
  )
290
320
 
291
321
  const { headers = {} } = request
292
322
  const requestHasIfNoneMatchHeader = "if-none-match" in headers
293
- if (requestHasIfNoneMatchHeader && headers["if-none-match"] === fileContentEtag) {
323
+ if (
324
+ requestHasIfNoneMatchHeader &&
325
+ headers["if-none-match"] === fileContentEtag
326
+ ) {
294
327
  return {
295
328
  status: 304,
296
329
  timing: computeEtagTiming,
@@ -306,24 +339,30 @@ const getEtagResponse = async (request, { etagCacheDisabled, sourceUrl, sourceSt
306
339
  }
307
340
  }
308
341
 
309
- const computeEtag = async (request, { etagCacheDisabled, sourceUrl, sourceStat }) => {
310
- if (!etagCacheDisabled) {
311
- const etagCacheEntry = ETAG_CACHE.get(sourceUrl)
312
- if (etagCacheEntry && fileStatAreTheSame(etagCacheEntry.sourceStat, sourceStat)) {
313
- return etagCacheEntry.eTag
342
+ const computeEtag = async (
343
+ request,
344
+ { etagMemory, etagMemoryMaxSize, sourceUrl, sourceStat },
345
+ ) => {
346
+ if (etagMemory) {
347
+ const etagMemoryEntry = ETAG_MEMORY_MAP.get(sourceUrl)
348
+ if (
349
+ etagMemoryEntry &&
350
+ fileStatAreTheSame(etagMemoryEntry.sourceStat, sourceStat)
351
+ ) {
352
+ return etagMemoryEntry.eTag
314
353
  }
315
354
  }
316
355
  const fileContentAsBuffer = await createOperation({
317
356
  cancellationToken: request.cancellationToken,
318
- start: () => readFile(urlToFileSystemPath(sourceUrl)),
357
+ start: () => readFile(sourceUrl, { as: "buffer" }),
319
358
  })
320
359
  const eTag = bufferToEtag(fileContentAsBuffer)
321
- if (!etagCacheDisabled) {
322
- if (ETAG_CACHE.size >= ETAG_CACHE_MAX_SIZE) {
323
- const firstKey = Array.from(ETAG_CACHE.keys())[0]
324
- ETAG_CACHE.delete(firstKey)
360
+ if (etagMemory) {
361
+ if (ETAG_MEMORY_MAP.size >= etagMemoryMaxSize) {
362
+ const firstKey = Array.from(ETAG_MEMORY_MAP.keys())[0]
363
+ ETAG_MEMORY_MAP.delete(firstKey)
325
364
  }
326
- ETAG_CACHE.set(sourceUrl, { sourceStat, eTag })
365
+ ETAG_MEMORY_MAP.set(sourceUrl, { sourceStat, eTag })
327
366
  }
328
367
  return eTag
329
368
  }
@@ -377,7 +416,10 @@ const getMtimeResponse = async (request, { sourceStat }) => {
377
416
  }
378
417
  }
379
418
 
380
- const getCompressedResponse = async (request, { sourceUrl, contentTypeMap }) => {
419
+ const getCompressedResponse = async (
420
+ request,
421
+ { sourceUrl, contentTypeMap },
422
+ ) => {
381
423
  const acceptedCompressionFormat = negotiateContentEncoding(
382
424
  request,
383
425
  Object.keys(availableCompressionFormats),
@@ -387,7 +429,9 @@ const getCompressedResponse = async (request, { sourceUrl, contentTypeMap }) =>
387
429
  }
388
430
 
389
431
  const fileReadableStream = fileUrlToReadableStream(sourceUrl)
390
- const body = await availableCompressionFormats[acceptedCompressionFormat](fileReadableStream)
432
+ const body = await availableCompressionFormats[acceptedCompressionFormat](
433
+ fileReadableStream,
434
+ )
391
435
 
392
436
  return {
393
437
  status: 200,
@@ -419,7 +463,10 @@ const availableCompressionFormats = {
419
463
  },
420
464
  }
421
465
 
422
- const getRawResponse = async (request, { sourceStat, sourceUrl, contentTypeMap }) => {
466
+ const getRawResponse = async (
467
+ request,
468
+ { sourceStat, sourceUrl, contentTypeMap },
469
+ ) => {
423
470
  return {
424
471
  status: 200,
425
472
  headers: {
@@ -0,0 +1,28 @@
1
+ // https://www.w3.org/TR/server-timing/
2
+
3
+ import { performance } from "node:perf_hooks"
4
+
5
+ import { timingToServerTimingResponseHeaders } from "./timing_header.js"
6
+
7
+ export const pluginServerTiming = () => {
8
+ return {
9
+ server_timing: {
10
+ onRequest: () => {
11
+ const requestStartEvent = performance.now()
12
+ return {
13
+ onResponse: (response) => {
14
+ const responseEndEvent = performance.now()
15
+ const timeToCreateResponse = responseEndEvent - requestStartEvent
16
+ const serverTiming = {
17
+ ...response.timing,
18
+ "time to start responding": timeToCreateResponse,
19
+ }
20
+ return {
21
+ headers: timingToServerTimingResponseHeaders(serverTiming),
22
+ }
23
+ },
24
+ }
25
+ },
26
+ },
27
+ }
28
+ }